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 # Directory index handling
DirectoryIndex index.php index.html 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$"> <FilesMatch "\.php$">
SetHandler application/x-httpd-php-alias-{{HOST}} SetHandler application/x-httpd-php-alias-{{HOST}}
</FilesMatch> </FilesMatch>
+1 -2
View File
@@ -15,13 +15,12 @@ Alias /{{HOST}} "{{PATH}}"
# Directory index handling # Directory index handling
DirectoryIndex index.php index.html 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$"> <FilesMatch "\.php$">
SetHandler application/x-httpd-php-alias-{{HOST}} SetHandler application/x-httpd-php-alias-{{HOST}}
</FilesMatch> </FilesMatch>
# Action maps the handler to the binary via the ScriptAlias URL # 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" Action application/x-httpd-php-alias-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe"
</Directory> </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", "service": "php:5.6.40",
"action": "start", "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 () => { ipcMain.handle('services:status', async () => {
return processManager.getStatuses() return processManager.getStatuses()
}) })
+79 -11
View File
@@ -235,6 +235,13 @@ export class ProcessManager {
// Also force kill lingering processes even if not in map // Also force kill lingering processes even if not in map
const result = await this.forceKillAll(name) 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') this.states.set(name, 'stopped')
return result return result
} }
@@ -271,15 +278,33 @@ export class ProcessManager {
} }
} else { } else {
if (isWindows) { if (isWindows) {
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'apache' ? 'httpd.exe' : name === 'php' ? 'php-cgi.exe' : 'mariadbd.exe' let executableName = ''
spawn('taskkill', ['/F', '/IM', executableName, '/T'], { windowsHide: true }) if (name === 'nginx') executableName = 'nginx.exe'
.on('close', () => resolve(true)) else if (name === 'apache') executableName = 'httpd.exe'
.on('error', () => resolve(true)) 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 { } else {
const processName = name === 'nginx' ? 'nginx' : name === 'apache' ? 'httpd' : name === 'php' ? 'php-cgi' : 'mariadbd' let processName = ''
spawn('pkill', ['-f', processName], { windowsHide: true }) if (name === 'nginx') processName = 'nginx'
.on('close', () => resolve(true)) else if (name === 'apache') processName = 'httpd'
.on('error', () => resolve(true)) 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 { } else {
@@ -470,10 +495,53 @@ export class ProcessManager {
return Array.from(new Set(foundLogs)).slice(-count) return Array.from(new Set(foundLogs)).slice(-count)
} }
stopAll(): void { async aggressiveKillAll(): Promise<void> {
for (const name of this.processes.keys()) { const isWindows = process.platform === 'win32';
this.stopService(name) 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 () => { const handleStopAll = async () => {
if (!window.api) return 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([ // Show immediate "stopping" UI feedback for all running services
...criticalServices.filter(s => status[s]?.status === 'running' || status[s]?.status === 'starting').map(s => window.api?.stopService(s)), const updatedStatus = { ...status }
...phpToStop.map(s => window.api?.stopService(s)), Object.keys(updatedStatus).forEach(s => {
...mariaDbToStop.map(s => window.api?.stopService(s)), if (updatedStatus[s].status === 'running') updatedStatus[s].status = 'stopping'
window.api?.stopService('mariadb') // Also stop default })
]) setStatus(updatedStatus)
const result = await window.api.invoke('services:stop-all')
fetchStatus() 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) => { const formatMemory = (bytes?: number) => {
@@ -1587,6 +1590,8 @@ function App(): JSX.Element {
projects={projects} projects={projects}
status={status} status={status}
settings={settings} settings={settings}
phpVersions={phpVersions}
mariaDbVersions={mariaDbVersions}
projectSearch={projectSearch} projectSearch={projectSearch}
setProjectSearch={setProjectSearch} setProjectSearch={setProjectSearch}
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
@@ -14,7 +14,10 @@ import {
Divider, Divider,
LinearProgress, LinearProgress,
CircularProgress, CircularProgress,
Stack Stack,
Tooltip,
Chip,
Avatar
} from '@mui/material' } from '@mui/material'
import { import {
Refresh as RefreshIcon, Refresh as RefreshIcon,
@@ -26,7 +29,10 @@ import {
Language as WebIcon, Language as WebIcon,
Dns as ServerIcon, Dns as ServerIcon,
Code as PhpIcon, Code as PhpIcon,
Storage as DbIcon Storage as DbIcon,
Settings as SettingsIcon,
Assignment as LogIcon,
Analytics as StatusIcon
} from '@mui/icons-material' } from '@mui/icons-material'
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import 'prismjs/themes/prism-tomorrow.css' import 'prismjs/themes/prism-tomorrow.css'
@@ -58,9 +64,10 @@ export default function DashboardDetails({
onReloadService, onReloadService,
onSelectProject onSelectProject
}: DashboardDetailsProps) { }: DashboardDetailsProps) {
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState('overview');
const [configContent, setConfigContent] = useState(''); const [configContent, setConfigContent] = useState('');
const [projectConfigContent, setProjectConfigContent] = useState(''); const [projectConfigContent, setProjectConfigContent] = useState('');
const [liveConfigContent, setLiveConfigContent] = useState('');
const [isLoadingConfig, setIsLoadingConfig] = useState(false); const [isLoadingConfig, setIsLoadingConfig] = useState(false);
const [logs, setLogs] = useState<string[]>([]); const [logs, setLogs] = useState<string[]>([]);
const logEndRef = useRef<HTMLDivElement>(null); const logEndRef = useRef<HTMLDivElement>(null);
@@ -100,17 +107,31 @@ export default function DashboardDetails({
if (res && res.success) { if (res && res.success) {
setter(res.content || ''); setter(res.content || '');
} else { } else {
console.error('Failed to fetch config:', res?.message);
setter(''); setter('');
} }
} catch (error) { } catch (error) {
console.error('Error fetching config:', error);
setter(''); setter('');
} finally { } finally {
setIsLoadingConfig(false); 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 saveConfig = async (templateName: string, content: string) => {
const res = await window.api.invoke('config:template:save', { templateName, content }); const res = await window.api.invoke('config:template:save', { templateName, content });
if (res.success) { if (res.success) {
@@ -118,14 +139,27 @@ export default function DashboardDetails({
title: t('common.success'), title: t('common.success'),
text: res.message, text: res.message,
icon: 'success', icon: 'success',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
background: '#1e1e1e', background: '#1e1e1e',
color: '#fff' 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({ Swal.fire({
title: t('common.error'), title: t('common.success'),
text: res.message, text: res.message,
icon: 'error', icon: 'success',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
background: '#1e1e1e', background: '#1e1e1e',
color: '#fff' color: '#fff'
}); });
@@ -138,38 +172,41 @@ export default function DashboardDetails({
setLogs(res || []); setLogs(res || []);
}; };
const getHighlightLanguage = (templateName: string) => { const getHighlightLanguage = (name: string) => {
if (!templateName) return Prism.languages.clike; if (!name) return Prism.languages.clike;
if (templateName.includes('nginx')) return Prism.languages.nginx; if (name.includes('nginx')) return Prism.languages.nginx;
if (templateName.includes('httpd') || templateName.includes('apache')) return Prism.languages.apacheconf; if (name.includes('httpd') || name.includes('apache')) return Prism.languages.apacheconf;
if (templateName.includes('.ini') || templateName.includes('.conf')) return Prism.languages.ini; if (name.includes('.ini') || name.includes('.conf')) return Prism.languages.ini;
return Prism.languages.clike; return Prism.languages.clike;
}; };
const getHighlightLabel = (templateName: string) => { const getHighlightLabel = (name: string) => {
if (!templateName) return 'clike'; if (!name) return 'clike';
if (templateName.includes('nginx')) return 'nginx'; if (name.includes('nginx')) return 'nginx';
if (templateName.includes('httpd') || templateName.includes('apache')) return 'apacheconf'; if (name.includes('httpd') || name.includes('apache')) return 'apacheconf';
if (templateName.includes('.ini') || templateName.includes('.conf')) return 'ini'; if (name.includes('.ini') || name.includes('.conf')) return 'ini';
return 'clike'; return 'clike';
}; };
useEffect(() => { useEffect(() => {
if (activeTab === 1) fetchConfig(getTemplateName(), setConfigContent); if (activeTab === 'settings_template') fetchConfig(getTemplateName(), setConfigContent);
if (activeTab === 2) { if (activeTab === 'projects_template') {
const templates = getProjectTemplateNames(); const templates = getProjectTemplateNames();
if (templates.length > 0) fetchConfig(templates[0], setProjectConfigContent); if (templates.length > 0) fetchConfig(templates[0], setProjectConfigContent);
} }
if (activeTab === 3) { if (activeTab === 'logs') {
fetchLogs(); fetchLogs();
const interval = setInterval(fetchLogs, 3000); const interval = setInterval(fetchLogs, 3000);
return () => clearInterval(interval); return () => clearInterval(interval);
} }
if (activeTab === 'settings_live' && serviceId) {
fetchLiveConfig(serviceId);
}
return undefined; return undefined;
}, [activeTab, serviceId]); }, [activeTab, serviceId]);
useEffect(() => { useEffect(() => {
if (activeTab === 3 && logEndRef.current) { if (activeTab === 'logs' && logEndRef.current) {
logEndRef.current.scrollIntoView({ behavior: 'smooth' }); logEndRef.current.scrollIntoView({ behavior: 'smooth' });
} }
}, [logs, activeTab]); }, [logs, activeTab]);
@@ -186,7 +223,6 @@ export default function DashboardDetails({
const getPort = () => { const getPort = () => {
if (currentStatus.port) return currentStatus.port; if (currentStatus.port) return currentStatus.port;
// Fallback for stopped services or missing data
if (serviceId === 'nginx') return settings.nginxPort; if (serviceId === 'nginx') return settings.nginxPort;
if (serviceId === 'apache') return settings.apachePort; if (serviceId === 'apache') return settings.apachePort;
if (serviceType === 'php') { if (serviceType === 'php') {
@@ -210,109 +246,119 @@ export default function DashboardDetails({
if (serviceId === 'nginx') return !p.serverType || p.serverType === 'nginx'; if (serviceId === 'nginx') return !p.serverType || p.serverType === 'nginx';
if (serviceId === 'apache') return p.serverType === 'apache'; if (serviceId === 'apache') return p.serverType === 'apache';
if (serviceType === 'php') return p.phpVersion === serviceId.split(':')[1]; 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; return false;
}); });
const ResourceMonitor = ({ cpu, memory }: { cpu?: number; memory?: number }) => ( const ResourceMonitor = ({ cpu, memory }: { cpu?: number; memory?: number }) => (
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Box sx={{ mb: 1 }}> <Box sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.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: 'rgba(255,255,255,0.4)', fontWeight: 800 }}>CPU USAGE</Typography>
<Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 'bold' }}>{cpu || 0}%</Typography> <Typography variant="caption" sx={{ color: 'primary.main', fontWeight: 800 }}>{cpu || 0}%</Typography>
</Box> </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> <Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}> <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: 'rgba(255,255,255,0.4)', fontWeight: 800 }}>RAM USAGE</Typography>
<Typography variant="caption" sx={{ color: 'secondary.light', fontWeight: 'bold' }}> <Typography variant="caption" sx={{ color: 'secondary.main', fontWeight: 800 }}>
{memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'} {memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'}
</Typography> </Typography>
</Box> </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>
</Box> </Box>
); );
const isPhp = serviceType === 'php';
return ( return (
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}> <Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', bgcolor: 'background.default' }}>
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ p: 4, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: 3 }}>
<Paper sx={{ p: 1.5, bgcolor: 'rgba(255, 255, 255, 0.05)', borderRadius: 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" fontSize="large" />} {serviceType === 'nginx' && <WebIcon color="primary" sx={{ fontSize: 40 }} />}
{serviceType === 'apache' && <ServerIcon color="primary" fontSize="large" />} {serviceType === 'apache' && <ServerIcon color="primary" sx={{ fontSize: 40 }} />}
{serviceType === 'php' && <PhpIcon color="primary" fontSize="large" />} {serviceType === 'php' && <PhpIcon color="primary" sx={{ fontSize: 40 }} />}
{serviceType === 'mariadb' && <DbIcon color="primary" fontSize="large" />} {serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />}
</Paper> </Paper>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Typography variant="h5" sx={{ fontWeight: 800 }}>{serviceId.toUpperCase()}</Typography> <Typography variant="h4" sx={{ fontWeight: 800, letterSpacing: '-1px' }}>{serviceId.toUpperCase()}</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.4)' }}> <Typography variant="subtitle2" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{isStarting && <Box component="span" sx={{ color: 'warning.light' }}>{t('common.starting', 'Başlatılıyor...')}</Box>} <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' }} />
{isStopping && <Box component="span" sx={{ color: 'warning.light' }}>{t('common.stopping', 'Durduruluyor...')}</Box>} {isStarting && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700 }}>{t('common.starting').toUpperCase()}</Box>}
{isRunning && <Box component="span" sx={{ color: 'success.light' }}>{t('common.running', 'Çalışıyor')}</Box>} {isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700 }}>{t('common.stopping').toUpperCase()}</Box>}
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5 }}>{t('common.stopped', 'Durduruldu')}</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> </Typography>
</Box> </Box>
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={2}>
{isRunning && !isTransitioning && ( {isRunning && !isTransitioning && (
<IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main' }}> <Tooltip title={t('common.reload')}>
<RefreshIcon /> <IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.05)', color: 'primary.main', border: '1px solid rgba(57, 167, 255, 0.1)', width: 48, height: 48, borderRadius: 3 }}>
</IconButton> <RefreshIcon />
</IconButton>
</Tooltip>
)} )}
<Button <Button
variant="contained" variant="contained"
color={isRunning ? 'error' : 'success'} color={isRunning ? 'error' : 'success'}
disabled={isTransitioning} disabled={isTransitioning}
startIcon={ startIcon={isTransitioning ? <CircularProgress size={20} color="inherit" /> : isRunning ? <StopIcon /> : <StartIcon />}
isTransitioning
? <CircularProgress size={20} color="inherit" />
: isRunning ? <StopIcon /> : <StartIcon />
}
onClick={() => onToggleService(serviceId)} 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> </Button>
</Stack> </Stack>
</Box> </Box>
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}> <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)}> <Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} textColor="primary" indicatorColor="primary">
<Tab label={t('dashboard.overview')} /> <Tab label={t('dashboard.overview')} value="overview" sx={{ fontWeight: 700, py: 2.5 }} />
<Tab label={t('dashboard.service_settings')} /> <Tab label={isPhp ? 'php.ini (Şablon)' : t('dashboard.service_settings')} value="settings_template" sx={{ fontWeight: 700, py: 2.5 }} />
{(serviceId === 'nginx' || serviceId === 'apache' || serviceType === 'php') && ( {isPhp && <Tab label="php.ini" value="settings_live" sx={{ fontWeight: 700, py: 2.5 }} />}
<Tab label={t('dashboard.project_settings')} /> {(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('diagnostics.logs')} value="logs" sx={{ fontWeight: 700, py: 2.5 }} />
<Tab label={t('projects.title')} /> <Tab label={t('projects.title')} value="projects" sx={{ fontWeight: 700, py: 2.5 }} />
</Tabs> </Tabs>
</Box> </Box>
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3 }}> <Box sx={{ flexGrow: 1, overflowY: 'auto', p: 4 }}>
{activeTab === 0 && ( {activeTab === 'overview' && (
<Grid container spacing={4}> <Grid container spacing={4}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>{t('dashboard.system_stats')}</Typography> <Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}> <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} /> <ResourceMonitor cpu={currentStatus.cpu} memory={currentStatus.memory} />
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>{t('dashboard.service_info')}</Typography> <Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}> <SettingsIcon /> {t('dashboard.service_info')}
<Stack spacing={2}> </Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}>
<Typography variant="body2" sx={{ opacity: 0.5 }}>{t('common.port')}</Typography> <Stack spacing={3}>
<Typography variant="body2" sx={{ fontWeight: 600 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{getPort()} <Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.port').toUpperCase()}</Typography>
</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> </Box>
<Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} /> <Divider sx={{ opacity: 0.05 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2" sx={{ opacity: 0.5 }}>{t('common.status')}</Typography> <Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.status').toUpperCase()}</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: isRunning ? 'success.main' : 'error.main' }}> <Chip
{(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()} label={(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()}
</Typography> color={isRunning ? 'success' : 'error'}
sx={{ fontWeight: 900, borderRadius: 2 }}
/>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
@@ -320,46 +366,34 @@ export default function DashboardDetails({
</Grid> </Grid>
)} )}
{activeTab === 1 && ( {(activeTab === 'settings_template' || activeTab === 'settings_live') && (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="overline" sx={{ fontWeight: 800, color: 'primary.main', letterSpacing: 2 }}> <Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}>
{getTemplateName().toUpperCase()} {activeTab === 'settings_live' ? 'php.ini (ACTIVE)' : getTemplateName().toUpperCase()}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
size="small" size="large"
startIcon={<SaveIcon />} startIcon={isLoadingConfig ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
onClick={() => saveConfig(getTemplateName(), configContent)} disabled={isLoadingConfig}
sx={{ borderRadius: 2 }} 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> </Button>
</Box> </Box>
<Paper sx={{ <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' }}>
flexGrow: 1,
bgcolor: '#1e1e1e',
borderRadius: 3,
border: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}>
{isLoadingConfig ? ( {isLoadingConfig ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
) : ( ) : (
<Box sx={{ flexGrow: 1, overflow: 'auto' }}> <Box sx={{ flexGrow: 1, overflow: 'auto' }}>
<Editor <Editor
value={configContent} value={activeTab === 'settings_live' ? liveConfigContent : configContent}
onValueChange={setConfigContent} onValueChange={activeTab === 'settings_live' ? setLiveConfigContent : setConfigContent}
highlight={code => Prism.highlight(code, getHighlightLanguage(getTemplateName()), getHighlightLabel(getTemplateName()))} highlight={code => Prism.highlight(code, getHighlightLanguage(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()), getHighlightLabel(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()))}
padding={20} padding={25}
style={{ style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }}
fontFamily: '"Fira Code", "Consolas", monospace',
fontSize: '14px',
minHeight: '100%',
outline: 'none'
}}
textareaClassName="editor-textarea" textareaClassName="editor-textarea"
/> />
</Box> </Box>
@@ -368,31 +402,23 @@ export default function DashboardDetails({
</Box> </Box>
)} )}
{activeTab === 2 && ( {activeTab === 'projects_template' && (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="overline" sx={{ fontWeight: 800, color: 'primary.main', letterSpacing: 2 }}> <Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}>
{getProjectTemplateNames()[0]?.toUpperCase()} {getProjectTemplateNames()[0]?.toUpperCase()}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
size="small" size="large"
startIcon={<SaveIcon />} startIcon={<SaveIcon />}
onClick={() => saveConfig(getProjectTemplateNames()[0], projectConfigContent)} 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> </Button>
</Box> </Box>
<Paper sx={{ <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' }}>
flexGrow: 1,
bgcolor: '#1e1e1e',
borderRadius: 3,
border: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}>
{isLoadingConfig ? ( {isLoadingConfig ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
) : ( ) : (
@@ -401,13 +427,8 @@ export default function DashboardDetails({
value={projectConfigContent} value={projectConfigContent}
onValueChange={setProjectConfigContent} onValueChange={setProjectConfigContent}
highlight={code => Prism.highlight(code, getHighlightLanguage(getProjectTemplateNames()[0]), getHighlightLabel(getProjectTemplateNames()[0]))} highlight={code => Prism.highlight(code, getHighlightLanguage(getProjectTemplateNames()[0]), getHighlightLabel(getProjectTemplateNames()[0]))}
padding={20} padding={25}
style={{ style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }}
fontFamily: '"Fira Code", "Consolas", monospace',
fontSize: '14px',
minHeight: '100%',
outline: 'none'
}}
textareaClassName="editor-textarea" textareaClassName="editor-textarea"
/> />
</Box> </Box>
@@ -416,44 +437,67 @@ export default function DashboardDetails({
</Box> </Box>
)} )}
{activeTab === 3 && ( {activeTab === 'logs' && (
<Box sx={{ bgcolor: '#000', borderRadius: 2, p: 2, height: '60vh', overflowY: 'auto' }}> <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)' }}>
{logs.map((log, i) => ( <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.3)' }}>
<Typography key={i} sx={{ <LogIcon fontSize="small" />
fontFamily: 'monospace', <Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 1 }}>REALTIME SERVICE LOGS</Typography>
fontSize: '0.8rem', </Box>
color: log.toLowerCase().includes('error') ? '#f44336' : (log.toLowerCase().includes('warn') ? '#ff9800' : '#33d9b2'), {logs.length === 0 ? (
mb: 0.5 <Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.2 }}>
}}> <Typography variant="body2" sx={{ fontStyle: 'italic' }}>{t('diagnostics.noLogs')}</Typography>
{log} </Box>
</Typography> ) : (
))} logs.map((log, i) => (
<Typography key={i} sx={{
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} /> <div ref={logEndRef} />
</Box> </Box>
)} )}
{activeTab === 4 && ( {activeTab === 'projects' && (
<Paper sx={{ bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)', overflow: 'hidden' }}> <Paper sx={{ bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)', overflow: 'hidden' }}>
<List disablePadding> <List disablePadding>
{serviceProjects.length === 0 ? ( {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) => ( serviceProjects.map((p_item, idx) => (
<Box key={p.id}> <Box key={p_item.id}>
<ListItem> <ListItem sx={{ py: 3, px: 4, '&:hover': { bgcolor: 'rgba(255,255,255,0.02)' } }}>
<Paper sx={{ p: 1, mr: 2, bgcolor: 'rgba(255,255,255,0.03)' }}><ProjectIcon sx={{ opacity: 0.5 }} /></Paper> <Avatar sx={{ mr: 3, bgcolor: 'rgba(255,255,255,0.03)', color: 'primary.main', borderRadius: 2 }}>
<ProjectIcon />
</Avatar>
<ListItemText <ListItemText
primary={p.name} primary={p_item.name}
secondary={`${p.host} • PHP ${p.phpVersion}`} secondary={`${p_item.host} • PHP ${p_item.phpVersion}`}
primaryTypographyProps={{ fontWeight: 700 }} primaryTypographyProps={{ fontWeight: 800, fontSize: '1.1rem' }}
secondaryTypographyProps={{ sx: { opacity: 0.5, mt: 0.5, fontFamily: 'monospace' } }}
/> />
<ListItemSecondaryAction> <ListItemSecondaryAction>
<IconButton onClick={() => onSelectProject(p.id)} color="primary"> <Button
<OpenIcon fontSize="small" /> variant="outlined"
</IconButton> onClick={() => onSelectProject(p_item.id)}
startIcon={<OpenIcon fontSize="small" />}
sx={{ borderRadius: 2, fontWeight: 700 }}
>
{t('common.open')}
</Button>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>
{idx < serviceProjects.length - 1 && <Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />} {idx < serviceProjects.length - 1 && <Divider sx={{ opacity: 0.05 }} />}
</Box> </Box>
)) ))
)} )}
@@ -7,12 +7,17 @@ import {
Tabs, Tabs,
Tab, Tab,
Grid, Grid,
Chip,
Divider, Divider,
Stack, Stack,
Tooltip, Tooltip,
CircularProgress, CircularProgress,
Alert Alert,
TextField,
MenuItem,
Select,
FormControl,
InputLabel,
Paper
} from '@mui/material' } from '@mui/material'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { import {
@@ -21,12 +26,15 @@ import {
Stop as StopIcon, Stop as StopIcon,
PlayArrow as StartIcon, PlayArrow as StartIcon,
Launch as LaunchIcon, Launch as LaunchIcon,
Edit as EditIcon,
Delete as DeleteIcon, Delete as DeleteIcon,
Tune as TuningIcon, Tune as TuningIcon,
OpenInNew as OpenIcon, OpenInNew as OpenIcon,
Code as CodeIcon, 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' } from '@mui/icons-material'
import ProjectResourceMonitor from './ProjectResourceMonitor' import ProjectResourceMonitor from './ProjectResourceMonitor'
import ProjectConfigEditor from './ProjectConfigEditor' import ProjectConfigEditor from './ProjectConfigEditor'
@@ -36,6 +44,8 @@ interface ProjectDetailsProps {
project: any; project: any;
status: any; status: any;
settings: any; settings: any;
phpVersions: any[];
mariaDbVersions: any[];
projectDetailTab: number; projectDetailTab: number;
setProjectDetailTab: (v: number) => void; setProjectDetailTab: (v: number) => void;
t: any; t: any;
@@ -55,10 +65,11 @@ export default function ProjectDetails({
project: p, project: p,
status, status,
settings, settings,
phpVersions,
mariaDbVersions,
projectDetailTab, projectDetailTab,
setProjectDetailTab, setProjectDetailTab,
t, t,
onEdit,
onDelete, onDelete,
onToggleStatus, onToggleStatus,
onOpenSite, onOpenSite,
@@ -73,6 +84,28 @@ export default function ProjectDetails({
const [configText, setConfigText] = useState<string>(''); const [configText, setConfigText] = useState<string>('');
const [isConfigLoading, setIsConfigLoading] = useState<boolean>(false); const [isConfigLoading, setIsConfigLoading] = useState<boolean>(false);
const [isConfigSaving, setIsConfigSaving] = 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(() => { useEffect(() => {
if (p?.host) { 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) { if (!p) {
return ( return (
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: 0.3 }}> <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' status[`php:${p.phpVersion}`]?.status === 'running'
); );
const installedPhp = phpVersions.filter(v => v.status === 'installed');
const installedMariaDb = mariaDbVersions.filter(v => v.status === 'installed');
return ( return (
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', bgcolor: 'rgba(0,0,0,0.1)', height: '100%', overflow: 'hidden' }}> <Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', bgcolor: 'rgba(255,255,255,0.01)', height: '100%', overflow: 'hidden', position: 'relative' }}>
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'flex-start', gap: 2 }}> {/* Header */}
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main', width: 48, height: 48, mt: 0.5 }}> <Box sx={{ p: 4, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'flex-start', gap: 3, position: 'relative' }}>
<ProjectIcon /> <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> </Avatar>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2.5 }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}>{p.name}</Typography> <Typography variant="h4" sx={{ fontWeight: 800, letterSpacing: '-0.02em', mb: 0.5 }}>{p.name}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <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} {p.path}
</Typography> </Typography>
<IconButton size="small" onClick={onOpenFolder} sx={{ color: 'rgba(255,255,255,0.2)', p: 0.5 }}> <Tooltip title={t('projects.open_folder')}>
<FolderOpenIcon sx={{ fontSize: '0.9rem' }} /> <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)' } }}>
</IconButton> <FolderOpenIcon sx={{ fontSize: '1rem' }} />
</IconButton>
</Tooltip>
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
<Button <Button
variant="outlined" variant="contained"
size="small" size="medium"
disabled={isTransitioning} disabled={isTransitioning}
startIcon={isTransitioning ? <CircularProgress size={16} /> : (isRunning ? <StopIcon /> : <StartIcon />)} startIcon={isTransitioning ? <CircularProgress size={18} color="inherit" /> : (isRunning ? <StopIcon /> : <StartIcon />)}
color={isRunning ? 'error' : 'primary'} color={isRunning ? 'error' : 'success'}
onClick={async () => { onClick={async () => {
if (setTransitioning) setTransitioning(true); if (setTransitioning) setTransitioning(true);
try { try {
await onToggleStatus(); await onToggleStatus();
} finally { } finally {
if (setTransitioning) { if (setTransitioning) {
// Short delay to ensure status update is reflected
setTimeout(() => setTransitioning(false), 500); setTimeout(() => setTransitioning(false), 500);
} }
} }
}} }}
sx={{ 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)' }}
px: 2,
height: 36,
fontWeight: 'bold',
transition: 'all 0.1s ease-in-out',
'&:active': { transform: 'scale(0.96)' }
}}
> >
{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>
<Button <Button
variant="outlined" variant="outlined"
size="small" size="medium"
startIcon={<LaunchIcon />} startIcon={<LaunchIcon />}
onClick={() => onOpenSite(isHostMapped)} onClick={() => onOpenSite(isHostMapped)}
sx={{ 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)' } }}
px: 2,
height: 36,
transition: 'all 0.1s ease-in-out',
'&:active': { transform: 'scale(0.96)' }
}}
> >
{t('projects.open_site')} {t('projects.open_site').toUpperCase()}
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
size="small" size="medium"
startIcon={<CodeIcon />} startIcon={<CodeIcon />}
onClick={onOpenVSCode} onClick={onOpenVSCode}
sx={{ 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)' } }}
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)' }
}}
> >
VS Code Aç VS CODE AÇ
</Button> </Button>
{!isHostMapped && ( {!isHostMapped && (
<Button <Button
variant="outlined" variant="contained"
size="small" size="medium"
color="warning" color="warning"
onClick={() => { onClick={() => {
const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`; const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`;
onAddHost(finalHost); onAddHost(finalHost);
}} }}
sx={{ sx={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 800, boxShadow: '0 4px 12px rgba(255, 152, 0, 0.2)' }}
px: 2,
height: 36,
fontWeight: 'bold',
transition: 'all 0.1s ease-in-out',
'&:active': { transform: 'scale(0.96)' }
}}
> >
Hosts Dosyasına Ekle HOSTS DOSYASINA EKLE
</Button> </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>
{/* 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> </Box>
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}> {/* 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"> <Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary">
<Tab label={t('projects.overview')} /> <Tab label={t('projects.overview')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} />
<Tab label={t('projects.database')} /> <Tab label={t('projects.database')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} />
<Tab label={t('projects.server_settings')} /> <Tab label={t('projects.server_settings')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} />
</Tabs> </Tabs>
</Box> </Box>
{/* Content Area */}
<Box sx={{ p: 0, flexGrow: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}> <Box sx={{ p: 0, flexGrow: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{projectDetailTab === 0 && ( {projectDetailTab === 0 && (
<Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}> <Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}>
<Grid container spacing={3}> <Grid container spacing={4}>
<Grid item xs={12} md={6}> {/* Editable Form Side */}
<Box sx={{ mb: 4 }}> <Grid item xs={12} md={7}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.site_domain')}</Typography> <Paper sx={{ p: 3, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}> <Typography variant="h6" sx={{ mb: 3, fontWeight: 800, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography sx={{ fontWeight: 500 }}>{p.host}.local</Typography> <SettingsIcon /> {t('projects.edit_details', 'Proje Detaylarını Düzenle')}
<Chip label="CHANGE" size="small" variant="outlined" sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} /> </Typography>
</Box>
</Box>
<Box sx={{ mb: 4 }}> <Stack spacing={3}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.web_server')}</Typography> <TextField
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}> label={t('projects.site_name')}
<Typography sx={{ fontWeight: 500 }}>{p.serverType.toUpperCase()}</Typography> fullWidth
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} /> variant="filled"
</Box> value={editForm.name}
</Box> onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }}
/>
<Box sx={{ mb: 4 }}> <TextField
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.php_version')}</Typography> label={t('projects.site_domain')}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}> fullWidth
<Typography sx={{ fontWeight: 500 }}>{p.phpVersion}</Typography> variant="filled"
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} /> value={editForm.host}
</Box> onChange={(e) => setEditForm({ ...editForm, host: e.target.value })}
</Box> 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>
<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> </Grid>
<Grid item xs={12} md={6}> {/* Status Monitor Side */}
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}> <Grid item xs={12} md={5}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}> <Paper sx={{ p: 3, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)', height: '100%' }}>
<TuningIcon fontSize="small" color="primary" /> {t('dashboard.status')} <Typography variant="subtitle1" sx={{ mb: 4, fontWeight: 800, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.6)' }}>
<StatusIcon sx={{ color: 'secondary.main' }} /> {t('dashboard.status')}
</Typography> </Typography>
<ProjectResourceMonitor
serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'} <Box sx={{ mb: 4 }}>
serverStats={p.serverType === 'apache' ? status.apache : status.nginx} <ProjectResourceMonitor
phpStats={status[`php:${p.phpVersion}`]} serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'}
/> serverStats={p.serverType === 'apache' ? status.apache : status.nginx}
</Box> 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>
</Grid> </Grid>
</Box> </Box>
@@ -314,63 +454,66 @@ export default function ProjectDetails({
{projectDetailTab === 1 && ( {projectDetailTab === 1 && (
<Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}> <Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}>
<Box sx={{ maxWidth: 600 }}> <Box sx={{ maxWidth: 700 }}>
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}> <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', mb: 3 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.mariadb_version')}</Typography> <Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'secondary.main' }}>
<Typography sx={{ mt: 0.5, fontWeight: 600 }}>MariaDB {p.mariadbVersion}</Typography> <DbIcon />
</Avatar>
<Box>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>{t('projects.mariadb_version')}</Typography>
<Typography variant="h6" sx={{ fontWeight: 700 }}>MariaDB {p.mariadbVersion}</Typography>
</Box>
</Box> </Box>
<Button <Button
variant="outlined" variant="outlined"
size="small" size="medium"
startIcon={<OpenIcon />} startIcon={<OpenIcon />}
onClick={onManageDb} onClick={onManageDb}
sx={{ borderRadius: 2.5, px: 3, height: 44, fontWeight: 700 }}
> >
{t('mariadb_manager.title')} {t('mariadb_manager.title')}
</Button> </Button>
</Box> </Box>
<Divider sx={{ mb: 3, opacity: 0.1 }} /> <Divider sx={{ mb: 4, opacity: 0.1 }} />
<Stack spacing={2}> <Grid container spacing={3}>
<Box> {[
<Typography variant="caption" color="text.secondary">Host</Typography> { label: 'Host', value: 'localhost' },
<Typography variant="body2" sx={{ fontWeight: 600 }}>localhost</Typography> { label: 'Port', value: settings.mariaDbPort },
</Box> { label: 'Username', value: 'root' },
<Box> { label: 'Password', value: '(no password)', italic: true }
<Typography variant="caption" color="text.secondary">Port</Typography> ].map((item, idx) => (
<Typography variant="body2" sx={{ fontWeight: 600 }}>{settings.mariaDbPort}</Typography> <Grid item xs={6} key={idx}>
</Box> <Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700, display: 'block', mb: 0.5 }}>{item.label.toUpperCase()}</Typography>
<Box> <Typography variant="body1" sx={{ fontWeight: 600, color: item.italic ? 'rgba(255,255,255,0.3)' : '#fff', fontStyle: item.italic ? 'italic' : 'normal' }}>
<Typography variant="caption" color="text.secondary">Username</Typography> {item.value}
<Typography variant="body2" sx={{ fontWeight: 600 }}>root</Typography> </Typography>
</Box> </Grid>
<Box> ))}
<Typography variant="caption" color="text.secondary">Password</Typography> </Grid>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>(no password)</Typography> </Paper>
</Box>
</Stack>
</Box>
</Box> </Box>
</Box> </Box>
)} )}
{projectDetailTab === 2 && ( {projectDetailTab === 2 && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> <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 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CodeIcon color="primary" /> <ServerIcon color="primary" />
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}> <Typography variant="subtitle2" sx={{ fontWeight: 800, textTransform: 'uppercase', letterSpacing: 1 }}>
{p.serverType.toUpperCase()} {t('projects.server_settings')} {p.serverType} {t('projects.server_settings')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1.5 }}>
<Button <Button
size="small" size="small"
startIcon={<TuningIcon />} startIcon={<TuningIcon />}
onClick={loadConfig} onClick={loadConfig}
sx={{ color: 'rgba(255,255,255,0.5)' }} sx={{ color: 'rgba(255,255,255,0.5)', borderRadius: 2 }}
> >
{t('common.refresh')} {t('common.refresh')}
</Button> </Button>
@@ -378,17 +521,17 @@ export default function ProjectDetails({
variant="contained" variant="contained"
size="small" size="small"
color="primary" color="primary"
startIcon={isConfigSaving ? <CircularProgress size={16} /> : <SaveIcon />} startIcon={isConfigSaving ? <CircularProgress size={16} color="inherit" /> : <SaveIcon />}
disabled={isConfigSaving || isConfigLoading} disabled={isConfigSaving || isConfigLoading}
onClick={handleSaveConfig} onClick={handleSaveConfig}
sx={{ px: 3 }} sx={{ px: 4, borderRadius: 2, fontWeight: 800 }}
> >
{t('common.save').toUpperCase()} {t('common.save').toUpperCase()}
</Button> </Button>
</Box> </Box>
</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')} {t('projects.manual_edit_warning')}
</Alert> </Alert>
@@ -6,6 +6,8 @@ interface ProjectManagerProps {
projects: any[]; projects: any[];
status: any; status: any;
settings: any; settings: any;
phpVersions: any[];
mariaDbVersions: any[];
projectSearch: string; projectSearch: string;
setProjectSearch: (v: string) => void; setProjectSearch: (v: string) => void;
selectedProjectId: string | null; selectedProjectId: string | null;
@@ -28,6 +30,8 @@ export default function ProjectManager({
projects, projects,
status, status,
settings, settings,
phpVersions,
mariaDbVersions,
projectSearch, projectSearch,
setProjectSearch, setProjectSearch,
selectedProjectId, selectedProjectId,
@@ -72,6 +76,8 @@ export default function ProjectManager({
project={p} project={p}
status={status} status={status}
settings={settings} settings={settings}
phpVersions={phpVersions}
mariaDbVersions={mariaDbVersions}
projectDetailTab={projectDetailTab} projectDetailTab={projectDetailTab}
setProjectDetailTab={setProjectDetailTab} setProjectDetailTab={setProjectDetailTab}
t={t} t={t}