feat: implement multi-service management dashboard with project, database, and PHP extension support

This commit is contained in:
Ümit Tunç
2026-04-03 15:24:20 +03:00
parent 2413c19393
commit e7581d442a
11 changed files with 1066 additions and 333 deletions
+635
View File
@@ -1,4 +1,639 @@
[ [
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "5cna280nd",
"timestamp": 1775215993040
},
{
"service": "php:8.5.4",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "16dhybbv5",
"timestamp": 1775215993039
},
{
"service": "php:5.6.40",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "hilyatq39",
"timestamp": 1775215993031
},
{
"service": "php:7.4.33",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "hbnrqkyoy",
"timestamp": 1775215993023
},
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "m5oe9unpm",
"timestamp": 1775215978795
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "g9k4uuyb6",
"timestamp": 1775215978779
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "igj6t393e",
"timestamp": 1775215978760
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "bv7xjvoud",
"timestamp": 1775215978742
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "w225k0xie",
"timestamp": 1775215978291
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "x5k5b4mth",
"timestamp": 1775215978272
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "51ppyzb6e",
"timestamp": 1775215977753
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "yq4trg6v9",
"timestamp": 1775215977702
},
{
"service": "mariadb:10.11.8",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "2cesewr4o",
"timestamp": 1775215972222
},
{
"service": "mariadb:11.4.2",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "vrt0ac3xz",
"timestamp": 1775215972214
},
{
"service": "php:8.5.4",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "tz9lm7fhp",
"timestamp": 1775215972206
},
{
"service": "php:5.6.40",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "xrg2toecz",
"timestamp": 1775215972199
},
{
"service": "php:8.2.30",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "82hwbc4ly",
"timestamp": 1775215972191
},
{
"service": "php:7.4.33",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "2noq8lw9w",
"timestamp": 1775215972185
},
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "9oha4uilw",
"timestamp": 1775215972159
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "jrwvf5v1v",
"timestamp": 1775215972157
},
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "1czbs8x8z",
"timestamp": 1775215964037
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "0up4w85t2",
"timestamp": 1775215964021
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "gxnmtaz6o",
"timestamp": 1775215964004
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "dzb88em3m",
"timestamp": 1775215963987
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "bu12qdsrx",
"timestamp": 1775215963482
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "kpy95fkd1",
"timestamp": 1775215963459
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "6z4v7vlbz",
"timestamp": 1775215962983
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "hs28kfp1j",
"timestamp": 1775215962935
},
{
"service": "php:5.6.40",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "xbrt3pru2",
"timestamp": 1775215960741
},
{
"service": "php:8.5.4",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "a9dtwotqn",
"timestamp": 1775215960735
},
{
"service": "php:7.4.33",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "f1p0g2q9k",
"timestamp": 1775215960728
},
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "ybtvisrvv",
"timestamp": 1775215960706
},
{
"service": "php:8.2.30",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "p5uu02qoq",
"timestamp": 1775215960705
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "3l8qhvac1",
"timestamp": 1775215960695
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "rradgo2b9",
"timestamp": 1775215905340
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "7xbqz2ep0",
"timestamp": 1775215904095
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "439aj13y2",
"timestamp": 1775215901797
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "mmfuuw4ec",
"timestamp": 1775215900591
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "vdc11eywn",
"timestamp": 1775215869698
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "nvd414ghx",
"timestamp": 1775215868488
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "8acx9s7xg",
"timestamp": 1775215865230
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "sxtv8gp6o",
"timestamp": 1775215863969
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "7i2g4nqu5",
"timestamp": 1775215838776
},
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "5oab99keg",
"timestamp": 1775215837463
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "spcyyxy1w",
"timestamp": 1775215831998
},
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "vbhzxidnn",
"timestamp": 1775215796300
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "gm6ejjr5a",
"timestamp": 1775215796279
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "i8npou62c",
"timestamp": 1775215796260
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "6ncfpzo9k",
"timestamp": 1775215796242
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "im5nypjfv",
"timestamp": 1775215795208
},
{
"service": "mariadb:10.11.8",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "za9mzxy37",
"timestamp": 1775215792771
},
{
"service": "mariadb:11.4.2",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "mhlcdoxmt",
"timestamp": 1775215792764
},
{
"service": "php:8.5.4",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "eycw4hxb5",
"timestamp": 1775215792756
},
{
"service": "php:5.6.40",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "2z7211041",
"timestamp": 1775215792749
},
{
"service": "php:8.2.30",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "jg1diypfu",
"timestamp": 1775215792743
},
{
"service": "php:7.4.33",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "6rg35x2f8",
"timestamp": 1775215792736
},
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "ydizt9o8j",
"timestamp": 1775215792717
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "afhcuxp1i",
"timestamp": 1775215792707
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "u0nzj9fmp",
"timestamp": 1775215787250
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: null",
"id": "lakde3svs",
"timestamp": 1775215781259
},
{
"service": "nginx",
"action": "stop",
"status": "info",
"id": "89gx0ty64",
"timestamp": 1775215781255
},
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "sj461rtvu",
"timestamp": 1775215438924
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "y8yibjkty",
"timestamp": 1775215438908
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "c2gx42hum",
"timestamp": 1775215438891
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "zzhvseonm",
"timestamp": 1775215438875
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "j39z9a0js",
"timestamp": 1775215438399
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "wjz63bmwh",
"timestamp": 1775215438380
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "cl5wh9qzj",
"timestamp": 1775215437873
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "chvykaz6s",
"timestamp": 1775215437824
},
{
"service": "mariadb:10.11.8",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "e1p660xfr",
"timestamp": 1775215430918
},
{
"service": "mariadb:11.4.2",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "zcp93q2mf",
"timestamp": 1775215430911
},
{
"service": "php:8.5.4",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "9lsi8dk1j",
"timestamp": 1775215430903
},
{
"service": "php:5.6.40",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "g8a4x1i2q",
"timestamp": 1775215430896
},
{
"service": "php:8.2.30",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "tjrzyhopg",
"timestamp": 1775215430890
},
{
"service": "php:7.4.33",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "wu418ce9i",
"timestamp": 1775215430884
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "8tbns1luu",
"timestamp": 1775215430877
},
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "bfecmmhs2",
"timestamp": 1775215430863
},
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "vhbul5hj6",
"timestamp": 1775215425999
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "xhdvhe9v9",
"timestamp": 1775215425982
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "whbiv7hnd",
"timestamp": 1775215425964
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "njoudmced",
"timestamp": 1775215425945
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "fv15yavsp",
"timestamp": 1775215425449
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "q646z3m24",
"timestamp": 1775215425427
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "i9b435j7i",
"timestamp": 1775215424957
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "g9eb3ymy8",
"timestamp": 1775215424900
},
{ {
"service": "php:5.6.40", "service": "php:5.6.40",
"action": "start", "action": "start",
+4
View File
@@ -719,6 +719,10 @@ export function registerIpcHandlers(): void {
return processManager.getStatuses() return processManager.getStatuses()
}) })
ipcMain.handle('service:status', async (_event, name: string) => {
return processManager.getServiceState(name);
})
ipcMain.handle('services:logs', async (_event, serviceName: string) => { ipcMain.handle('services:logs', async (_event, serviceName: string) => {
return processManager.getLogs(serviceName) return processManager.getLogs(serviceName)
}) })
+224 -125
View File
@@ -459,79 +459,130 @@ function App(): JSX.Element {
setSettings(prev => ({ ...prev, language: newLang })) setSettings(prev => ({ ...prev, language: newLang }))
} }
const handleStartAll = async () => { const runServiceOperation = async (title: string, services: string[], operation: 'start' | 'stop') => {
if (!window.api) return const results: Record<string, any> = {};
const criticalServices = ['nginx', 'apache'] const getDisplayLabel = (srv: string) => {
for (const s of criticalServices) { if (srv === 'nginx') return 'Nginx';
if (status[s]?.status === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } })) if (srv === 'apache') return 'Apache';
if (srv.startsWith('php:')) return `PHP ${srv.split(':')[1]}`;
if (srv.startsWith('mariadb:')) return `MariaDB ${srv.split(':')[1]}`;
return srv;
};
const updateSwal = (currentSrv?: string) => {
const html = `
<div style="text-align: left; font-family: 'Inter', sans-serif; padding-top: 10px;">
${services.map(srv => {
const label = getDisplayLabel(srv);
const res = results[srv];
let icon = '<span style="color: rgba(255,255,255,0.1); margin-right: 12px; font-size: 0.8rem;">○</span>';
let statusColor = 'rgba(255,255,255,0.4)';
let note = operation === 'start' ? t('common.ready') : t('common.running');
if (res?.success) {
icon = '<span style="color: #33d9b2; margin-right: 12px; font-size: 0.8rem;">●</span>';
statusColor = '#33d9b2';
note = operation === 'start' ? t('common.started') : t('common.stopped');
} else if (res?.error) {
icon = '<span style="color: #ff5252; margin-right: 12px; font-size: 0.8rem;">●</span>';
statusColor = '#ff5252';
note = t('common.error');
} else if (srv === currentSrv) {
icon = '<span style="color: #39A7FF; margin-right: 12px; font-size: 0.8rem;" class="swal2-loading-dots">●</span>';
statusColor = '#39A7FF';
note = operation === 'start' ? t('common.starting') : t('common.stopping');
}
return '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; padding: 8px 14px; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.03); border-radius: 8px; font-size: 0.8rem;">' +
'<span style="font-weight: 600; display: flex; align-items: center;">' + icon + ' ' + label + '</span>' +
'<span style="font-size: 0.65rem; color: ' + statusColor + '; font-weight: 800; letter-spacing: 0.5px;">' + note.toUpperCase() + '</span>' +
'</div>';
}).join('')}
</div>
`;
Swal.update({ html });
};
Swal.fire({
title: title.toUpperCase(),
html: '<div></div>',
allowOutsideClick: false,
showConfirmButton: false,
background: '#1a1d21',
color: '#fff',
customClass: {
title: 'swal-title-small'
},
didOpen: () => {
updateSwal();
}
});
for (const srv of services) {
updateSwal(srv);
try {
const res = operation === 'start' ? await window.api.startService(srv) : await window.api.stopService(srv);
if (res.success) {
// Poll for status confirmation
let attempts = 0;
const maxAttempts = 15; // ~7.5 seconds
let confirmed = false;
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 500));
const statusRes = await window.api.invoke('service:status', srv);
if (operation === 'start' && statusRes === 'running') {
confirmed = true;
break;
}
if (operation === 'stop' && statusRes === 'stopped') {
confirmed = true;
break;
}
attempts++;
}
results[srv] = { success: confirmed, error: !confirmed ? t('common.timeout') : null };
} else {
results[srv] = { success: false, error: res.message };
}
} catch (err: any) {
results[srv] = { success: false, error: err.message };
}
updateSwal();
} }
const phpToStart = phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`)
for (const s of phpToStart) {
if ((status[s]?.status || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } }))
}
const mariaDbToStart = mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`) const anyError = Object.values(results).some(r => r.error);
for (const s of mariaDbToStart) {
if ((status[s]?.status || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } })) Swal.update({
} icon: anyError ? 'warning' : 'success',
showConfirmButton: true,
confirmButtonText: t('common.close').toUpperCase()
});
const results = await Promise.all([ // If no error, auto-close after 3s
...criticalServices.filter(s => status[s]?.status !== 'running').map(s => window.api?.startService(s).then((r: any) => ({ service: s, ...r }))), if (!anyError) {
...phpToStart.filter(s => (status[s]?.status || 'stopped') !== 'running').map(s => window.api?.startService(s).then((r: any) => ({ service: s, ...r }))), setTimeout(() => {
...mariaDbToStart.filter(s => (status[s]?.status || 'stopped') !== 'running').map(s => window.api?.startService(s).then((r: any) => ({ service: s, ...r }))) if (Swal.isVisible()) Swal.close();
]) }, 3000);
const errors = results.filter(r => r && !r.success)
if (errors.length > 0) {
const errorList = errors.map(e => `<b>${e.service}:</b> ${e.message}`).join('<br/>')
Swal.fire({
title: t('common.error'),
html: `<div style="text-align: left; font-size: 0.9rem;">${errorList}</div>`,
icon: 'error',
background: '#1e1e1e',
color: '#fff'
})
} else {
setNotification({ open: true, message: t('dashboard.all_started'), severity: 'success' })
} }
fetchStatus() fetchStatus();
} };
const handleStopAll = async () => { const handleStartAll = () => {
if (!window.api) return const services = ['nginx', 'apache', ...phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`), ...mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`)];
runServiceOperation(t('dashboard.starting_all'), services, 'start');
// 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') const handleStopAll = () => {
const services = ['nginx', 'apache', ...phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`), ...mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`)];
if (!result.success) { runServiceOperation(t('dashboard.stopping_all'), services, 'stop');
Swal.fire({ };
title: t('common.error'),
text: result.message,
icon: 'error',
background: '#1e1e1e',
color: '#fff'
})
} else {
setNotification({ open: true, message: t('dashboard.all_stopped'), severity: 'success' })
}
fetchStatus()
}
const formatMemory = (bytes?: number) => {
if (!bytes) return ''
const mb = bytes / (1024 * 1024)
if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB`
return `${Math.round(mb)} MB`
}
const handleAddProject = async () => { const handleAddProject = async () => {
if (!window.api) return if (!window.api) return
@@ -716,7 +767,6 @@ function App(): JSX.Element {
setIsPhpExtensionsOpen(true) setIsPhpExtensionsOpen(true)
} }
const fetchProcessList = async (id: string) => { const fetchProcessList = async (id: string) => {
if (!window.api) return if (!window.api) return
setIsProcessListLoading(true) setIsProcessListLoading(true)
@@ -752,6 +802,7 @@ function App(): JSX.Element {
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed') const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
return ( return (
<> <>
<Dialog <Dialog
@@ -841,6 +892,21 @@ function App(): JSX.Element {
'.swal2-container': { '.swal2-container': {
zIndex: '9999 !important' zIndex: '9999 !important'
}, },
'.swal-title-small': {
fontSize: '1rem !important',
fontWeight: '800 !important',
letterSpacing: '1px !important',
color: '#fff !important',
marginTop: '10px !important'
},
'@keyframes swalLoadingDots': {
'0%': { opacity: 0.3, transform: 'scale(0.8)' },
'50%': { opacity: 1, transform: 'scale(1.2)' },
'100%': { opacity: 0.3, transform: 'scale(0.8)' }
},
'.swal2-loading-dots': {
animation: 'swalLoadingDots 1.4s infinite ease-in-out'
},
'.nginx-error-container': { '.nginx-error-container': {
textAlign: 'left !important', textAlign: 'left !important',
fontFamily: 'monospace !important', fontFamily: 'monospace !important',
@@ -863,51 +929,84 @@ function App(): JSX.Element {
backdropFilter: 'blur(10px)' backdropFilter: 'blur(10px)'
}} }}
> >
<Toolbar sx={{ gap: 1 }}> <Toolbar sx={{ gap: 1.5, minHeight: '56px !important' }}>
<Box sx={{ flexGrow: 1 }} /> <Box sx={{ flexGrow: 1 }} />
<Tooltip title={t('dashboard.start_all')}>
<IconButton color="inherit" onClick={handleStartAll} sx={{ '&:hover': { color: 'success.main' } }}> <Button
<StartIcon /> variant="outlined"
</IconButton> size="small"
</Tooltip> color="success"
<Tooltip title={t('dashboard.stop_all')}> onClick={handleStartAll}
<IconButton color="inherit" onClick={handleStopAll} sx={{ '&:hover': { color: 'error.main' } }}> startIcon={<StartIcon sx={{ fontSize: '1rem !important' }} />}
<StopIcon /> sx={{
</IconButton> borderRadius: 1.5,
</Tooltip> fontSize: '0.7rem',
<Divider orientation="vertical" flexItem sx={{ mx: 1, borderColor: 'rgba(255,255,255,0.1)' }} /> fontWeight: 800,
<FormControl size="small" variant="outlined" sx={{ minWidth: 80 }}> textTransform: 'none',
borderColor: 'rgba(76, 175, 80, 0.3)',
px: 1.5,
height: 28
}}
>
{t('dashboard.start_all')}
</Button>
<Button
variant="outlined"
size="small"
color="error"
onClick={handleStopAll}
startIcon={<StopIcon sx={{ fontSize: '1rem !important' }} />}
sx={{
borderRadius: 1.5,
fontSize: '0.7rem',
fontWeight: 800,
textTransform: 'none',
borderColor: 'rgba(244, 67, 54, 0.3)',
px: 1.5,
height: 28
}}
>
{t('dashboard.stop_all')}
</Button>
<Divider orientation="vertical" flexItem sx={{ mx: 0.5, borderColor: 'rgba(255,255,255,0.1)', height: 20, mt: 1.5 }} />
<FormControl size="small" variant="outlined" sx={{ minWidth: 70 }}>
<Select <Select
value={i18n.language} value={i18n.language}
onChange={handleLanguageChange} onChange={handleLanguageChange}
displayEmpty displayEmpty
IconComponent={LanguageIcon} size="small"
sx={{ sx={{
color: 'white', color: 'white',
height: '35px', height: '28px',
fontSize: '0.8rem', fontSize: '0.7rem',
'.MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.2)' }, fontWeight: 700,
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.4)' }, '.MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.1)' },
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'primary.main' }, '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.2)' },
'.MuiSelect-icon': { color: 'white' } '.MuiSelect-icon': { color: 'rgba(255,255,255,0.5)', fontSize: '1rem' }
}} }}
> >
{supportedLngs.map(lang => ( {supportedLngs.map(lang => (
<MenuItem key={lang} value={lang}> <MenuItem key={lang} value={lang} sx={{ fontSize: '0.75rem' }}>
{lang.toUpperCase()} {lang.toUpperCase()}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<Tooltip title={t('mariadb_process_monitor.title')}> <Tooltip title={t('mariadb_process_monitor.title')}>
<IconButton color="inherit" onClick={() => setActiveTab('db_monitor')} sx={{ '&:hover': { color: 'primary.main' } }}> <IconButton size="small" color="inherit" onClick={() => setActiveTab('db_monitor')} sx={{ p: 0.5, opacity: 0.7, '&:hover': { opacity: 1, color: 'primary.main' } }}>
<TerminalIcon /> <TerminalIcon sx={{ fontSize: '1.2rem' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<IconButton color="inherit" onClick={() => setActiveTab('settings')} sx={{ ml: 1 }}>
<SettingsIcon /> <IconButton size="small" color="inherit" onClick={() => setActiveTab('settings')} sx={{ p: 0.5, opacity: 0.7, '&:hover': { opacity: 1 } }}>
<SettingsIcon sx={{ fontSize: '1.2rem' }} />
</IconButton> </IconButton>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Drawer <Drawer
@@ -925,60 +1024,60 @@ function App(): JSX.Element {
<Box sx={{ overflow: 'auto' }}> <Box sx={{ overflow: 'auto' }}>
<List> <List>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')}> <ListItemButton selected={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')} sx={{ py: 0.5 }}>
<ListItemIcon><DashboardIcon color={activeTab === 'dashboard' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><DashboardIcon fontSize="small" color={activeTab === 'dashboard' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('dashboard.title')} /> <ListItemText primary={t('dashboard.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'projects'} onClick={() => setActiveTab('projects')}> <ListItemButton selected={activeTab === 'projects'} onClick={() => setActiveTab('projects')} sx={{ py: 0.5 }}>
<ListItemIcon><ProjectIcon color={activeTab === 'projects' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><ProjectIcon fontSize="small" color={activeTab === 'projects' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('projects.title')} /> <ListItemText primary={t('projects.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'tuning'} onClick={() => setActiveTab('tuning')}> <ListItemButton selected={activeTab === 'tuning'} onClick={() => setActiveTab('tuning')} sx={{ py: 0.5 }}>
<ListItemIcon><TuningIcon color={activeTab === 'tuning' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><TuningIcon fontSize="small" color={activeTab === 'tuning' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('tuning.title')} /> <ListItemText primary={t('tuning.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'tools'} onClick={() => setActiveTab('tools')}> <ListItemButton selected={activeTab === 'tools'} onClick={() => setActiveTab('tools')} sx={{ py: 0.5 }}>
<ListItemIcon><ToolsIcon color={activeTab === 'tools' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><ToolsIcon fontSize="small" color={activeTab === 'tools' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('common.tools')} /> <ListItemText primary={t('common.tools')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'db_manager'} onClick={() => setActiveTab('db_manager')}> <ListItemButton selected={activeTab === 'db_manager'} onClick={() => setActiveTab('db_manager')} sx={{ py: 0.5 }}>
<ListItemIcon><DbIcon color={activeTab === 'db_manager' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><DbIcon fontSize="small" color={activeTab === 'db_manager' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_manager.title')} /> <ListItemText primary={t('mariadb_manager.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'db_monitor'} onClick={() => setActiveTab('db_monitor')}> <ListItemButton selected={activeTab === 'db_monitor'} onClick={() => setActiveTab('db_monitor')} sx={{ py: 0.5 }}>
<ListItemIcon><TerminalIcon color={activeTab === 'db_monitor' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><TerminalIcon fontSize="small" color={activeTab === 'db_monitor' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_process_monitor.title')} /> <ListItemText primary={t('mariadb_process_monitor.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
</List> </List>
<Divider /> <Divider sx={{ opacity: 0.05 }} />
<List> <List>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'user_guide'} onClick={() => setActiveTab('user_guide')}> <ListItemButton selected={activeTab === 'user_guide'} onClick={() => setActiveTab('user_guide')} sx={{ py: 0.5 }}>
<ListItemIcon><GuideIcon color={activeTab === 'user_guide' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><GuideIcon fontSize="small" color={activeTab === 'user_guide' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('common.user_guide')} /> <ListItemText primary={t('common.user_guide')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'settings'} onClick={() => setActiveTab('settings')}> <ListItemButton selected={activeTab === 'settings'} onClick={() => setActiveTab('settings')} sx={{ py: 0.5 }}>
<ListItemIcon><SettingsIcon color={activeTab === 'settings' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><SettingsIcon fontSize="small" color={activeTab === 'settings' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('settings.general')} /> <ListItemText primary={t('settings.general')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'event_monitor'} onClick={() => setActiveTab('event_monitor')}> <ListItemButton selected={activeTab === 'event_monitor'} onClick={() => setActiveTab('event_monitor')} sx={{ py: 0.5 }}>
<ListItemIcon><HistoryIcon color={activeTab === 'event_monitor' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon sx={{ minWidth: 40 }}><HistoryIcon fontSize="small" color={activeTab === 'event_monitor' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('service_monitor.title')} /> <ListItemText primary={t('service_monitor.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
</List> </List>
@@ -1402,9 +1501,11 @@ function App(): JSX.Element {
t={t} t={t}
onFetchStatus={fetchStatus} onFetchStatus={fetchStatus}
onSelectProject={handleSelectProject} onSelectProject={handleSelectProject}
runServiceOperation={runServiceOperation}
/> />
)} )}
{activeTab === 'db_monitor' && ( {activeTab === 'db_monitor' && (
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Paper sx={{ p: 3, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2 }}> <Paper sx={{ p: 3, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2 }}>
@@ -1634,13 +1735,10 @@ function App(): JSX.Element {
status[`php:${p.phpVersion}`]?.status === 'running' status[`php:${p.phpVersion}`]?.status === 'running'
); );
if (isRunning) { if (isRunning) {
await window.api.stopService(p.serverType === 'apache' ? 'apache' : 'nginx'); runServiceOperation(t('projects.stop_site'), [p.serverType === 'apache' ? 'apache' : 'nginx', `php:${p.phpVersion}`], 'stop');
await window.api.stopService(`php:${p.phpVersion}`);
} else { } else {
await window.api.startService(p.serverType === 'apache' ? 'apache' : 'nginx'); runServiceOperation(t('projects.start_site'), [p.serverType === 'apache' ? 'apache' : 'nginx', `php:${p.phpVersion}`], 'start');
await window.api.startService(`php:${p.phpVersion}`);
} }
fetchStatus();
}} }}
onOpenSite={(p: any, isHostMapped: boolean) => { onOpenSite={(p: any, isHostMapped: boolean) => {
const port = p.serverType === 'apache' ? settings.apachePort : settings.nginxPort const port = p.serverType === 'apache' ? settings.apachePort : settings.nginxPort
@@ -1665,6 +1763,7 @@ function App(): JSX.Element {
} }
}} }}
onManageDb={() => setActiveTab('db_manager')} onManageDb={() => setActiveTab('db_manager')}
onRefreshProjects={fetchProjects}
/> />
)} )}
@@ -284,22 +284,22 @@ export default function DashboardDetails({
{serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />} {serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />}
</Paper> </Paper>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.5px', color: 'primary.main' }}> <Typography variant="h6" sx={{ fontWeight: 800, letterSpacing: '-0.5px', color: 'primary.main', fontSize: '1.25rem' }}>
{serviceId.replace(':', ' ').replace('php', 'PHP').replace('mariadb', 'MariaDB').replace('nginx', 'Nginx').replace('apache', 'Apache')} {serviceId.replace(':', ' ').replace('php', 'PHP').replace('mariadb', 'MariaDB').replace('nginx', 'Nginx').replace('apache', 'Apache')}
</Typography> </Typography>
<Typography variant="caption" sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}> <Typography variant="caption" sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.2 }}>
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: isRunning ? 'success.main' : 'error.main', boxShadow: isRunning ? '0 0 10px rgba(76, 175, 80, 0.5)' : 'none' }} /> <Box sx={{ width: 6, height: 6, 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, letterSpacing: 0.5 }}>{t('common.starting')}</Box>} {isStarting && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.starting')}</Box>}
{isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5 }}>{t('common.stopping')}</Box>} {isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.stopping')}</Box>}
{isRunning && <Box component="span" sx={{ color: 'success.main', fontWeight: 700, letterSpacing: 0.5 }}>{t('common.running')}</Box>} {isRunning && <Box component="span" sx={{ color: 'success.main', fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.running')}</Box>}
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5, fontWeight: 700, letterSpacing: 0.5 }}>{t('common.stopped')}</Box>} {!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5, fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.stopped')}</Box>}
</Typography> </Typography>
</Box> </Box>
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={1.5}>
{isRunning && !isTransitioning && ( {isRunning && !isTransitioning && (
<Tooltip title={t('common.reload')}> <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 }}> <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: 32, height: 32, borderRadius: 2 }}>
<RefreshIcon /> <RefreshIcon sx={{ fontSize: '1.1rem' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
@@ -307,9 +307,10 @@ export default function DashboardDetails({
variant="outlined" variant="outlined"
color={isRunning ? 'error' : 'success'} color={isRunning ? 'error' : 'success'}
disabled={isTransitioning} disabled={isTransitioning}
startIcon={isTransitioning ? <CircularProgress size={20} color="inherit" /> : isRunning ? <StopIcon /> : <StartIcon />} size="small"
startIcon={isTransitioning ? <CircularProgress size={14} color="inherit" /> : isRunning ? <StopIcon sx={{ fontSize: '1.1rem' }} /> : <StartIcon sx={{ fontSize: '1.1rem' }} />}
onClick={() => onToggleService(serviceId)} onClick={() => onToggleService(serviceId)}
sx={{ borderRadius: 3, px: 4, height: 48, fontWeight: 800, fontSize: '1rem', boxShadow: 'none' }} sx={{ borderRadius: 2, px: 2, height: 32, fontWeight: 800, fontSize: '0.75rem', boxShadow: 'none' }}
> >
{isStarting ? t('common.starting') : (isStopping ? t('common.stopping') : (isRunning ? t('common.stop') : t('common.start')))} {isStarting ? t('common.starting') : (isStopping ? t('common.stopping') : (isRunning ? t('common.stop') : t('common.start')))}
</Button> </Button>
@@ -318,7 +319,7 @@ export default function DashboardDetails({
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 4, bgcolor: 'rgba(255,255,255,0.01)' }}> <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" sx={{ <Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} textColor="primary" indicatorColor="primary" sx={{
'.MuiTab-root': { textTransform: 'none', minHeight: 64, fontWeight: 700, fontSize: '0.95rem' } '.MuiTab-root': { textTransform: 'none', minHeight: 48, fontWeight: 700, fontSize: '0.8rem' }
}}> }}>
<Tab label={t('dashboard.overview')} value="overview" /> <Tab label={t('dashboard.overview')} value="overview" />
<Tab label={isPhp ? 'php.ini (Şablon)' : t('dashboard.service_settings')} value="settings_template" /> <Tab label={isPhp ? 'php.ini (Şablon)' : t('dashboard.service_settings')} value="settings_template" />
@@ -331,38 +332,37 @@ export default function DashboardDetails({
</Tabs> </Tabs>
</Box> </Box>
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 4 }}> <Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3 }}>
{activeTab === 'overview' && ( {activeTab === 'overview' && (
<Grid container spacing={4}> <Grid container spacing={2}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}> <Typography variant="body2" sx={{ fontWeight: 800, mb: 1.5, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.8rem' }}>
<StatusIcon /> {t('dashboard.system_stats', 'Sistem İstatistikleri')} <StatusIcon sx={{ fontSize: '1rem' }} /> {t('dashboard.system_stats', 'Sistem İstatistikleri')}
</Typography> </Typography>
<Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}> <Paper sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 3, 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="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}> <Typography variant="body2" sx={{ fontWeight: 800, mb: 1.5, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.8rem' }}>
<SettingsIcon /> {t('dashboard.service_info', 'Servis Bilgileri')} <SettingsIcon sx={{ fontSize: '1rem' }} /> {t('dashboard.service_info', 'Servis Bilgileri')}
</Typography> </Typography>
<Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}> <Paper sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
<Stack spacing={3}> <Stack spacing={1.5}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.port', 'Port')}</Typography> <Typography variant="caption" sx={{ opacity: 0.5, fontWeight: 700, fontSize: '0.75rem' }}>{t('common.port', 'Port')}</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="body2" sx={{ fontWeight: 800, color: 'primary.main', fontFamily: 'monospace', fontSize: '0.75rem' }}>
<Typography variant="body1" sx={{ fontWeight: 800, color: 'primary.main', fontFamily: 'monospace' }}> {getPort()}
{getPort()} </Typography>
</Typography>
</Paper>
</Box> </Box>
<Divider sx={{ opacity: 0.05 }} /> <Divider sx={{ opacity: 0.05 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.status', 'Durum')}</Typography> <Typography variant="caption" sx={{ opacity: 0.5, fontWeight: 700, fontSize: '0.75rem' }}>{t('common.status', 'Durum')}</Typography>
<Chip <Chip
label={isRunning ? t('common.running', 'Çalışıyor') : t('common.stopped', 'Durduruldu')} label={isRunning ? t('common.running', 'Çalışıyor') : t('common.stopped', 'Durduruldu')}
size="small"
color={isRunning ? 'success' : 'error'} color={isRunning ? 'success' : 'error'}
sx={{ fontWeight: 900, borderRadius: 2 }} sx={{ fontWeight: 900, borderRadius: 1.5, height: 20, fontSize: '0.65rem' }}
/> />
</Box> </Box>
</Stack> </Stack>
@@ -373,21 +373,22 @@ export default function DashboardDetails({
{(activeTab === 'settings_template' || activeTab === 'settings_live') && ( {(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: 3 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1 }}> <Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1, fontSize: '0.7rem' }}>
{activeTab === 'settings_live' ? 'php.ini (Aktif Konfigürasyon)' : getTemplateName()} {activeTab === 'settings_live' ? 'php.ini (Aktif Konfigürasyon)' : getTemplateName()}
</Typography> </Typography>
<Button <Button
variant="outlined" variant="outlined"
size="large" size="small"
startIcon={isLoadingConfig ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />} startIcon={isLoadingConfig ? <CircularProgress size={16} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem' }} />}
disabled={isLoadingConfig} disabled={isLoadingConfig}
onClick={() => activeTab === 'settings_live' ? saveLiveConfig(serviceId!, liveConfigContent) : saveConfig(getTemplateName(), configContent)} onClick={() => activeTab === 'settings_live' ? saveLiveConfig(serviceId!, liveConfigContent) : saveConfig(getTemplateName(), configContent)}
sx={{ borderRadius: 2.5, px: 4, fontWeight: 800 }} sx={{ borderRadius: 2, px: 2, fontWeight: 800, fontSize: '0.75rem' }}
> >
{t('common.save').toUpperCase()} {t('common.save').toUpperCase()}
</Button> </Button>
</Box> </Box>
<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' }}> <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 ? ( {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>
@@ -13,6 +13,7 @@ interface DashboardManagerProps {
t: any; t: any;
onFetchStatus: () => void; onFetchStatus: () => void;
onSelectProject: (projectId: string) => void; onSelectProject: (projectId: string) => void;
runServiceOperation: (title: string, services: string[], operation: 'start' | 'stop') => void;
} }
export default function DashboardManager({ export default function DashboardManager({
@@ -23,53 +24,20 @@ export default function DashboardManager({
mariaDbVersions, mariaDbVersions,
t, t,
onFetchStatus, onFetchStatus,
onSelectProject onSelectProject,
runServiceOperation
}: DashboardManagerProps) { }: DashboardManagerProps) {
const [selectedServiceId, setSelectedServiceId] = useState<string | null>('nginx'); const [selectedServiceId, setSelectedServiceId] = useState<string | null>('nginx');
const [isToggling, setIsToggling] = useState<string | null>(null);
const [toggledStates, setToggledStates] = useState<Record<string, string>>({});
const handleToggleService = async (service: string) => { const handleToggleService = async (service: string) => {
if (!window.api) return;
if (isToggling) return;
const currentStatus = status[service]?.status || 'stopped'; const currentStatus = status[service]?.status || 'stopped';
const isRunning = currentStatus === 'running'; const isRunning = currentStatus === 'running';
const title = isRunning ? t('common.stopping') : t('common.starting');
const operation = isRunning ? 'stop' : 'start';
setIsToggling(service); runServiceOperation(title, [service], operation);
// Optimistically set transition state if not already there
setToggledStates(prev => ({ ...prev, [service]: isRunning ? 'stopping' : 'starting' }));
try {
const result = isRunning
? await window.api.stopService(service)
: await window.api.startService(service);
if (result && !result.success) {
Swal.fire({
title: t('common.error'),
text: result.message,
icon: 'error',
background: '#1e1e1e',
color: '#fff'
});
}
} finally {
setIsToggling(null);
setToggledStates(prev => {
const updated = { ...prev };
delete updated[service];
return updated;
});
onFetchStatus();
}
}; };
// Merge actual status with our local transition states
const mergedStatus = { ...status };
Object.keys(toggledStates).forEach(service => {
mergedStatus[service] = { ...mergedStatus[service], status: toggledStates[service] };
});
const handleReloadService = async (service: string) => { const handleReloadService = async (service: string) => {
if (!window.api) return; if (!window.api) return;
@@ -89,7 +57,7 @@ export default function DashboardManager({
return ( return (
<Box sx={{ display: 'flex', height: 'calc(100vh - 120px)', bgcolor: 'background.default', overflow: 'hidden', borderRadius: 4, border: '1px solid rgba(255, 255, 255, 0.05)', boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}> <Box sx={{ display: 'flex', height: 'calc(100vh - 120px)', bgcolor: 'background.default', overflow: 'hidden', borderRadius: 4, border: '1px solid rgba(255, 255, 255, 0.05)', boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}>
<DashboardSidebar <DashboardSidebar
status={mergedStatus} status={status}
phpVersions={phpVersions} phpVersions={phpVersions}
mariaDbVersions={mariaDbVersions} mariaDbVersions={mariaDbVersions}
selectedServiceId={selectedServiceId} selectedServiceId={selectedServiceId}
@@ -98,7 +66,7 @@ export default function DashboardManager({
/> />
<DashboardDetails <DashboardDetails
serviceId={selectedServiceId} serviceId={selectedServiceId}
status={mergedStatus} status={status}
settings={settings} settings={settings}
projects={projects} projects={projects}
t={t} t={t}
@@ -190,64 +190,65 @@ const MariaDbDatabaseManager: React.FC<MariaDbDatabaseManagerProps> = ({ version
<DbIcon color="primary" /> <DbIcon color="primary" />
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>{t('mariadb_manager.title')}</Typography> <Typography variant="h6" sx={{ fontWeight: 'bold' }}>{t('mariadb_manager.title')}</Typography>
</Box> </Box>
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={1.5}>
<FormControl size="small" sx={{ minWidth: 200 }}> <FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel id="version-select-label">{t('mariadb.version')}</InputLabel> <InputLabel id="version-select-label" sx={{ fontSize: '0.75rem' }}>{t('mariadb.version')}</InputLabel>
<Select <Select
labelId="version-select-label" labelId="version-select-label"
value={selectedVersion} value={selectedVersion}
label={t('mariadb.version')} label={t('mariadb.version')}
onChange={(e) => setSelectedVersion(e.target.value)} onChange={(e) => setSelectedVersion(e.target.value)}
sx={{ fontSize: '0.75rem', height: 32 }}
> >
{installedVersions.map(v => ( {installedVersions.map(v => (
<MenuItem key={v.id} value={v.id}>MariaDB {v.version}</MenuItem> <MenuItem key={v.id} value={v.id} sx={{ fontSize: '0.75rem' }}>MariaDB {v.version}</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<Button startIcon={<RefreshIcon />} onClick={fetchData} disabled={loading}> <Button variant="outlined" size="small" startIcon={<RefreshIcon sx={{ fontSize: '1rem' }} />} onClick={fetchData} disabled={loading} sx={{ fontSize: '0.7rem', height: 32, borderRadius: 2 }}>
{t('common.refresh')} {t('common.refresh')}
</Button> </Button>
</Stack> </Stack>
</Stack> </Stack>
{!selectedVersion && ( {!selectedVersion && (
<Alert severity="info" sx={{ mt: 2 }}>{t('mariadb_manager.no_active_version')}</Alert> <Alert severity="info" sx={{ mt: 2, fontSize: '0.75rem' }}>{t('mariadb_manager.no_active_version')}</Alert>
)} )}
{selectedVersion && ( {selectedVersion && (
<Stack spacing={4}> <Stack spacing={3}>
{/* Databases Section */} {/* Databases Section */}
<Box> <Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'primary.light' }}> <Typography variant="subtitle2" sx={{ fontWeight: 800, color: 'primary.light', fontSize: '0.8rem' }}>
{t('mariadb_manager.databases_title')} {t('mariadb_manager.databases_title')}
</Typography> </Typography>
<Button variant="outlined" size="small" startIcon={<AddIcon />} onClick={handleCreateDb}> <Button variant="outlined" size="small" startIcon={<AddIcon sx={{ fontSize: '1rem' }} />} onClick={handleCreateDb} sx={{ fontSize: '0.7rem', height: 28, borderRadius: 1.5 }}>
{t('mariadb_manager.create_db_btn')} {t('mariadb_manager.create_db_btn')}
</Button> </Button>
</Box> </Box>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 400 }}> <TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 300, borderRadius: 2 }}>
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.db_name')}</TableCell> <TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>{t('mariadb_manager.db_name')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.db_size')}</TableCell> <TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>{t('mariadb_manager.db_size')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold', textAlign: 'right' }}>{t('common.actions')}</TableCell> <TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1, textAlign: 'right' }}>{t('common.actions')}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={24} sx={{ my: 2 }} /></TableCell></TableRow> <TableRow><TableCell colSpan={3} align="center"><CircularProgress size={20} sx={{ my: 1 }} /></TableCell></TableRow>
) : databases.length === 0 ? ( ) : databases.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 3, color: 'text.secondary' }}>{t('common.no_data')}</TableCell></TableRow> <TableRow><TableCell colSpan={3} align="center" sx={{ py: 2, color: 'text.secondary', fontSize: '0.7rem' }}>{t('common.no_data')}</TableCell></TableRow>
) : databases.map(db => ( ) : databases.map(db => (
<TableRow key={db.name}> <TableRow key={db.name}>
<TableCell sx={{ fontWeight: 'bold' }}>{db.name}</TableCell> <TableCell sx={{ fontWeight: 600, fontSize: '0.75rem' }}>{db.name}</TableCell>
<TableCell>{db.size.toFixed(2)} MB</TableCell> <TableCell sx={{ fontSize: '0.7rem', opacity: 0.8 }}>{db.size.toFixed(2)} MB</TableCell>
<TableCell align="right"> <TableCell align="right" sx={{ py: 0.5 }}>
<Tooltip title={t('common.delete')}> <Tooltip title={t('common.delete')}>
<IconButton color="error" size="small" onClick={() => handleDeleteDb(db.name)}> <IconButton color="error" size="small" onClick={() => handleDeleteDb(db.name)} sx={{ p: 0.5 }}>
<DeleteIcon fontSize="small" /> <DeleteIcon sx={{ fontSize: '1rem' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
@@ -258,50 +259,50 @@ const MariaDbDatabaseManager: React.FC<MariaDbDatabaseManagerProps> = ({ version
</TableContainer> </TableContainer>
</Box> </Box>
<Divider sx={{ opacity: 0.1 }} /> <Divider sx={{ opacity: 0.05 }} />
{/* Users Section */} {/* Users Section */}
<Box> <Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'secondary.light' }}> <Typography variant="subtitle2" sx={{ fontWeight: 800, color: 'secondary.light', fontSize: '0.8rem' }}>
{t('mariadb_manager.users_title')} {t('mariadb_manager.users_title')}
</Typography> </Typography>
<Button variant="outlined" color="secondary" size="small" startIcon={<AddIcon />} onClick={handleCreateUser}> <Button variant="outlined" color="secondary" size="small" startIcon={<AddIcon sx={{ fontSize: '1rem' }} />} onClick={handleCreateUser} sx={{ fontSize: '0.7rem', height: 28, borderRadius: 1.5 }}>
{t('mariadb_manager.create_user_btn')} {t('mariadb_manager.create_user_btn')}
</Button> </Button>
</Box> </Box>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 400 }}> <TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 300, borderRadius: 2 }}>
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.user_label')}</TableCell> <TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>{t('mariadb_manager.user_label')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>Host</TableCell> <TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>Host</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold', textAlign: 'right' }}>{t('common.actions')}</TableCell> <TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1, textAlign: 'right' }}>{t('common.actions')}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={24} sx={{ my: 2 }} /></TableCell></TableRow> <TableRow><TableCell colSpan={3} align="center"><CircularProgress size={20} sx={{ my: 1 }} /></TableCell></TableRow>
) : users.length === 0 ? ( ) : users.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 3, color: 'text.secondary' }}>{t('common.no_data')}</TableCell></TableRow> <TableRow><TableCell colSpan={3} align="center" sx={{ py: 2, color: 'text.secondary', fontSize: '0.7rem' }}>{t('common.no_data')}</TableCell></TableRow>
) : users.map(user => ( ) : users.map(user => (
<TableRow key={`${user.user}-${user.host}`}> <TableRow key={`${user.user}-${user.host}`}>
<TableCell sx={{ fontWeight: 'bold' }}> <TableCell sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<UserIcon fontSize="inherit" sx={{ opacity: 0.5 }} /> <UserIcon sx={{ opacity: 0.5, fontSize: '0.9rem' }} />
{user.user} {user.user}
</Box> </Box>
</TableCell> </TableCell>
<TableCell> <TableCell sx={{ fontSize: '0.7rem', opacity: 0.8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<HostIcon fontSize="inherit" sx={{ opacity: 0.5 }} /> <HostIcon sx={{ opacity: 0.5, fontSize: '0.9rem' }} />
{user.host} {user.host}
</Box> </Box>
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right" sx={{ py: 0.5 }}>
<Tooltip title={t('common.delete')}> <Tooltip title={t('common.delete')}>
<IconButton color="error" size="small" onClick={() => handleDeleteUser(user.user, user.host)}> <IconButton color="error" size="small" onClick={() => handleDeleteUser(user.user, user.host)} sx={{ p: 0.5 }}>
<DeleteIcon fontSize="small" /> <DeleteIcon sx={{ fontSize: '1rem' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
@@ -311,6 +312,7 @@ const MariaDbDatabaseManager: React.FC<MariaDbDatabaseManagerProps> = ({ version
</Table> </Table>
</TableContainer> </TableContainer>
</Box> </Box>
</Stack> </Stack>
)} )}
</Paper> </Paper>
@@ -178,17 +178,17 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
} }
}} }}
> >
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <DialogTitle sx={{ m: 0, p: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box> <Box>
<Typography variant="h6">{t('php.extensions.title', { version: phpVersion })}</Typography> <Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t('php.extensions.title', { version: phpVersion })}</Typography>
<Typography variant="caption" color="text.secondary">{t('php.extensions.desc')}</Typography> <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem' }}>{t('php.extensions.desc')}</Typography>
</Box> </Box>
<IconButton onClick={onClose} sx={{ color: 'text.secondary' }}> <IconButton onClick={onClose} size="small" sx={{ color: 'text.secondary' }}>
<CloseIcon /> <CloseIcon fontSize="small" />
</IconButton> </IconButton>
</DialogTitle> </DialogTitle>
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.1)', p: 0 }}> <DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.05)', p: 0 }}>
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.03)', display: 'flex', gap: 1 }}> <Box sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.02)', display: 'flex', gap: 1 }}>
<TextField <TextField
fullWidth fullWidth
size="small" size="small"
@@ -198,33 +198,34 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<SearchIcon sx={{ color: 'text.secondary' }} /> <SearchIcon sx={{ color: 'text.secondary', fontSize: '1rem' }} />
</InputAdornment> </InputAdornment>
), ),
sx: { color: '#fff', bgcolor: 'rgba(0,0,0,0.2)' } sx: { color: '#fff', bgcolor: 'rgba(0,0,0,0.2)', fontSize: '0.75rem', height: 32 }
}} }}
/> />
<Button <Button
variant="outlined" variant="outlined"
startIcon={<RawIcon />} size="small"
startIcon={<RawIcon sx={{ fontSize: '1rem' }} />}
onClick={fetchRawConfig} onClick={fetchRawConfig}
sx={{ whiteSpace: 'nowrap', borderColor: 'rgba(255,255,255,0.2)', color: '#fff' }} sx={{ whiteSpace: 'nowrap', borderColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.7)', fontSize: '0.7rem', height: 32, borderRadius: 1.5 }}
> >
{t('php.extensions.raw_editor')} {t('php.extensions.raw_editor')}
</Button> </Button>
</Box> </Box>
{loading ? ( {loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress /> <CircularProgress size={24} />
</Box> </Box>
) : ( ) : (
<List sx={{ maxHeight: 400, overflow: 'auto' }}> <List sx={{ maxHeight: 350, overflow: 'auto' }} dense>
{filteredExtensions.length === 0 ? ( {filteredExtensions.length === 0 ? (
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={searchTerm ? t('php.extensions.not_found') : t('php.extensions.none')} primary={searchTerm ? t('php.extensions.not_found') : t('php.extensions.none')}
sx={{ textAlign: 'center', color: 'text.secondary' }} primaryTypographyProps={{ sx: { textAlign: 'center', color: 'text.secondary', fontSize: '0.75rem' } }}
/> />
</ListItem> </ListItem>
) : ( ) : (
@@ -232,14 +233,15 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
<ListItem <ListItem
key={ext.name} key={ext.name}
divider divider
sx={{ borderColor: 'rgba(255,255,255,0.05)' }} sx={{ borderColor: 'rgba(255,255,255,0.03)', py: 0.5 }}
secondaryAction={ secondaryAction={
saving === ext.name ? ( saving === ext.name ? (
<CircularProgress size={24} /> <CircularProgress size={16} />
) : ( ) : (
<Switch <Switch
edge="end" edge="end"
checked={ext.enabled} checked={ext.enabled}
size="small"
onChange={(e) => handleToggle(ext.name, e.target.checked)} onChange={(e) => handleToggle(ext.name, e.target.checked)}
color="primary" color="primary"
/> />
@@ -248,13 +250,13 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
> >
<ListItemText <ListItemText
primary={ primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.75rem', fontWeight: 600 }}>
{ext.name} {ext.name}
{ext.enabled && <Chip label={t('php.extensions.active')} size="small" color="success" sx={{ height: 18, fontSize: '0.65rem' }} />} {ext.enabled && <Chip label={t('php.extensions.active')} size="small" color="success" sx={{ height: 16, fontSize: '0.55rem', fontWeight: 900 }} />}
</Box> </Box>
} }
secondary={ext.dll} secondary={ext.dll}
secondaryTypographyProps={{ sx: { color: 'text.secondary', fontSize: '0.75rem' } }} secondaryTypographyProps={{ sx: { color: 'text.secondary', fontSize: '0.65rem', opacity: 0.5 } }}
/> />
</ListItem> </ListItem>
)) ))
@@ -262,11 +264,11 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
</List> </List>
)} )}
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)' }}> <DialogActions sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Button onClick={fetchExtensions} startIcon={<RefreshIcon />} sx={{ mr: 'auto' }}> <Button onClick={fetchExtensions} size="small" startIcon={<RefreshIcon sx={{ fontSize: '0.9rem' }} />} sx={{ mr: 'auto', fontSize: '0.7rem' }}>
{t('common.refresh').toUpperCase()} {t('common.refresh').toUpperCase()}
</Button> </Button>
<Button onClick={onClose} variant="outlined" color="primary"> <Button onClick={onClose} variant="outlined" size="small" sx={{ fontSize: '0.7rem', borderRadius: 1.5 }}>
{t('common.close').toUpperCase()} {t('common.close').toUpperCase()}
</Button> </Button>
</DialogActions> </DialogActions>
@@ -282,14 +284,16 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
bgcolor: '#1a1a1a', bgcolor: '#1a1a1a',
color: '#fff', color: '#fff',
borderRadius: 2, borderRadius: 2,
backgroundImage: 'none',
border: '1px solid rgba(255,255,255,0.1)'
} }
}} }}
> >
<DialogTitle sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 2 }}> <DialogTitle sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">{t('php.extensions.ini_edit', { version: phpVersion })}</Typography> <Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t('php.extensions.ini_edit', { version: phpVersion })}</Typography>
<IconButton onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}> <IconButton onClick={() => setIsRawEditorOpen(false)} size="small" sx={{ color: 'rgba(255,255,255,0.5)' }}>
<CloseIcon /> <CloseIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
<Typography <Typography
@@ -298,6 +302,8 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
color: 'primary.main', color: 'primary.main',
wordBreak: 'break-all', wordBreak: 'break-all',
cursor: 'pointer', cursor: 'pointer',
fontSize: '0.6rem',
opacity: 0.7,
'&:hover': { textDecoration: 'underline', color: 'primary.light' } '&:hover': { textDecoration: 'underline', color: 'primary.light' }
}} }}
onClick={() => window.api.invoke('shell:open-path', configPath)} onClick={() => window.api.invoke('shell:open-path', configPath)}
@@ -306,7 +312,7 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
{configPath} {configPath}
</Typography> </Typography>
</DialogTitle> </DialogTitle>
<Box sx={{ px: 2, pb: 2, bgcolor: '#1a1a1a' }}> <Box sx={{ px: 1.5, pb: 1.5, bgcolor: '#1a1a1a' }}>
<TextField <TextField
fullWidth fullWidth
size="small" size="small"
@@ -316,37 +322,42 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<SearchIcon sx={{ color: 'text.secondary', fontSize: '1.2rem' }} /> <SearchIcon sx={{ color: 'text.secondary', fontSize: '1rem' }} />
</InputAdornment> </InputAdornment>
), ),
sx: { sx: {
color: '#fff', color: '#fff',
bgcolor: 'rgba(255,255,255,0.05)', bgcolor: 'rgba(255,255,255,0.03)',
'& fieldset': { borderColor: 'rgba(255,255,255,0.1)' } fontSize: '0.75rem',
height: 32,
'& fieldset': { borderColor: 'rgba(255,255,255,0.05)' }
} }
}} }}
/> />
</Box> </Box>
<DialogContent sx={{ p: 0, minHeight: 400, borderTop: '1px solid rgba(255,255,255,0.1)' }}> <DialogContent sx={{ p: 0, minHeight: 350, borderTop: '1px solid rgba(255,255,255,0.05)' }}>
<PhpIniCodeEditor <PhpIniCodeEditor
value={rawConfig} value={rawConfig}
onChange={setRawConfig} onChange={setRawConfig}
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 2, bgcolor: 'rgba(0,0,0,0.2)' }}> <DialogActions sx={{ p: 1.5, bgcolor: 'rgba(0,0,0,0.1)' }}>
<Button onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}> <Button onClick={() => setIsRawEditorOpen(false)} size="small" sx={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.7rem' }}>
{t('common.cancel').toUpperCase()} {t('common.cancel').toUpperCase()}
</Button> </Button>
<Button <Button
onClick={handleSaveRaw} onClick={handleSaveRaw}
variant="outlined" variant="outlined"
color="primary" color="primary"
startIcon={isSavingRaw ? <CircularProgress size={18} color="inherit" /> : <SaveIcon />} size="small"
startIcon={isSavingRaw ? <CircularProgress size={14} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem' }} />}
disabled={isSavingRaw} disabled={isSavingRaw}
sx={{ fontSize: '0.7rem', borderRadius: 1.5, px: 2 }}
> >
{isSavingRaw ? t('php.extensions.saving_btn') : t('php.extensions.save_restart_btn')} {isSavingRaw ? t('php.extensions.saving_btn') : t('php.extensions.save_restart_btn')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</> </>
) )
@@ -57,6 +57,7 @@ interface ProjectDetailsProps {
onOpenVSCode: () => void; onOpenVSCode: () => void;
onManageDb: () => void; onManageDb: () => void;
onAddHost: (hostname: string) => void; onAddHost: (hostname: string) => void;
onRefreshProjects: () => void;
isTransitioning?: boolean; isTransitioning?: boolean;
setTransitioning?: (v: boolean) => void; setTransitioning?: (v: boolean) => void;
} }
@@ -77,6 +78,7 @@ export default function ProjectDetails({
onOpenVSCode, onOpenVSCode,
onManageDb, onManageDb,
onAddHost, onAddHost,
onRefreshProjects,
isTransitioning = false, isTransitioning = false,
setTransitioning setTransitioning
}: ProjectDetailsProps) { }: ProjectDetailsProps) {
@@ -152,6 +154,7 @@ export default function ProjectDetails({
background: '#1e1e1e', background: '#1e1e1e',
color: '#fff' color: '#fff'
}); });
onRefreshProjects();
} }
} catch (e: any) { } catch (e: any) {
Swal.fire({ Swal.fire({
@@ -187,6 +190,7 @@ export default function ProjectDetails({
background: '#1e1e1e', background: '#1e1e1e',
color: '#fff' color: '#fff'
}); });
if (onRefreshProjects) onRefreshProjects();
} }
} catch (error: any) { } catch (error: any) {
Swal.fire({ Swal.fire({
@@ -243,9 +247,9 @@ export default function ProjectDetails({
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
<Button <Button
variant="outlined" variant="outlined"
size="medium" size="small"
disabled={isTransitioning} disabled={isTransitioning}
startIcon={isTransitioning ? <CircularProgress size={18} color="inherit" /> : (isRunning ? <StopIcon /> : <StartIcon />)} startIcon={isTransitioning ? <CircularProgress size={16} color="inherit" /> : (isRunning ? <StopIcon sx={{ fontSize: '1rem !important' }} /> : <StartIcon sx={{ fontSize: '1rem !important' }} />)}
color={isRunning ? 'error' : 'success'} color={isRunning ? 'error' : 'success'}
onClick={async () => { onClick={async () => {
if (setTransitioning) setTransitioning(true); if (setTransitioning) setTransitioning(true);
@@ -257,27 +261,27 @@ export default function ProjectDetails({
} }
} }
}} }}
sx={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 800, boxShadow: 'none' }} sx={{ px: 2, height: 32, borderRadius: 1.5, fontWeight: 700, fontSize: '0.75rem', boxShadow: 'none' }}
> >
{isTransitioning ? t('common.processing') : (isRunning ? t('projects.stop_site').toUpperCase() : t('projects.start_site').toUpperCase())} {isTransitioning ? t('common.processing') : (isRunning ? t('projects.stop_site').toUpperCase() : t('projects.start_site').toUpperCase())}
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
size="medium" size="small"
startIcon={<LaunchIcon />} startIcon={<LaunchIcon sx={{ fontSize: '1rem !important' }} />}
onClick={() => onOpenSite(isHostMapped)} onClick={() => onOpenSite(isHostMapped)}
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)' } }} sx={{ px: 2, height: 32, borderRadius: 1.5, fontWeight: 600, fontSize: '0.75rem', borderColor: 'rgba(255,255,255,0.1)', '&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(57, 167, 255, 0.05)' } }}
> >
{t('projects.open_site').toUpperCase()} {t('projects.open_site').toUpperCase()}
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
size="medium" size="small"
startIcon={<CodeIcon />} startIcon={<CodeIcon sx={{ fontSize: '1rem !important' }} />}
onClick={onOpenVSCode} onClick={onOpenVSCode}
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)' } }} sx={{ px: 2, height: 32, borderRadius: 1.5, borderColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.6)', fontWeight: 600, fontSize: '0.75rem', '&:hover': { borderColor: '#007ACC', color: '#007ACC', bgcolor: 'rgba(0, 122, 204, 0.05)' } }}
> >
VS CODE VS CODE
</Button> </Button>
@@ -285,13 +289,13 @@ export default function ProjectDetails({
{!isHostMapped && ( {!isHostMapped && (
<Button <Button
variant="outlined" variant="outlined"
size="medium" size="small"
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={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 800, boxShadow: 'none' }} sx={{ px: 2, height: 32, borderRadius: 1.5, fontWeight: 700, fontSize: '0.75rem', boxShadow: 'none' }}
> >
HOSTS DOSYASINA EKLE HOSTS DOSYASINA EKLE
</Button> </Button>
@@ -305,17 +309,17 @@ export default function ProjectDetails({
<IconButton <IconButton
onClick={onDelete} onClick={onDelete}
sx={{ sx={{
width: 48, width: 40,
height: 48, height: 40,
bgcolor: 'rgba(255, 68, 68, 0.05)', bgcolor: 'rgba(255, 68, 68, 0.05)',
color: 'error.main', color: 'error.main',
border: '1px solid rgba(255, 68, 68, 0.1)', border: '1px solid rgba(255, 68, 68, 0.1)',
borderRadius: 3, borderRadius: 1.5,
'&:hover': { bgcolor: 'error.main', color: '#fff', transform: 'scale(1.05) rotate(5deg)', boxShadow: '0 4px 12px rgba(255, 68, 68, 0.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' transition: 'all 0.2s'
}} }}
> >
<DeleteIcon /> <DeleteIcon sx={{ fontSize: '1.25rem' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
@@ -324,9 +328,9 @@ export default function ProjectDetails({
{/* Tabs */} {/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 4, bgcolor: 'rgba(255,255,255,0.01)' }}> <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')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} /> <Tab label={t('projects.overview')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120, fontSize: '0.8rem' }} />
<Tab label={t('projects.database')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} /> <Tab label={t('projects.database')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120, fontSize: '0.8rem' }} />
<Tab label={t('projects.server_settings')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} /> <Tab label={t('projects.server_settings')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120, fontSize: '0.8rem' }} />
</Tabs> </Tabs>
</Box> </Box>
@@ -338,8 +342,8 @@ export default function ProjectDetails({
{/* Editable Form Side */} {/* Editable Form Side */}
<Grid item xs={12} md={7}> <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)' }}> <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 }}> <Typography variant="h6" sx={{ mb: 3, fontWeight: 800, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5, fontSize: '1rem' }}>
<SettingsIcon /> {t('projects.edit_details', 'Proje Detaylarını Düzenle')} <SettingsIcon sx={{ fontSize: '1.25rem' }} /> {t('projects.edit_details', 'Proje Detaylarını Düzenle')}
</Typography> </Typography>
<Stack spacing={3}> <Stack spacing={3}>
@@ -347,60 +351,62 @@ export default function ProjectDetails({
label={t('projects.site_name')} label={t('projects.site_name')}
fullWidth fullWidth
variant="filled" variant="filled"
size="small"
value={editForm.name} value={editForm.name}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })} onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }} sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, '& .MuiInputLabel-root': { fontSize: '0.85rem' }, '& .MuiInputBase-input': { fontSize: '0.85rem' } }}
/> />
<TextField <TextField
label={t('projects.site_domain')} label={t('projects.site_domain')}
fullWidth fullWidth
variant="filled" variant="filled"
size="small"
value={editForm.host} value={editForm.host}
onChange={(e) => setEditForm({ ...editForm, host: e.target.value })} onChange={(e) => setEditForm({ ...editForm, host: e.target.value })}
helperText={t('projects.host_helper', 'Domain adı (örn: myproject)')} helperText={t('projects.host_helper', 'Domain adı (örn: myproject)')}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }} sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, '& .MuiInputLabel-root': { fontSize: '0.85rem' }, '& .MuiInputBase-input': { fontSize: '0.85rem' }, '& .MuiFormHelperText-root': { fontSize: '0.7rem' } }}
/> />
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={6}> <Grid item xs={6}>
<FormControl fullWidth variant="filled"> <FormControl fullWidth variant="filled" size="small">
<InputLabel>{t('projects.web_server')}</InputLabel> <InputLabel sx={{ fontSize: '0.85rem' }}>{t('projects.web_server')}</InputLabel>
<Select <Select
value={editForm.serverType} value={editForm.serverType}
onChange={(e) => setEditForm({ ...editForm, serverType: e.target.value })} onChange={(e) => setEditForm({ ...editForm, serverType: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }} sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, fontSize: '0.85rem' }}
> >
<MenuItem value="nginx">Nginx</MenuItem> <MenuItem value="nginx" sx={{ fontSize: '0.85rem' }}>Nginx</MenuItem>
<MenuItem value="apache">Apache</MenuItem> <MenuItem value="apache" sx={{ fontSize: '0.85rem' }}>Apache</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<FormControl fullWidth variant="filled"> <FormControl fullWidth variant="filled" size="small">
<InputLabel>{t('projects.php_version')}</InputLabel> <InputLabel sx={{ fontSize: '0.85rem' }}>{t('projects.php_version')}</InputLabel>
<Select <Select
value={editForm.phpVersion} value={editForm.phpVersion}
onChange={(e) => setEditForm({ ...editForm, phpVersion: e.target.value })} onChange={(e) => setEditForm({ ...editForm, phpVersion: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }} sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, fontSize: '0.85rem' }}
> >
{installedPhp.map(v => ( {installedPhp.map(v => (
<MenuItem key={v.version} value={v.version}>PHP {v.version}</MenuItem> <MenuItem key={v.version} value={v.version} sx={{ fontSize: '0.85rem' }}>PHP {v.version}</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>
</Grid> </Grid>
<FormControl fullWidth variant="filled"> <FormControl fullWidth variant="filled" size="small">
<InputLabel>{t('projects.mariadb_version')}</InputLabel> <InputLabel sx={{ fontSize: '0.85rem' }}>{t('projects.mariadb_version')}</InputLabel>
<Select <Select
value={editForm.mariaDbVersion} value={editForm.mariaDbVersion}
onChange={(e) => setEditForm({ ...editForm, mariaDbVersion: e.target.value })} onChange={(e) => setEditForm({ ...editForm, mariaDbVersion: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }} sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, fontSize: '0.85rem' }}
> >
{installedMariaDb.map(v => ( {installedMariaDb.map(v => (
<MenuItem key={v.version} value={v.version}>MariaDB {v.version}</MenuItem> <MenuItem key={v.version} value={v.version} sx={{ fontSize: '0.85rem' }}>MariaDB {v.version}</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
@@ -408,11 +414,11 @@ export default function ProjectDetails({
<Box sx={{ pt: 1, display: 'flex', justifyContent: 'flex-start' }}> <Box sx={{ pt: 1, display: 'flex', justifyContent: 'flex-start' }}>
<Button <Button
variant="outlined" variant="outlined"
size="large" size="small"
startIcon={isSaving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />} startIcon={isSaving ? <CircularProgress size={16} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem !important' }} />}
onClick={handleProjectUpdate} onClick={handleProjectUpdate}
disabled={isSaving} disabled={isSaving}
sx={{ px: 6, py: 1.5, borderRadius: 3, fontWeight: 800, fontSize: '1rem', color: 'primary.main', border: '1px solid rgba(33, 150, 243, 0.5)', '&:hover': { bgcolor: 'rgba(33, 150, 243, 0.05)', borderColor: 'primary.main' } }} sx={{ px: 4, py: 1, borderRadius: 2, fontWeight: 700, fontSize: '0.75rem', color: 'primary.main', border: '1px solid rgba(33, 150, 243, 0.5)', '&:hover': { bgcolor: 'rgba(33, 150, 243, 0.05)', borderColor: 'primary.main' } }}
> >
{t('common.save').toUpperCase()} {t('common.save').toUpperCase()}
</Button> </Button>
@@ -424,8 +430,8 @@ export default function ProjectDetails({
{/* Status Monitor Side */} {/* Status Monitor Side */}
<Grid item xs={12} md={5}> <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%' }}> <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)' }}> <Typography variant="subtitle1" sx={{ mb: 4, fontWeight: 800, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.6)', fontSize: '0.9rem' }}>
<StatusIcon sx={{ color: 'secondary.main' }} /> {t('dashboard.status')} <StatusIcon sx={{ color: 'secondary.main', fontSize: '1.25rem' }} /> {t('dashboard.status')}
</Typography> </Typography>
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
@@ -439,10 +445,10 @@ export default function ProjectDetails({
<Divider sx={{ mb: 3, opacity: 0.1 }} /> <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)' }}> <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 }}> <Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 800, display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, textTransform: 'uppercase', letterSpacing: 1, fontSize: '0.7rem' }}>
<CodeIcon fontSize="small" /> System Info <CodeIcon fontSize="small" /> System Info
</Typography> </Typography>
<Typography variant="body2" sx={{ opacity: 0.5, lineHeight: 1.6 }}> <Typography variant="body2" sx={{ opacity: 0.5, lineHeight: 1.6, fontSize: '0.75rem' }}>
{t('projects.manual_edit_help', 'Bu sayfadaki değişiklikler kaydedildiğinde servis yapılandırmaları ve hosts dosyası otomatik olarak yenilenecektir.')} {t('projects.manual_edit_help', 'Bu sayfadaki değişiklikler kaydedildiğinde servis yapılandırmaları ve hosts dosyası otomatik olarak yenilenecektir.')}
</Typography> </Typography>
</Box> </Box>
@@ -458,22 +464,22 @@ export default function ProjectDetails({
<Paper sx={{ p: 4, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}> <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', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'secondary.main' }}> <Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'secondary.main', width: 48, height: 48 }}>
<DbIcon /> <DbIcon />
</Avatar> </Avatar>
<Box> <Box>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>{t('projects.mariadb_version')}</Typography> <Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1, fontSize: '0.65rem' }}>{t('projects.mariadb_version')}</Typography>
<Typography variant="h6" sx={{ fontWeight: 700 }}>MariaDB {p.mariadbVersion}</Typography> <Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>MariaDB {p.mariaDbVersion}</Typography>
</Box> </Box>
</Box> </Box>
<Button <Button
variant="outlined" variant="outlined"
size="medium" size="small"
startIcon={<OpenIcon />} startIcon={<OpenIcon sx={{ fontSize: '1rem !important' }} />}
onClick={onManageDb} onClick={onManageDb}
sx={{ borderRadius: 2.5, px: 3, height: 44, fontWeight: 700 }} sx={{ borderRadius: 1.5, px: 2, height: 32, fontWeight: 700, fontSize: '0.75rem' }}
> >
{t('mariadb_manager.title')} {t('mariadb_manager.title').toUpperCase()}
</Button> </Button>
</Box> </Box>
@@ -487,8 +493,8 @@ export default function ProjectDetails({
{ label: 'Password', value: '(no password)', italic: true } { label: 'Password', value: '(no password)', italic: true }
].map((item, idx) => ( ].map((item, idx) => (
<Grid item xs={6} key={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="caption" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700, display: 'block', mb: 0.5, fontSize: '0.65rem' }}>{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' }}> <Typography variant="body1" sx={{ fontWeight: 600, color: item.italic ? 'rgba(255,255,255,0.3)' : '#fff', fontStyle: item.italic ? 'italic' : 'normal', fontSize: '0.85rem' }}>
{item.value} {item.value}
</Typography> </Typography>
</Grid> </Grid>
@@ -501,19 +507,19 @@ export default function ProjectDetails({
{projectDetailTab === 2 && ( {projectDetailTab === 2 && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<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={{ 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={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<ServerIcon color="primary" /> <ServerIcon color="primary" sx={{ fontSize: '1.25rem' }} />
<Typography variant="subtitle2" sx={{ fontWeight: 800, textTransform: 'uppercase', letterSpacing: 1 }}> <Typography variant="subtitle2" sx={{ fontWeight: 800, textTransform: 'uppercase', letterSpacing: 1, fontSize: '0.75rem' }}>
{p.serverType} {t('projects.server_settings')} {p.serverType} {t('projects.server_settings')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1.5 }}> <Box sx={{ display: 'flex', gap: 1.5 }}>
<Button <Button
size="small" size="small"
startIcon={<TuningIcon />} startIcon={<TuningIcon sx={{ fontSize: '1rem !important' }} />}
onClick={loadConfig} onClick={loadConfig}
sx={{ color: 'rgba(255,255,255,0.5)', borderRadius: 2 }} sx={{ color: 'rgba(255,255,255,0.5)', borderRadius: 1.5, fontSize: '0.75rem' }}
> >
{t('common.refresh')} {t('common.refresh')}
</Button> </Button>
@@ -521,24 +527,24 @@ export default function ProjectDetails({
variant="outlined" variant="outlined"
size="small" size="small"
color="primary" color="primary"
startIcon={isConfigSaving ? <CircularProgress size={16} color="inherit" /> : <SaveIcon />} startIcon={isConfigSaving ? <CircularProgress size={14} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem !important' }} />}
disabled={isConfigSaving || isConfigLoading} disabled={isConfigSaving || isConfigLoading}
onClick={handleSaveConfig} onClick={handleSaveConfig}
sx={{ px: 4, borderRadius: 2, fontWeight: 800 }} sx={{ px: 3, height: 32, borderRadius: 1.5, fontWeight: 800, fontSize: '0.75rem' }}
> >
{t('common.save').toUpperCase()} {t('common.save').toUpperCase()}
</Button> </Button>
</Box> </Box>
</Box> </Box>
<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 }}> <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)', fontWeight: 600, '& .MuiAlert-message': { fontSize: '0.75rem' } }}>
{t('projects.manual_edit_warning')} {t('projects.manual_edit_warning')}
</Alert> </Alert>
<Box sx={{ flexGrow: 1, position: 'relative', overflow: 'hidden' }}> <Box sx={{ flexGrow: 1, position: 'relative', overflow: 'hidden' }}>
{isConfigLoading ? ( {isConfigLoading ? (
<Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'rgba(0,0,0,0.5)', zIndex: 10 }}> <Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'rgba(0,0,0,0.5)', zIndex: 10 }}>
<CircularProgress /> <CircularProgress size={32} />
</Box> </Box>
) : null} ) : null}
<ProjectConfigEditor <ProjectConfigEditor
@@ -24,6 +24,7 @@ interface ProjectManagerProps {
onOpenVSCode: (path: string) => void; onOpenVSCode: (path: string) => void;
onAddHost: (hostname: string) => void; onAddHost: (hostname: string) => void;
onManageDb: () => void; onManageDb: () => void;
onRefreshProjects: () => void;
} }
export default function ProjectManager({ export default function ProjectManager({
@@ -47,7 +48,8 @@ export default function ProjectManager({
onOpenFolder, onOpenFolder,
onOpenVSCode, onOpenVSCode,
onAddHost, onAddHost,
onManageDb onManageDb,
onRefreshProjects
}: ProjectManagerProps) { }: ProjectManagerProps) {
// Defensive check: find selected project // Defensive check: find selected project
@@ -89,6 +91,7 @@ export default function ProjectManager({
onOpenVSCode={() => onOpenVSCode(p.path)} onOpenVSCode={() => onOpenVSCode(p.path)}
onAddHost={onAddHost} onAddHost={onAddHost}
onManageDb={onManageDb} onManageDb={onManageDb}
onRefreshProjects={onRefreshProjects}
/> />
</Box> </Box>
); );
+2
View File
@@ -60,6 +60,8 @@
"quick_look": "Quick Look", "quick_look": "Quick Look",
"start_all": "Start All", "start_all": "Start All",
"stop_all": "Stop All", "stop_all": "Stop All",
"starting_all": "Starting All Services",
"stopping_all": "Stopping All Services",
"all_started": "All services started.", "all_started": "All services started.",
"all_stopped": "All services stopped.", "all_stopped": "All services stopped.",
"overview": "Overview", "overview": "Overview",
+2
View File
@@ -61,6 +61,8 @@
"quick_look": "Hızlı Bakış", "quick_look": "Hızlı Bakış",
"start_all": "Tümünü Başlat", "start_all": "Tümünü Başlat",
"stop_all": "Tümünü Durdur", "stop_all": "Tümünü Durdur",
"starting_all": "Tüm Servisler Başlatılıyor",
"stopping_all": "Tüm Servisler Durduruluyor",
"all_started": "Tüm servisler başlatıldı.", "all_started": "Tüm servisler başlatıldı.",
"all_stopped": "Tüm servisler durduruldu.", "all_stopped": "Tüm servisler durduruldu.",
"overview": "Genel Bakış", "overview": "Genel Bakış",