feat: implement project management UI with configuration editing and service control capabilities

This commit is contained in:
Ümit Tunç
2026-04-03 08:29:23 +03:00
parent ee4e3660f0
commit 1086a2c22a
9 changed files with 1070 additions and 321 deletions
+1 -1
View File
@@ -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)
<FilesMatch "\.php$">
SetHandler application/x-httpd-php-alias-{{HOST}}
</FilesMatch>
+1 -2
View File
@@ -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)
<FilesMatch "\.php$">
SetHandler application/x-httpd-php-alias-{{HOST}}
</FilesMatch>
# 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"
</Directory>
+475
View File
@@ -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",
+9
View File
@@ -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()
})
+73 -5
View File
@@ -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'
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 {
const processName = name === 'nginx' ? 'nginx' : name === 'apache' ? 'httpd' : name === 'php' ? 'php-cgi' : 'mariadbd'
resolve(true)
}
} else {
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<void> {
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<number>()
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))
}
}
+15 -10
View File
@@ -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}
@@ -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<string[]>([]);
const logEndRef = useRef<HTMLDivElement>(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 }) => (
<Box sx={{ mt: 2 }}>
<Box sx={{ 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.4)', fontWeight: 'bold' }}>CPU</Typography>
<Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 'bold' }}>{cpu || 0}%</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 800 }}>CPU USAGE</Typography>
<Typography variant="caption" sx={{ color: 'primary.main', fontWeight: 800 }}>{cpu || 0}%</Typography>
</Box>
<LinearProgress variant="determinate" value={cpu || 0} sx={{ height: 6, borderRadius: 3, bgcolor: 'rgba(255,255,255,0.05)' }} />
<LinearProgress variant="determinate" value={cpu || 0} sx={{ height: 8, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { borderRadius: 4, background: 'linear-gradient(90deg, #2196F3, #21CBF3)' } }} />
</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' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 800 }}>RAM USAGE</Typography>
<Typography variant="caption" sx={{ color: 'secondary.main', fontWeight: 800 }}>
{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' } }} />
<LinearProgress variant="determinate" value={Math.min(((memory || 0) / (1024 * 1024) / 1024) * 100, 100)} sx={{ height: 8, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { borderRadius: 4, background: 'linear-gradient(90deg, #9C27B0, #E91E63)' } }} />
</Box>
</Box>
);
const isPhp = serviceType === 'php';
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" />}
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', bgcolor: 'background.default' }}>
<Box sx={{ p: 4, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: 3 }}>
<Paper sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}>
{serviceType === 'nginx' && <WebIcon color="primary" sx={{ fontSize: 40 }} />}
{serviceType === 'apache' && <ServerIcon color="primary" sx={{ fontSize: 40 }} />}
{serviceType === 'php' && <PhpIcon color="primary" sx={{ fontSize: 40 }} />}
{serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />}
</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)' }}>
{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 variant="h4" sx={{ fontWeight: 800, letterSpacing: '-1px' }}>{serviceId.toUpperCase()}</Typography>
<Typography variant="subtitle2" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ width: 8, height: 8, 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 }}>{t('common.starting').toUpperCase()}</Box>}
{isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700 }}>{t('common.stopping').toUpperCase()}</Box>}
{isRunning && <Box component="span" sx={{ color: 'success.main', fontWeight: 700 }}>{t('common.running').toUpperCase()}</Box>}
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.stopped').toUpperCase()}</Box>}
</Typography>
</Box>
<Stack direction="row" spacing={1}>
<Stack direction="row" spacing={2}>
{isRunning && !isTransitioning && (
<IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main' }}>
<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>
</Tooltip>
)}
<Button
variant="contained"
color={isRunning ? 'error' : 'success'}
disabled={isTransitioning}
startIcon={
isTransitioning
? <CircularProgress size={20} color="inherit" />
: isRunning ? <StopIcon /> : <StartIcon />
}
startIcon={isTransitioning ? <CircularProgress size={20} color="inherit" /> : isRunning ? <StopIcon /> : <StartIcon />}
onClick={() => onToggleService(serviceId)}
sx={{ borderRadius: 2, minWidth: 100 }}
sx={{ borderRadius: 3, px: 4, height: 48, fontWeight: 800, fontSize: '1rem', boxShadow: isRunning ? '0 4px 12px rgba(244, 67, 54, 0.2)' : '0 4px 12px rgba(76, 175, 80, 0.2)' }}
>
{isStarting ? t('common.starting', '...') : (isStopping ? t('common.stopping', '...') : (isRunning ? t('common.stop', 'Durdur') : t('common.start', 'Başlat')))}
{isStarting ? t('common.starting') : (isStopping ? t('common.stopping') : (isRunning ? t('common.stop') : t('common.start')))}
</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('dashboard.overview')} />
<Tab label={t('dashboard.service_settings')} />
{(serviceId === 'nginx' || serviceId === 'apache' || serviceType === 'php') && (
<Tab label={t('dashboard.project_settings')} />
<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">
<Tab label={t('dashboard.overview')} value="overview" sx={{ fontWeight: 700, py: 2.5 }} />
<Tab label={isPhp ? 'php.ini (Şablon)' : t('dashboard.service_settings')} value="settings_template" sx={{ fontWeight: 700, py: 2.5 }} />
{isPhp && <Tab label="php.ini" value="settings_live" sx={{ fontWeight: 700, py: 2.5 }} />}
{(serviceId === 'nginx' || serviceId === 'apache' || isPhp) && (
<Tab label={t('dashboard.project_settings')} value="projects_template" sx={{ fontWeight: 700, py: 2.5 }} />
)}
<Tab label={t('diagnostics.logs')} />
<Tab label={t('projects.title')} />
<Tab label={t('diagnostics.logs')} value="logs" sx={{ fontWeight: 700, py: 2.5 }} />
<Tab label={t('projects.title')} value="projects" sx={{ fontWeight: 700, py: 2.5 }} />
</Tabs>
</Box>
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3 }}>
{activeTab === 0 && (
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 4 }}>
{activeTab === 'overview' && (
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>{t('dashboard.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)' }}>
<Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<StatusIcon /> {t('dashboard.system_stats')}
</Typography>
<Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, 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' }}>{t('dashboard.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 }}>{t('common.port')}</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<SettingsIcon /> {t('dashboard.service_info')}
</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}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.port').toUpperCase()}</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>
</Box>
<Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ opacity: 0.5 }}>{t('common.status')}</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: isRunning ? 'success.main' : 'error.main' }}>
{(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()}
</Typography>
<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').toUpperCase()}</Typography>
<Chip
label={(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()}
color={isRunning ? 'success' : 'error'}
sx={{ fontWeight: 900, borderRadius: 2 }}
/>
</Box>
</Stack>
</Paper>
@@ -320,46 +366,34 @@ export default function DashboardDetails({
</Grid>
)}
{activeTab === 1 && (
{(activeTab === 'settings_template' || activeTab === 'settings_live') && (
<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()}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}>
{activeTab === 'settings_live' ? 'php.ini (ACTIVE)' : getTemplateName().toUpperCase()}
</Typography>
<Button
variant="contained"
size="small"
startIcon={<SaveIcon />}
onClick={() => saveConfig(getTemplateName(), configContent)}
sx={{ borderRadius: 2 }}
size="large"
startIcon={isLoadingConfig ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
disabled={isLoadingConfig}
onClick={() => activeTab === 'settings_live' ? saveLiveConfig(serviceId!, liveConfigContent) : saveConfig(getTemplateName(), configContent)}
sx={{ borderRadius: 2.5, px: 4, fontWeight: 800 }}
>
{t('common.save', 'KAYDET')}
{t('common.save').toUpperCase()}
</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'
}}>
<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>
) : (
<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'
}}
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"
/>
</Box>
@@ -368,31 +402,23 @@ export default function DashboardDetails({
</Box>
)}
{activeTab === 2 && (
{activeTab === 'projects_template' && (
<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 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}>
{getProjectTemplateNames()[0]?.toUpperCase()}
</Typography>
<Button
variant="contained"
size="small"
size="large"
startIcon={<SaveIcon />}
onClick={() => saveConfig(getProjectTemplateNames()[0], projectConfigContent)}
sx={{ borderRadius: 2 }}
sx={{ borderRadius: 2.5, px: 4, fontWeight: 800 }}
>
{t('common.save', 'KAYDET')}
{t('common.save').toUpperCase()}
</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'
}}>
<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>
) : (
@@ -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"
/>
</Box>
@@ -416,44 +437,67 @@ export default function DashboardDetails({
</Box>
)}
{activeTab === 3 && (
<Box sx={{ bgcolor: '#000', borderRadius: 2, p: 2, height: '60vh', overflowY: 'auto' }}>
{logs.map((log, i) => (
{activeTab === 'logs' && (
<Box sx={{ bgcolor: '#0b0e11', borderRadius: 4, p: 3, height: '65vh', overflowY: 'auto', border: '1px solid rgba(255,255,255,0.05)', boxShadow: 'inset 0 0 20px rgba(0,0,0,0.5)' }}>
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.3)' }}>
<LogIcon fontSize="small" />
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 1 }}>REALTIME SERVICE LOGS</Typography>
</Box>
{logs.length === 0 ? (
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.2 }}>
<Typography variant="body2" sx={{ fontStyle: 'italic' }}>{t('diagnostics.noLogs')}</Typography>
</Box>
) : (
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
fontFamily: '"Fira Code", monospace',
fontSize: '0.85rem',
color: log.toLowerCase().includes('error') ? '#ff5252' : (log.toLowerCase().includes('warn') ? '#ffb142' : '#33d9b2'),
mb: 0.8,
lineHeight: 1.5,
opacity: 0.9
}}>
{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' }}>
{activeTab === 'projects' && (
<Paper sx={{ bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, 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>
<Box sx={{ p: 8, textAlign: 'center', opacity: 0.2 }}>
<ProjectIcon sx={{ fontSize: 64, mb: 2 }} />
<Typography variant="h6">{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>
serviceProjects.map((p_item, idx) => (
<Box key={p_item.id}>
<ListItem sx={{ py: 3, px: 4, '&:hover': { bgcolor: 'rgba(255,255,255,0.02)' } }}>
<Avatar sx={{ mr: 3, bgcolor: 'rgba(255,255,255,0.03)', color: 'primary.main', borderRadius: 2 }}>
<ProjectIcon />
</Avatar>
<ListItemText
primary={p.name}
secondary={`${p.host} • PHP ${p.phpVersion}`}
primaryTypographyProps={{ fontWeight: 700 }}
primary={p_item.name}
secondary={`${p_item.host} • PHP ${p_item.phpVersion}`}
primaryTypographyProps={{ fontWeight: 800, fontSize: '1.1rem' }}
secondaryTypographyProps={{ sx: { opacity: 0.5, mt: 0.5, fontFamily: 'monospace' } }}
/>
<ListItemSecondaryAction>
<IconButton onClick={() => onSelectProject(p.id)} color="primary">
<OpenIcon fontSize="small" />
</IconButton>
<Button
variant="outlined"
onClick={() => onSelectProject(p_item.id)}
startIcon={<OpenIcon fontSize="small" />}
sx={{ borderRadius: 2, fontWeight: 700 }}
>
{t('common.open')}
</Button>
</ListItemSecondaryAction>
</ListItem>
{idx < serviceProjects.length - 1 && <Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />}
{idx < serviceProjects.length - 1 && <Divider sx={{ opacity: 0.05 }} />}
</Box>
))
)}
@@ -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<string>('');
const [isConfigLoading, setIsConfigLoading] = useState<boolean>(false);
const [isConfigSaving, setIsConfigSaving] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState(false);
// Editing State
const [editForm, setEditForm] = useState<any>({
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 (
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: 0.3 }}>
@@ -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 (
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', bgcolor: 'rgba(0,0,0,0.1)', height: '100%', overflow: 'hidden' }}>
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main', width: 48, height: 48, mt: 0.5 }}>
<ProjectIcon />
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', bgcolor: 'rgba(255,255,255,0.01)', height: '100%', overflow: 'hidden', position: 'relative' }}>
{/* Header */}
<Box sx={{ p: 4, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'flex-start', gap: 3, position: 'relative' }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main', width: 64, height: 64, borderRadius: 3, border: '1px solid rgba(57, 167, 255, 0.2)' }}>
<ProjectIcon sx={{ fontSize: 32 }} />
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Box sx={{ mb: 2 }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}>{p.name}</Typography>
<Box sx={{ mb: 2.5 }}>
<Typography variant="h4" sx={{ fontWeight: 800, letterSpacing: '-0.02em', mb: 0.5 }}>{p.name}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontFamily: 'monospace' }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.4)', fontFamily: 'monospace' }}>
{p.path}
</Typography>
<IconButton size="small" onClick={onOpenFolder} sx={{ color: 'rgba(255,255,255,0.2)', p: 0.5 }}>
<FolderOpenIcon sx={{ fontSize: '0.9rem' }} />
<Tooltip title={t('projects.open_folder')}>
<IconButton size="small" onClick={onOpenFolder} sx={{ color: 'rgba(255,255,255,0.2)', p: 0.5, '&:hover': { color: 'primary.main', bgcolor: 'rgba(57, 167, 255, 0.1)' } }}>
<FolderOpenIcon sx={{ fontSize: '1rem' }} />
</IconButton>
</Tooltip>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
<Button
variant="outlined"
size="small"
variant="contained"
size="medium"
disabled={isTransitioning}
startIcon={isTransitioning ? <CircularProgress size={16} /> : (isRunning ? <StopIcon /> : <StartIcon />)}
color={isRunning ? 'error' : 'primary'}
startIcon={isTransitioning ? <CircularProgress size={18} color="inherit" /> : (isRunning ? <StopIcon /> : <StartIcon />)}
color={isRunning ? 'error' : 'success'}
onClick={async () => {
if (setTransitioning) setTransitioning(true);
try {
await onToggleStatus();
} finally {
if (setTransitioning) {
// Short delay to ensure status update is reflected
setTimeout(() => setTransitioning(false), 500);
}
}
}}
sx={{
px: 2,
height: 36,
fontWeight: 'bold',
transition: 'all 0.1s ease-in-out',
'&:active': { transform: 'scale(0.96)' }
}}
sx={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 800, boxShadow: isRunning ? '0 4px 12px rgba(244, 67, 54, 0.2)' : '0 4px 12px rgba(76, 175, 80, 0.2)' }}
>
{isTransitioning ? t('common.processing') : (isRunning ? t('projects.stop_site') : t('projects.start_site'))}
{isTransitioning ? t('common.processing') : (isRunning ? t('projects.stop_site').toUpperCase() : t('projects.start_site').toUpperCase())}
</Button>
<Button
variant="outlined"
size="small"
size="medium"
startIcon={<LaunchIcon />}
onClick={() => onOpenSite(isHostMapped)}
sx={{
px: 2,
height: 36,
transition: 'all 0.1s ease-in-out',
'&:active': { transform: 'scale(0.96)' }
}}
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)' } }}
>
{t('projects.open_site')}
{t('projects.open_site').toUpperCase()}
</Button>
<Button
variant="outlined"
size="small"
size="medium"
startIcon={<CodeIcon />}
onClick={onOpenVSCode}
sx={{
px: 2,
height: 36,
borderColor: 'rgba(255,255,255,0.1)',
color: 'rgba(255,255,255,0.7)',
transition: 'all 0.1s ease-in-out',
'&:active': { transform: 'scale(0.96)' }
}}
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)' } }}
>
VS Code Aç
VS CODE AÇ
</Button>
{!isHostMapped && (
<Button
variant="outlined"
size="small"
variant="contained"
size="medium"
color="warning"
onClick={() => {
const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`;
onAddHost(finalHost);
}}
sx={{
px: 2,
height: 36,
fontWeight: 'bold',
transition: 'all 0.1s ease-in-out',
'&:active': { transform: 'scale(0.96)' }
}}
sx={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 800, boxShadow: '0 4px 12px rgba(255, 152, 0, 0.2)' }}
>
Hosts Dosyasına Ekle
HOSTS DOSYASINA EKLE
</Button>
)}
<Tooltip title={t('common.edit')}>
<IconButton onClick={onEdit} sx={{ width: 36, height: 36, border: '1px solid rgba(255,255,255,0.1)', borderRadius: '50%' }}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('common.delete')}>
<IconButton onClick={onDelete} sx={{ width: 36, height: 36, border: '1px solid rgba(255,255,255,0.1)', borderRadius: '50%', '&:hover': { color: 'error.main', borderColor: 'error.main' } }}>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}>
{/* Fixed Delete Button at top right */}
<Box sx={{ position: 'absolute', top: 32, right: 32 }}>
<Tooltip title={t('common.delete')}>
<IconButton
onClick={onDelete}
sx={{
width: 48,
height: 48,
bgcolor: 'rgba(255, 68, 68, 0.05)',
color: 'error.main',
border: '1px solid rgba(255, 68, 68, 0.1)',
borderRadius: 3,
'&: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 />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* 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')} />
<Tab label={t('projects.database')} />
<Tab label={t('projects.server_settings')} />
<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 }} />
</Tabs>
</Box>
{/* Content Area */}
<Box sx={{ p: 0, flexGrow: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{projectDetailTab === 0 && (
<Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 4 }}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.site_domain')}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
<Typography sx={{ fontWeight: 500 }}>{p.host}.local</Typography>
<Chip label="CHANGE" size="small" variant="outlined" sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
</Box>
</Box>
<Grid container spacing={4}>
{/* 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>
<Box sx={{ mb: 4 }}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.web_server')}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
<Typography sx={{ fontWeight: 500 }}>{p.serverType.toUpperCase()}</Typography>
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
</Box>
</Box>
<Stack spacing={3}>
<TextField
label={t('projects.site_name')}
fullWidth
variant="filled"
value={editForm.name}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }}
/>
<Box sx={{ mb: 4 }}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.php_version')}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
<Typography sx={{ fontWeight: 500 }}>{p.phpVersion}</Typography>
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
</Box>
</Box>
<TextField
label={t('projects.site_domain')}
fullWidth
variant="filled"
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)' } }}
/>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormControl fullWidth variant="filled">
<InputLabel>{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)' } }}
>
<MenuItem value="nginx">Nginx</MenuItem>
<MenuItem value="apache">Apache</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth variant="filled">
<InputLabel>{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)' } }}
>
{installedPhp.map(v => (
<MenuItem key={v.version} value={v.version}>PHP {v.version}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
<TuningIcon fontSize="small" color="primary" /> {t('dashboard.status')}
<FormControl fullWidth variant="filled">
<InputLabel>{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)' } }}
>
{installedMariaDb.map(v => (
<MenuItem key={v.version} value={v.version}>MariaDB {v.version}</MenuItem>
))}
</Select>
</FormControl>
<Box sx={{ pt: 1, display: 'flex', justifyContent: 'flex-start' }}>
<Button
variant="contained"
size="large"
startIcon={isSaving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
onClick={handleProjectUpdate}
disabled={isSaving}
sx={{ px: 6, py: 1.5, borderRadius: 3, fontWeight: 800, fontSize: '1rem', background: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)', boxShadow: '0 8px 20px rgba(33, 150, 243, 0.3)' }}
>
{t('common.save').toUpperCase()}
</Button>
</Box>
</Stack>
</Paper>
</Grid>
{/* 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>
<Box sx={{ mb: 4 }}>
<ProjectResourceMonitor
serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'}
serverStats={p.serverType === 'apache' ? status.apache : status.nginx}
phpStats={status[`php:${p.phpVersion}`]}
/>
</Box>
<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 }}>
<CodeIcon fontSize="small" /> System Info
</Typography>
<Typography variant="body2" sx={{ opacity: 0.5, lineHeight: 1.6 }}>
{t('projects.manual_edit_help', 'Bu sayfadaki değişiklikler kaydedildiğinde servis yapılandırmaları ve hosts dosyası otomatik olarak yenilenecektir.')}
</Typography>
</Box>
</Paper>
</Grid>
</Grid>
</Box>
@@ -314,63 +454,66 @@ export default function ProjectDetails({
{projectDetailTab === 1 && (
<Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}>
<Box sx={{ maxWidth: 600 }}>
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ maxWidth: 700 }}>
<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' }}>
<DbIcon />
</Avatar>
<Box>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.mariadb_version')}</Typography>
<Typography sx={{ mt: 0.5, fontWeight: 600 }}>MariaDB {p.mariadbVersion}</Typography>
<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>
</Box>
</Box>
<Button
variant="outlined"
size="small"
size="medium"
startIcon={<OpenIcon />}
onClick={onManageDb}
sx={{ borderRadius: 2.5, px: 3, height: 44, fontWeight: 700 }}
>
{t('mariadb_manager.title')}
</Button>
</Box>
<Divider sx={{ mb: 3, opacity: 0.1 }} />
<Divider sx={{ mb: 4, opacity: 0.1 }} />
<Stack spacing={2}>
<Box>
<Typography variant="caption" color="text.secondary">Host</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>localhost</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Port</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{settings.mariaDbPort}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Username</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>root</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">Password</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>(no password)</Typography>
</Box>
</Stack>
</Box>
<Grid container spacing={3}>
{[
{ label: 'Host', value: 'localhost' },
{ label: 'Port', value: settings.mariaDbPort },
{ label: 'Username', value: 'root' },
{ 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' }}>
{item.value}
</Typography>
</Grid>
))}
</Grid>
</Paper>
</Box>
</Box>
)}
{projectDetailTab === 2 && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<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={{ 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={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CodeIcon color="primary" />
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{p.serverType.toUpperCase()} {t('projects.server_settings')}
<ServerIcon color="primary" />
<Typography variant="subtitle2" sx={{ fontWeight: 800, textTransform: 'uppercase', letterSpacing: 1 }}>
{p.serverType} {t('projects.server_settings')}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<Button
size="small"
startIcon={<TuningIcon />}
onClick={loadConfig}
sx={{ color: 'rgba(255,255,255,0.5)' }}
sx={{ color: 'rgba(255,255,255,0.5)', borderRadius: 2 }}
>
{t('common.refresh')}
</Button>
@@ -378,17 +521,17 @@ export default function ProjectDetails({
variant="contained"
size="small"
color="primary"
startIcon={isConfigSaving ? <CircularProgress size={16} /> : <SaveIcon />}
startIcon={isConfigSaving ? <CircularProgress size={16} color="inherit" /> : <SaveIcon />}
disabled={isConfigSaving || isConfigLoading}
onClick={handleSaveConfig}
sx={{ px: 3 }}
sx={{ px: 4, borderRadius: 2, fontWeight: 800 }}
>
{t('common.save').toUpperCase()}
</Button>
</Box>
</Box>
<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)' }}>
<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 }}>
{t('projects.manual_edit_warning')}
</Alert>
@@ -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}