From 1086a2c22af5bd76e43cbc084a0da4dfef531b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Fri, 3 Apr 2026 08:29:23 +0300 Subject: [PATCH] feat: implement project management UI with configuration editing and service control capabilities --- config/apache_project.conf.template | 2 +- config/apache_project_alias.conf.template | 3 +- service_events.json | 475 ++++++++++++++++++ src/main/ipc/index.ts | 9 + src/main/services/ProcessManager.ts | 90 +++- src/renderer/src/App.tsx | 27 +- .../components/Dashboard/DashboardDetails.tsx | 358 +++++++------ .../ProjectManager/ProjectDetails.tsx | 421 +++++++++++----- .../ProjectManager/ProjectManager.tsx | 6 + 9 files changed, 1070 insertions(+), 321 deletions(-) diff --git a/config/apache_project.conf.template b/config/apache_project.conf.template index 85fb296..6244793 100644 --- a/config/apache_project.conf.template +++ b/config/apache_project.conf.template @@ -17,7 +17,7 @@ Alias /{{HOST}} "{{PATH}}" # Directory index handling DirectoryIndex index.php index.html - # PHP Handling via mod_actions and CGI + # PHP Handling via mod_actions and CGI (EN STABİL YÖNTEM) SetHandler application/x-httpd-php-alias-{{HOST}} diff --git a/config/apache_project_alias.conf.template b/config/apache_project_alias.conf.template index 86182ac..6763523 100644 --- a/config/apache_project_alias.conf.template +++ b/config/apache_project_alias.conf.template @@ -15,13 +15,12 @@ Alias /{{HOST}} "{{PATH}}" # Directory index handling DirectoryIndex index.php index.html - # PHP Handling via mod_actions and CGI + # PHP Handling via mod_actions and CGI (EN STABİL YÖNTEM) SetHandler application/x-httpd-php-alias-{{HOST}} # Action maps the handler to the binary via the ScriptAlias URL - # We use a unique handler name to avoid conflict with VirtualHost config Action application/x-httpd-php-alias-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe" diff --git a/service_events.json b/service_events.json index 8f0988e..029566b 100644 --- a/service_events.json +++ b/service_events.json @@ -1,4 +1,479 @@ [ + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "j8d2w3b0b", + "timestamp": 1775190043284 + }, + { + "service": "php:7.4.33", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "7znzrdg8z", + "timestamp": 1775190035992 + }, + { + "service": "php:7.4.33", + "action": "stop", + "status": "info", + "id": "j8a2rse2x", + "timestamp": 1775190035979 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "wvqa01nia", + "timestamp": 1775189710583 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "cqyq3n6g6", + "timestamp": 1775189708710 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "8w0xss4vz", + "timestamp": 1775189708296 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "hje4qf5if", + "timestamp": 1775189706744 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "aa18fbyov", + "timestamp": 1775189669988 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "836elga8m", + "timestamp": 1775189668534 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "vcchnrqv4", + "timestamp": 1775189668094 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "86dmwkrb6", + "timestamp": 1775189666371 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "h9smf28xf", + "timestamp": 1775189575757 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "9f9tdutcl", + "timestamp": 1775189573903 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "n5cifasnf", + "timestamp": 1775189573509 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "o0sv49py5", + "timestamp": 1775189571988 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "g2dcxneob", + "timestamp": 1775189512213 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "s4w0hso14", + "timestamp": 1775189511574 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "60srbq5kj", + "timestamp": 1775189511560 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "9djtroc4u", + "timestamp": 1775189505056 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "guzt36y9u", + "timestamp": 1775189503604 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "zbc1wz7ab", + "timestamp": 1775189503250 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "opsbvh3zz", + "timestamp": 1775189501545 + }, + { + "service": "php:7.4.33", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "jwinj3nx9", + "timestamp": 1775189460996 + }, + { + "service": "php:7.4.33", + "action": "stop", + "status": "info", + "id": "4apqt056j", + "timestamp": 1775189460983 + }, + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "gajw98k0v", + "timestamp": 1775189436336 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "5mm3ugn4m", + "timestamp": 1775189436319 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "8aexj2zsk", + "timestamp": 1775189436300 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "gsbcbtiip", + "timestamp": 1775189436282 + }, + { + "service": "mariadb:11.4.2", + "action": "start", + "status": "success", + "id": "wtiut8ui0", + "timestamp": 1775189436067 + }, + { + "service": "mariadb:10.11.8", + "action": "start", + "status": "success", + "id": "gilbat8fo", + "timestamp": 1775189436048 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "5e3zmv9cj", + "timestamp": 1775189435299 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "imro81imn", + "timestamp": 1775189435236 + }, + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "13wv7rbx0", + "timestamp": 1775189327354 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "lh2mwoaqt", + "timestamp": 1775189327336 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "r6yxuczum", + "timestamp": 1775189327316 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "7kteqeipa", + "timestamp": 1775189327297 + }, + { + "service": "mariadb:11.4.2", + "action": "start", + "status": "success", + "id": "ow4rm4q2g", + "timestamp": 1775189327084 + }, + { + "service": "mariadb:10.11.8", + "action": "start", + "status": "success", + "id": "860heaux5", + "timestamp": 1775189327064 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "3spv9kr33", + "timestamp": 1775189326302 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "0ofwueil1", + "timestamp": 1775189326240 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "poysy0w5h", + "timestamp": 1775189293667 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "pw764jton", + "timestamp": 1775189291498 + }, + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "gqufxbkr6", + "timestamp": 1775189263980 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "ahje87h73", + "timestamp": 1775189263963 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "lgx6crt2r", + "timestamp": 1775189263945 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "1f4yp29hz", + "timestamp": 1775189263928 + }, + { + "service": "mariadb:11.4.2", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "c7t0f38mx", + "timestamp": 1775189261047 + }, + { + "service": "mariadb:10.11.8", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "wjumgnczg", + "timestamp": 1775189261040 + }, + { + "service": "php:5.6.40", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "13pa3vc9u", + "timestamp": 1775189261030 + }, + { + "service": "php:8.2.30", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "aq8u5y88i", + "timestamp": 1775189261021 + }, + { + "service": "php:8.5.4", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "31mnxniia", + "timestamp": 1775189261013 + }, + { + "service": "apache", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "qimhn6z16", + "timestamp": 1775189260993 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "s1qf1o0x9", + "timestamp": 1775189260990 + }, + { + "service": "mariadb:11.4.2", + "action": "stop", + "status": "info", + "id": "ay9u34n91", + "timestamp": 1775189260985 + }, + { + "service": "mariadb:10.11.8", + "action": "stop", + "status": "info", + "id": "ywurfx52h", + "timestamp": 1775189260978 + }, + { + "service": "php:5.6.40", + "action": "stop", + "status": "info", + "id": "dkyu7sbnb", + "timestamp": 1775189260970 + }, + { + "service": "php:8.2.30", + "action": "stop", + "status": "info", + "id": "u8sax0fs7", + "timestamp": 1775189260963 + }, + { + "service": "php:8.5.4", + "action": "stop", + "status": "info", + "id": "trebtxdcb", + "timestamp": 1775189260954 + }, + { + "service": "apache", + "action": "stop", + "status": "info", + "id": "1yc7n0aro", + "timestamp": 1775189260943 + }, + { + "service": "nginx", + "action": "stop", + "status": "info", + "id": "fnu72gjoj", + "timestamp": 1775189260941 + }, + { + "service": "php:7.4.33", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "hpzynrq5o", + "timestamp": 1775189223398 + }, + { + "service": "php:7.4.33", + "action": "stop", + "status": "info", + "id": "dzl7dzb31", + "timestamp": 1775189223385 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "gwwjvpm9p", + "timestamp": 1775189213352 + }, + { + "service": "php:7.4.33", + "action": "exit", + "status": "failure", + "details": "Exit code: null", + "id": "h0ckcojy3", + "timestamp": 1775189204176 + }, + { + "service": "php:7.4.33", + "action": "stop", + "status": "info", + "id": "54utk28a1", + "timestamp": 1775189204163 + }, { "service": "php:5.6.40", "action": "start", diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 6572bd1..9022a06 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -706,6 +706,15 @@ export function registerIpcHandlers(): void { } }) + ipcMain.handle('services:stop-all', async () => { + try { + await processManager.aggressiveKillAll() + return { success: true, message: 'Tüm servisler zorla durduruldu.' } + } catch (error: any) { + return { success: false, message: `Durdurma hatası: ${error.message}` } + } + }) + ipcMain.handle('services:status', async () => { return processManager.getStatuses() }) diff --git a/src/main/services/ProcessManager.ts b/src/main/services/ProcessManager.ts index 978289a..04fa3bd 100644 --- a/src/main/services/ProcessManager.ts +++ b/src/main/services/ProcessManager.ts @@ -235,6 +235,13 @@ export class ProcessManager { // Also force kill lingering processes even if not in map const result = await this.forceKillAll(name) + + // Final cleanup: Kill anything on the port + const port = this.getServicePort(name) + if (port) { + await this.killByPort(name, port) + } + this.states.set(name, 'stopped') return result } @@ -271,15 +278,33 @@ export class ProcessManager { } } else { if (isWindows) { - const executableName = name === 'nginx' ? 'nginx.exe' : name === 'apache' ? 'httpd.exe' : name === 'php' ? 'php-cgi.exe' : 'mariadbd.exe' - spawn('taskkill', ['/F', '/IM', executableName, '/T'], { windowsHide: true }) - .on('close', () => resolve(true)) - .on('error', () => resolve(true)) + let executableName = '' + if (name === 'nginx') executableName = 'nginx.exe' + else if (name === 'apache') executableName = 'httpd.exe' + else if (name.startsWith('php')) executableName = 'php-cgi.exe' + else if (name.startsWith('mariadb') || name.startsWith('mysql')) executableName = 'mariadbd.exe' + + if (executableName) { + spawn('taskkill', ['/F', '/IM', executableName, '/T'], { windowsHide: true }) + .on('close', () => resolve(true)) + .on('error', () => resolve(true)) + } else { + resolve(true) + } } else { - const processName = name === 'nginx' ? 'nginx' : name === 'apache' ? 'httpd' : name === 'php' ? 'php-cgi' : 'mariadbd' - spawn('pkill', ['-f', processName], { windowsHide: true }) - .on('close', () => resolve(true)) - .on('error', () => resolve(true)) + let processName = '' + if (name === 'nginx') processName = 'nginx' + else if (name === 'apache') processName = 'httpd' + else if (name.startsWith('php')) processName = 'php-cgi' + else if (name.startsWith('mariadb') || name.startsWith('mysql')) processName = 'mariadbd' + + if (processName) { + spawn('pkill', ['-f', processName], { windowsHide: true }) + .on('close', () => resolve(true)) + .on('error', () => resolve(true)) + } else { + resolve(true) + } } } } else { @@ -470,10 +495,53 @@ export class ProcessManager { return Array.from(new Set(foundLogs)).slice(-count) } - stopAll(): void { - for (const name of this.processes.keys()) { - this.stopService(name) + async aggressiveKillAll(): Promise { + const isWindows = process.platform === 'win32'; + const executables = ['nginx.exe', 'httpd.exe', 'php-cgi.exe', 'mariadbd.exe', 'mysqld.exe']; + const procNames = ['nginx', 'httpd', 'php-cgi', 'mariadbd', 'mysqld']; + + this.addLog('system', 'Aggressive stop initiated. Killing all service processes...') + + if (isWindows) { + for (const exe of executables) { + spawn('taskkill', ['/F', '/IM', exe, '/T'], { windowsHide: true }) + } + } else { + for (const proc of procNames) { + spawn('pkill', ['-9', '-f', proc], { windowsHide: true }) + } } + + // Kill all configured ports + const settings = configService.getSettings() + const portsToKill = new Set() + + if (settings.nginxPort) portsToKill.add(parseInt(settings.nginxPort)) + if (settings.apachePort) portsToKill.add(parseInt(settings.apachePort)) + if (settings.phpPort) portsToKill.add(parseInt(settings.phpPort)) + if (settings.mariaDbPort) portsToKill.add(parseInt(settings.mariaDbPort)) + + // Include versioned ports if any + if (settings.mariaDbPorts) { + Object.values(settings.mariaDbPorts).forEach((p: any) => portsToKill.add(parseInt(p))) + } + + for (const port of portsToKill) { + if (!isNaN(port)) { + await this.killByPort('system', port) + } + } + + this.processes.clear() + for (const key of this.states.keys()) { + this.states.set(key, 'stopped') + } + + this.addLog('system', 'All services aggressively stopped.') + } + + stopAll(): void { + this.aggressiveKillAll().catch(e => console.error('Failed to stop all services:', e)) } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index b26bafd..a8103a8 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -488,19 +488,22 @@ function App(): JSX.Element { const handleStopAll = async () => { if (!window.api) return - const criticalServices = ['nginx', 'apache'] - const phpToStop = Object.keys(status).filter(s => s.startsWith('php:') && (status[s]?.status === 'running' || status[s]?.status === 'starting')) - const mariaDbToStop = Object.keys(status).filter(s => (s.startsWith('mariadb:') || s.startsWith('mysql:')) && (status[s]?.status === 'running' || status[s]?.status === 'starting')) - - await Promise.all([ - ...criticalServices.filter(s => status[s]?.status === 'running' || status[s]?.status === 'starting').map(s => window.api?.stopService(s)), - ...phpToStop.map(s => window.api?.stopService(s)), - ...mariaDbToStop.map(s => window.api?.stopService(s)), - window.api?.stopService('mariadb') // Also stop default - ]) + + // 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 result = await window.api.invoke('services:stop-all') + fetchStatus() - setNotification({ open: true, message: t('dashboard.all_stopped'), severity: 'success' }) + setNotification({ + open: true, + message: result.success ? t('dashboard.all_stopped') : result.message, + severity: result.success ? 'success' : 'error' + }) } const formatMemory = (bytes?: number) => { @@ -1587,6 +1590,8 @@ function App(): JSX.Element { projects={projects} status={status} settings={settings} + phpVersions={phpVersions} + mariaDbVersions={mariaDbVersions} projectSearch={projectSearch} setProjectSearch={setProjectSearch} selectedProjectId={selectedProjectId} diff --git a/src/renderer/src/components/Dashboard/DashboardDetails.tsx b/src/renderer/src/components/Dashboard/DashboardDetails.tsx index bfea0d8..e906612 100644 --- a/src/renderer/src/components/Dashboard/DashboardDetails.tsx +++ b/src/renderer/src/components/Dashboard/DashboardDetails.tsx @@ -14,7 +14,10 @@ import { Divider, LinearProgress, CircularProgress, - Stack + Stack, + Tooltip, + Chip, + Avatar } from '@mui/material' import { Refresh as RefreshIcon, @@ -26,7 +29,10 @@ import { Language as WebIcon, Dns as ServerIcon, Code as PhpIcon, - Storage as DbIcon + Storage as DbIcon, + Settings as SettingsIcon, + Assignment as LogIcon, + Analytics as StatusIcon } from '@mui/icons-material' import { useState, useEffect, useRef } from 'react' import 'prismjs/themes/prism-tomorrow.css' @@ -58,9 +64,10 @@ export default function DashboardDetails({ onReloadService, onSelectProject }: DashboardDetailsProps) { - const [activeTab, setActiveTab] = useState(0); + const [activeTab, setActiveTab] = useState('overview'); const [configContent, setConfigContent] = useState(''); const [projectConfigContent, setProjectConfigContent] = useState(''); + const [liveConfigContent, setLiveConfigContent] = useState(''); const [isLoadingConfig, setIsLoadingConfig] = useState(false); const [logs, setLogs] = useState([]); const logEndRef = useRef(null); @@ -100,17 +107,31 @@ export default function DashboardDetails({ 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 fetchLiveConfig = async (sid: string) => { + setIsLoadingConfig(true); + try { + const res = await window.api.invoke('config:service-file:get', sid); + if (res && res.success) { + setLiveConfigContent(res.content || ''); + } else { + setLiveConfigContent(''); + } + } catch (error) { + setLiveConfigContent(''); + } finally { + setIsLoadingConfig(false); + } + }; + const saveConfig = async (templateName: string, content: string) => { const res = await window.api.invoke('config:template:save', { templateName, content }); if (res.success) { @@ -118,14 +139,27 @@ export default function DashboardDetails({ title: t('common.success'), text: res.message, icon: 'success', + toast: true, + position: 'top-end', + showConfirmButton: false, + timer: 3000, background: '#1e1e1e', color: '#fff' }); - } else { + } + }; + + const saveLiveConfig = async (sid: string, content: string) => { + const res = await window.api.invoke('config:service-file:save', { serviceId: sid, content }); + if (res.success) { Swal.fire({ - title: t('common.error'), + title: t('common.success'), text: res.message, - icon: 'error', + icon: 'success', + toast: true, + position: 'top-end', + showConfirmButton: false, + timer: 3000, background: '#1e1e1e', color: '#fff' }); @@ -138,38 +172,41 @@ export default function DashboardDetails({ 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; + const getHighlightLanguage = (name: string) => { + if (!name) return Prism.languages.clike; + if (name.includes('nginx')) return Prism.languages.nginx; + if (name.includes('httpd') || name.includes('apache')) return Prism.languages.apacheconf; + if (name.includes('.ini') || name.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'; + const getHighlightLabel = (name: string) => { + if (!name) return 'clike'; + if (name.includes('nginx')) return 'nginx'; + if (name.includes('httpd') || name.includes('apache')) return 'apacheconf'; + if (name.includes('.ini') || name.includes('.conf')) return 'ini'; return 'clike'; }; useEffect(() => { - if (activeTab === 1) fetchConfig(getTemplateName(), setConfigContent); - if (activeTab === 2) { + if (activeTab === 'settings_template') fetchConfig(getTemplateName(), setConfigContent); + if (activeTab === 'projects_template') { const templates = getProjectTemplateNames(); if (templates.length > 0) fetchConfig(templates[0], setProjectConfigContent); } - if (activeTab === 3) { + if (activeTab === 'logs') { fetchLogs(); const interval = setInterval(fetchLogs, 3000); return () => clearInterval(interval); } + if (activeTab === 'settings_live' && serviceId) { + fetchLiveConfig(serviceId); + } return undefined; }, [activeTab, serviceId]); useEffect(() => { - if (activeTab === 3 && logEndRef.current) { + if (activeTab === 'logs' && logEndRef.current) { logEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [logs, activeTab]); @@ -186,7 +223,6 @@ export default function DashboardDetails({ const getPort = () => { if (currentStatus.port) return currentStatus.port; - // Fallback for stopped services or missing data if (serviceId === 'nginx') return settings.nginxPort; if (serviceId === 'apache') return settings.apachePort; if (serviceType === 'php') { @@ -210,109 +246,119 @@ export default function DashboardDetails({ 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) + if (serviceType === 'mariadb') return true; return false; }); const ResourceMonitor = ({ cpu, memory }: { cpu?: number; memory?: number }) => ( - + - CPU - {cpu || 0}% + CPU USAGE + {cpu || 0}% - + - RAM - + RAM USAGE + {memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'} - + ); + const isPhp = serviceType === 'php'; + return ( - - - - {serviceType === 'nginx' && } - {serviceType === 'apache' && } - {serviceType === 'php' && } - {serviceType === 'mariadb' && } + + + + {serviceType === 'nginx' && } + {serviceType === 'apache' && } + {serviceType === 'php' && } + {serviceType === 'mariadb' && } - {serviceId.toUpperCase()} - - {isStarting && {t('common.starting', 'Başlatılıyor...')}} - {isStopping && {t('common.stopping', 'Durduruluyor...')}} - {isRunning && {t('common.running', 'Çalışıyor')}} - {!isRunning && !isStarting && !isStopping && {t('common.stopped', 'Durduruldu')}} + {serviceId.toUpperCase()} + + + {isStarting && {t('common.starting').toUpperCase()}} + {isStopping && {t('common.stopping').toUpperCase()}} + {isRunning && {t('common.running').toUpperCase()}} + {!isRunning && !isStarting && !isStopping && {t('common.stopped').toUpperCase()}} - + {isRunning && !isTransitioning && ( - onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main' }}> - - + + 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 }}> + + + )} - - setActiveTab(v)}> - - - {(serviceId === 'nginx' || serviceId === 'apache' || serviceType === 'php') && ( - + + setActiveTab(v)} textColor="primary" indicatorColor="primary"> + + + {isPhp && } + {(serviceId === 'nginx' || serviceId === 'apache' || isPhp) && ( + )} - - + + - - {activeTab === 0 && ( + + {activeTab === 'overview' && ( - {t('dashboard.system_stats')} - + + {t('dashboard.system_stats')} + + - {t('dashboard.service_info')} - - - - {t('common.port')} - - {getPort()} - + + {t('dashboard.service_info')} + + + + + {t('common.port').toUpperCase()} + + + {getPort()} + + - - - {t('common.status')} - - {(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()} - + + + {t('common.status').toUpperCase()} + @@ -320,46 +366,34 @@ export default function DashboardDetails({ )} - {activeTab === 1 && ( + {(activeTab === 'settings_template' || activeTab === 'settings_live') && ( - - - {getTemplateName().toUpperCase()} + + + {activeTab === 'settings_live' ? 'php.ini (ACTIVE)' : getTemplateName().toUpperCase()} - + {isLoadingConfig ? ( ) : ( Prism.highlight(code, getHighlightLanguage(getTemplateName()), getHighlightLabel(getTemplateName()))} - padding={20} - style={{ - fontFamily: '"Fira Code", "Consolas", monospace', - fontSize: '14px', - minHeight: '100%', - outline: 'none' - }} + value={activeTab === 'settings_live' ? liveConfigContent : configContent} + onValueChange={activeTab === 'settings_live' ? setLiveConfigContent : setConfigContent} + highlight={code => Prism.highlight(code, getHighlightLanguage(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()), getHighlightLabel(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()))} + padding={25} + style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }} textareaClassName="editor-textarea" /> @@ -368,31 +402,23 @@ export default function DashboardDetails({ )} - {activeTab === 2 && ( + {activeTab === 'projects_template' && ( - - + + {getProjectTemplateNames()[0]?.toUpperCase()} - + {isLoadingConfig ? ( ) : ( @@ -401,13 +427,8 @@ export default function DashboardDetails({ 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' - }} + padding={25} + style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }} textareaClassName="editor-textarea" /> @@ -416,44 +437,67 @@ export default function DashboardDetails({ )} - {activeTab === 3 && ( - - {logs.map((log, i) => ( - - {log} - - ))} + {activeTab === 'logs' && ( + + + + REALTIME SERVICE LOGS + + {logs.length === 0 ? ( + + {t('diagnostics.noLogs')} + + ) : ( + logs.map((log, i) => ( + + {log} + + )) + )}
)} - {activeTab === 4 && ( - + {activeTab === 'projects' && ( + {serviceProjects.length === 0 ? ( - {t('projects.empty')} + + + {t('projects.empty')} + ) : ( - serviceProjects.map((p, idx) => ( - - - + serviceProjects.map((p_item, idx) => ( + + + + + - onSelectProject(p.id)} color="primary"> - - + - {idx < serviceProjects.length - 1 && } + {idx < serviceProjects.length - 1 && } )) )} diff --git a/src/renderer/src/components/ProjectManager/ProjectDetails.tsx b/src/renderer/src/components/ProjectManager/ProjectDetails.tsx index 314ef40..6d744c2 100644 --- a/src/renderer/src/components/ProjectManager/ProjectDetails.tsx +++ b/src/renderer/src/components/ProjectManager/ProjectDetails.tsx @@ -7,12 +7,17 @@ import { Tabs, Tab, Grid, - Chip, Divider, Stack, Tooltip, CircularProgress, - Alert + Alert, + TextField, + MenuItem, + Select, + FormControl, + InputLabel, + Paper } from '@mui/material' import { useState, useEffect } from 'react' import { @@ -21,12 +26,15 @@ import { Stop as StopIcon, PlayArrow as StartIcon, Launch as LaunchIcon, - Edit as EditIcon, Delete as DeleteIcon, Tune as TuningIcon, OpenInNew as OpenIcon, Code as CodeIcon, - Save as SaveIcon + Save as SaveIcon, + Settings as SettingsIcon, + Dns as ServerIcon, + Analytics as StatusIcon, + Storage as DbIcon } from '@mui/icons-material' import ProjectResourceMonitor from './ProjectResourceMonitor' import ProjectConfigEditor from './ProjectConfigEditor' @@ -36,6 +44,8 @@ interface ProjectDetailsProps { project: any; status: any; settings: any; + phpVersions: any[]; + mariaDbVersions: any[]; projectDetailTab: number; setProjectDetailTab: (v: number) => void; t: any; @@ -55,10 +65,11 @@ export default function ProjectDetails({ project: p, status, settings, + phpVersions, + mariaDbVersions, projectDetailTab, setProjectDetailTab, t, - onEdit, onDelete, onToggleStatus, onOpenSite, @@ -73,6 +84,28 @@ export default function ProjectDetails({ const [configText, setConfigText] = useState(''); const [isConfigLoading, setIsConfigLoading] = useState(false); const [isConfigSaving, setIsConfigSaving] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + // Editing State + const [editForm, setEditForm] = useState({ + name: '', + host: '', + serverType: '', + phpVersion: '', + mariaDbVersion: '' + }); + + useEffect(() => { + if (p) { + setEditForm({ + name: p.name || '', + host: p.host || '', + serverType: p.serverType || 'nginx', + phpVersion: p.phpVersion || '', + mariaDbVersion: p.mariaDbVersion || '' + }); + } + }, [p]); useEffect(() => { if (p?.host) { @@ -133,6 +166,41 @@ export default function ProjectDetails({ } }; + const handleProjectUpdate = async () => { + if (!p) return; + setIsSaving(true); + try { + const result = await window.api.updateProject({ + ...p, + ...editForm + }); + + if (result && result.success) { + Swal.fire({ + icon: 'success', + title: t('common.success'), + text: t('projects.updated'), + toast: true, + position: 'top-end', + showConfirmButton: false, + timer: 3000, + background: '#1e1e1e', + color: '#fff' + }); + } + } catch (error: any) { + Swal.fire({ + icon: 'error', + title: t('common.error'), + text: error.message, + background: '#1e1e1e', + color: '#fff' + }); + } finally { + setIsSaving(false); + } + }; + if (!p) { return ( @@ -147,166 +215,238 @@ export default function ProjectDetails({ status[`php:${p.phpVersion}`]?.status === 'running' ); + const installedPhp = phpVersions.filter(v => v.status === 'installed'); + const installedMariaDb = mariaDbVersions.filter(v => v.status === 'installed'); + return ( - - - - + + {/* Header */} + + + - - {p.name} + + {p.name} - + {p.path} - - - + + + + + + + + {!isHostMapped && ( )} - - - - - - - - - - + + {/* Fixed Delete Button at top right */} + + + + + + + - + {/* Tabs */} + setProjectDetailTab(v)} textColor="primary" indicatorColor="primary"> - - - + + + + {/* Content Area */} {projectDetailTab === 0 && ( - - - - {t('projects.site_domain')} - - {p.host}.local - - - + + {/* Editable Form Side */} + + + + {t('projects.edit_details', 'Proje Detaylarını Düzenle')} + + + + setEditForm({ ...editForm, name: e.target.value })} + sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }} + /> + + 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)' } }} + /> - - {t('projects.web_server')} - - {p.serverType.toUpperCase()} - - - + + + + {t('projects.web_server')} + + + + + + {t('projects.php_version')} + + + + - - {t('projects.php_version')} - - {p.phpVersion} - - - + + {t('projects.mariadb_version')} + + + + + + + + - - - - {t('dashboard.status')} + {/* Status Monitor Side */} + + + + {t('dashboard.status')} - - + + + + + + + + + + System Info + + + {t('projects.manual_edit_help', 'Bu sayfadaki değişiklikler kaydedildiğinde servis yapılandırmaları ve hosts dosyası otomatik olarak yenilenecektir.')} + + + @@ -314,63 +454,66 @@ export default function ProjectDetails({ {projectDetailTab === 1 && ( - - - - - {t('projects.mariadb_version')} - MariaDB {p.mariadbVersion} + + + + + + + + + {t('projects.mariadb_version')} + MariaDB {p.mariadbVersion} + - + - - - Host - localhost - - - Port - {settings.mariaDbPort} - - - Username - root - - - Password - (no password) - - - + + {[ + { label: 'Host', value: 'localhost' }, + { label: 'Port', value: settings.mariaDbPort }, + { label: 'Username', value: 'root' }, + { label: 'Password', value: '(no password)', italic: true } + ].map((item, idx) => ( + + {item.label.toUpperCase()} + + {item.value} + + + ))} + + )} {projectDetailTab === 2 && ( - + - - - {p.serverType.toUpperCase()} {t('projects.server_settings')} + + + {p.serverType} {t('projects.server_settings')} - + @@ -378,17 +521,17 @@ export default function ProjectDetails({ variant="contained" size="small" color="primary" - startIcon={isConfigSaving ? : } + startIcon={isConfigSaving ? : } disabled={isConfigSaving || isConfigLoading} onClick={handleSaveConfig} - sx={{ px: 3 }} + sx={{ px: 4, borderRadius: 2, fontWeight: 800 }} > {t('common.save').toUpperCase()} - + {t('projects.manual_edit_warning')} diff --git a/src/renderer/src/components/ProjectManager/ProjectManager.tsx b/src/renderer/src/components/ProjectManager/ProjectManager.tsx index 700ed61..178ecc5 100644 --- a/src/renderer/src/components/ProjectManager/ProjectManager.tsx +++ b/src/renderer/src/components/ProjectManager/ProjectManager.tsx @@ -6,6 +6,8 @@ interface ProjectManagerProps { projects: any[]; status: any; settings: any; + phpVersions: any[]; + mariaDbVersions: any[]; projectSearch: string; setProjectSearch: (v: string) => void; selectedProjectId: string | null; @@ -28,6 +30,8 @@ export default function ProjectManager({ projects, status, settings, + phpVersions, + mariaDbVersions, projectSearch, setProjectSearch, selectedProjectId, @@ -72,6 +76,8 @@ export default function ProjectManager({ project={p} status={status} settings={settings} + phpVersions={phpVersions} + mariaDbVersions={mariaDbVersions} projectDetailTab={projectDetailTab} setProjectDetailTab={setProjectDetailTab} t={t}