feat: implement project management system with IPC services and multi-language support
This commit is contained in:
@@ -1,4 +1,351 @@
|
||||
[
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "dddyqabhb",
|
||||
"timestamp": 1775152237278
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "5clhuwgxn",
|
||||
"timestamp": 1775152236025
|
||||
},
|
||||
{
|
||||
"service": "php:5.6.40",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "3f3i7nrzx",
|
||||
"timestamp": 1775151972287
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "cc098qksj",
|
||||
"timestamp": 1775151972268
|
||||
},
|
||||
{
|
||||
"service": "php:8.2.30",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "6814jwso7",
|
||||
"timestamp": 1775151972248
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "e32wp6fdz",
|
||||
"timestamp": 1775151972229
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "6ktzrdwkw",
|
||||
"timestamp": 1775151971884
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "5dcg1ko1i",
|
||||
"timestamp": 1775151971865
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "u8jthf8zj",
|
||||
"timestamp": 1775151971244
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "xlp8uhm2c",
|
||||
"timestamp": 1775151971178
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "ug698rcco",
|
||||
"timestamp": 1775151837765
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "8lvo9sxz7",
|
||||
"timestamp": 1775151836521
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "a9vfbzxn2",
|
||||
"timestamp": 1775151829018
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "ze54njict",
|
||||
"timestamp": 1775151829005
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: null",
|
||||
"id": "uaer37d6w",
|
||||
"timestamp": 1775151828682
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "stop",
|
||||
"status": "info",
|
||||
"id": "5or3hh6p5",
|
||||
"timestamp": 1775151828671
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: null",
|
||||
"id": "yzmng3nm3",
|
||||
"timestamp": 1775151828345
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "stop",
|
||||
"status": "info",
|
||||
"id": "5dj60xh68",
|
||||
"timestamp": 1775151828339
|
||||
},
|
||||
{
|
||||
"service": "php:5.6.40",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "taw24xi9n",
|
||||
"timestamp": 1775151786087
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "d7cx0kco1",
|
||||
"timestamp": 1775151786070
|
||||
},
|
||||
{
|
||||
"service": "php:8.2.30",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "bren2nk3u",
|
||||
"timestamp": 1775151786050
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "ubmufryso",
|
||||
"timestamp": 1775151786033
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "bpbyftdye",
|
||||
"timestamp": 1775151785673
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "cc6cp0j39",
|
||||
"timestamp": 1775151785653
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "ta5nm4x9y",
|
||||
"timestamp": 1775151785048
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "mtg8rhrxw",
|
||||
"timestamp": 1775151784985
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "8n8vb3sst",
|
||||
"timestamp": 1775151495788
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "u5vjd12e2",
|
||||
"timestamp": 1775151494502
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "5wwt7m3au",
|
||||
"timestamp": 1775151347535
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "hebcatt1d",
|
||||
"timestamp": 1775151346107
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "njpd47glj",
|
||||
"timestamp": 1775151316169
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "4n9uj134g",
|
||||
"timestamp": 1775151314774
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "isd6s11sk",
|
||||
"timestamp": 1775151306664
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "qlpij4nl8",
|
||||
"timestamp": 1775151305292
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "nyn5eupfk",
|
||||
"timestamp": 1775151292858
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "1p19qlpm9",
|
||||
"timestamp": 1775151291558
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "ymbfiylvo",
|
||||
"timestamp": 1775151242832
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "edk0n7ek6",
|
||||
"timestamp": 1775151241487
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "6kkr2rr8p",
|
||||
"timestamp": 1775151241115
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "vh49m05rr",
|
||||
"timestamp": 1775151239716
|
||||
},
|
||||
{
|
||||
"service": "php:5.6.40",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "n4nlkvjhx",
|
||||
"timestamp": 1775151171177
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "0kg7gss85",
|
||||
"timestamp": 1775151171159
|
||||
},
|
||||
{
|
||||
"service": "php:8.2.30",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "xw56foupv",
|
||||
"timestamp": 1775151171141
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "6599ey64j",
|
||||
"timestamp": 1775151171123
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "b4fswrwcb",
|
||||
"timestamp": 1775151170806
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "lhw2apj29",
|
||||
"timestamp": 1775151170787
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "7fhj5rrnd",
|
||||
"timestamp": 1775151170139
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "sde218qdo",
|
||||
"timestamp": 1775151170080
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
|
||||
+88
-14
@@ -17,6 +17,31 @@ import { cliAliasService } from '../services/CliAliasService'
|
||||
import { cliBinService } from '../services/CliBinService'
|
||||
import { serviceEventLogger } from '../services/ServiceEventLogger'
|
||||
import { hostsService } from '../services/HostsService'
|
||||
import net from 'net'
|
||||
|
||||
async function waitForPort(port: number, timeout: number = 5000): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const client = net.createConnection({ port, host: '127.0.0.1' }, () => {
|
||||
client.end();
|
||||
resolve(true);
|
||||
});
|
||||
client.on('error', reject);
|
||||
client.setTimeout(500);
|
||||
client.on('timeout', () => {
|
||||
client.destroy();
|
||||
reject(new Error('timeout'));
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function findExecutable(dir: string, name: string): string | null {
|
||||
if (!fs.existsSync(dir)) return null
|
||||
@@ -433,7 +458,8 @@ export function registerIpcHandlers(): void {
|
||||
const errorLogs = await processManager.getLastErrorLogs('nginx')
|
||||
return { success: false, message: `Nginx başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
||||
}
|
||||
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx devreye alınamadı.' }
|
||||
const ready = await waitForPort(settings.nginxPort, 5000);
|
||||
return { success: ready, message: ready ? 'Nginx başlatıldı.' : 'Nginx başlatıldı ancak port cevabı alınamadı.' }
|
||||
}
|
||||
|
||||
if (serviceName === 'apache') {
|
||||
@@ -446,7 +472,8 @@ export function registerIpcHandlers(): void {
|
||||
const errorLogs = await processManager.getLastErrorLogs('apache')
|
||||
return { success: false, message: `Apache başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
||||
}
|
||||
return { success, message: success ? 'Apache başlatıldı.' : 'Apache devreye alınamadı.' }
|
||||
const ready = await waitForPort(parseInt(settings.apachePort || '8080'), 5000);
|
||||
return { success: ready, message: ready ? 'Apache başlatıldı.' : 'Apache başlatıldı ancak port cevabı alınamadı.' }
|
||||
}
|
||||
|
||||
if (serviceName.startsWith('php')) {
|
||||
@@ -521,14 +548,15 @@ export function registerIpcHandlers(): void {
|
||||
|
||||
// Add a small delay to ensure previous instance (if any) is fully killed
|
||||
await sleep(1000)
|
||||
const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false)
|
||||
const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false, { PHPRC: path.dirname(configPath) })
|
||||
|
||||
if (!success) {
|
||||
const errorLogs = await processManager.getLastErrorLogs(serviceName)
|
||||
return { success: false, message: `${phpVersion || ''} PHP başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
||||
}
|
||||
|
||||
return { success, message: success ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP devreye alınamadı.` }
|
||||
const ready = await waitForPort(phpPort, 5000);
|
||||
return { success: ready, message: ready ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP başlatıldı ancak port cevabı alınamadı.` }
|
||||
}
|
||||
|
||||
if (serviceName.startsWith('mariadb') || serviceName.startsWith('mysql')) {
|
||||
@@ -977,6 +1005,62 @@ export function registerIpcHandlers(): void {
|
||||
return { success: true, settings }
|
||||
})
|
||||
|
||||
ipcMain.handle('services:clear-events', async () => {
|
||||
try {
|
||||
serviceEventLogger.clear()
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
console.error('Failed to clear events:', error)
|
||||
return { success: false, message: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('project:get-config', async (_event, { host, serverType }) => {
|
||||
const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects'
|
||||
const baseDir = path.join(configService.getBasePath(), 'generated_configs', projectsDir)
|
||||
|
||||
// Try both host.conf and host.local.conf
|
||||
const paths = [
|
||||
path.join(baseDir, `${host}.conf`),
|
||||
path.join(baseDir, `${host}.local.conf`)
|
||||
]
|
||||
|
||||
for (const p of paths) {
|
||||
if (fs.existsSync(p)) {
|
||||
return { content: fs.readFileSync(p, 'utf8'), path: p }
|
||||
}
|
||||
}
|
||||
|
||||
return { content: '', message: `Konfigürasyon dosyası bulunamadı. Aranan yollar: ${paths.join(', ')}` }
|
||||
})
|
||||
|
||||
ipcMain.handle('project:save-config', async (_event, { host, serverType, content }) => {
|
||||
const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects'
|
||||
const baseDir = path.join(configService.getBasePath(), 'generated_configs', projectsDir)
|
||||
|
||||
// Try to find existing file first (to overwrite the right one)
|
||||
const paths = [
|
||||
path.join(baseDir, `${host}.conf`),
|
||||
path.join(baseDir, `${host}.local.conf`)
|
||||
]
|
||||
|
||||
let configPath = paths[0]
|
||||
for (const p of paths) {
|
||||
if (fs.existsSync(p)) {
|
||||
configPath = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let finalContent = content
|
||||
if (!finalContent.includes('# MANUAL_EDIT')) {
|
||||
finalContent = `# MANUAL_EDIT\n${finalContent}`
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, finalContent, 'utf8')
|
||||
return { success: true, message: 'Konfigürasyon başarıyla kaydedildi.' }
|
||||
})
|
||||
|
||||
// Nginx Config Management
|
||||
ipcMain.handle('nginx:config:get', async () => {
|
||||
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf')
|
||||
@@ -1321,14 +1405,4 @@ export function registerIpcHandlers(): void {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('services:clear-events', async () => {
|
||||
try {
|
||||
serviceEventLogger.clear()
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
console.error('Failed to clear events:', error)
|
||||
return { success: false, message: error.message }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export class ProcessManager {
|
||||
})
|
||||
}
|
||||
|
||||
async startService(name: string, binPath: string, args: string[], shell: boolean = true): Promise<boolean> {
|
||||
async startService(name: string, binPath: string, args: string[], shell: boolean = true, envVars: Record<string, string> = {}): Promise<boolean> {
|
||||
if (this.processes.has(name)) {
|
||||
return true
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export class ProcessManager {
|
||||
try {
|
||||
this.addLog(name, `${t('common.starting')}... (${binPath})`)
|
||||
|
||||
const serviceEnv = { ...process.env }
|
||||
const serviceEnv = { ...process.env, ...envVars }
|
||||
const binDirPath = path.dirname(binPath)
|
||||
if (serviceEnv.PATH) {
|
||||
serviceEnv.PATH = `${binDirPath}${path.delimiter}${serviceEnv.PATH}`
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-nginx'
|
||||
import 'prismjs/components/prism-apacheconf'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
|
||||
interface ProjectConfigEditorProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
serverType: string
|
||||
}
|
||||
|
||||
const ProjectConfigEditor: React.FC<ProjectConfigEditorProps> = ({ value, onChange, serverType }) => {
|
||||
const isApache = serverType === 'apache'
|
||||
const language = isApache ? 'apacheconf' : 'nginx'
|
||||
const prismLang = isApache ? Prism.languages.apacheconf : Prism.languages.nginx
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
borderRadius: '0px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
fontFamily: '"Fira code", "Fira Mono", monospace',
|
||||
fontSize: '14px',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<Editor
|
||||
value={value}
|
||||
onValueChange={code => onChange(code)}
|
||||
highlight={code => Prism.highlight(code, prismLang, language)}
|
||||
padding={20}
|
||||
style={{
|
||||
fontFamily: '"Fira code", "Fira Mono", monospace',
|
||||
fontSize: 14,
|
||||
minHeight: '100%',
|
||||
outline: 'none',
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
textareaClassName="project-config-textarea"
|
||||
preClassName="project-config-pre"
|
||||
/>
|
||||
<style>{`
|
||||
.project-config-textarea {
|
||||
outline: none !important;
|
||||
caret-color: #fff !important;
|
||||
white-space: pre !important;
|
||||
overflow-wrap: normal !important;
|
||||
}
|
||||
.project-config-pre {
|
||||
pointer-events: none;
|
||||
white-space: pre !important;
|
||||
overflow-wrap: normal !important;
|
||||
}
|
||||
.token.comment { color: #6a9955; }
|
||||
.token.directive, .token.section { color: #569cd6; font-weight: bold; }
|
||||
.token.variable { color: #9cdcfe; }
|
||||
.token.string { color: #ce9178; }
|
||||
.token.number { color: #b5cea8; }
|
||||
.token.boolean { color: #569cd6; }
|
||||
.token.operator, .token.punctuation { color: #d4d4d4; }
|
||||
|
||||
/* Apache specific */
|
||||
.token.directive-inline { color: #569cd6; }
|
||||
.token.directive-block { color: #569cd6; font-weight: bold; }
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectConfigEditor
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
Divider,
|
||||
Stack,
|
||||
Tooltip,
|
||||
CircularProgress
|
||||
CircularProgress,
|
||||
Alert
|
||||
} from '@mui/material'
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
@@ -24,9 +25,12 @@ import {
|
||||
Delete as DeleteIcon,
|
||||
Tune as TuningIcon,
|
||||
OpenInNew as OpenIcon,
|
||||
Code as CodeIcon
|
||||
Code as CodeIcon,
|
||||
Save as SaveIcon
|
||||
} from '@mui/icons-material'
|
||||
import ProjectResourceMonitor from './ProjectResourceMonitor'
|
||||
import ProjectConfigEditor from './ProjectConfigEditor'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
interface ProjectDetailsProps {
|
||||
project: any;
|
||||
@@ -66,6 +70,9 @@ export default function ProjectDetails({
|
||||
setTransitioning
|
||||
}: ProjectDetailsProps) {
|
||||
const [isHostMapped, setIsHostMapped] = useState<boolean>(true);
|
||||
const [configText, setConfigText] = useState<string>('');
|
||||
const [isConfigLoading, setIsConfigLoading] = useState<boolean>(false);
|
||||
const [isConfigSaving, setIsConfigSaving] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (p?.host) {
|
||||
@@ -73,6 +80,59 @@ export default function ProjectDetails({
|
||||
window.api.invoke('hosts:check', finalHost).then(setIsHostMapped);
|
||||
}
|
||||
}, [p]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectDetailTab === 2 && p) {
|
||||
loadConfig();
|
||||
}
|
||||
}, [projectDetailTab, p]);
|
||||
|
||||
const loadConfig = async () => {
|
||||
setIsConfigLoading(true);
|
||||
try {
|
||||
const res = await window.api.invoke('project:get-config', { host: p.host, serverType: p.serverType });
|
||||
setConfigText(res.content);
|
||||
} catch (e) {
|
||||
console.error('Failed to load config:', e);
|
||||
} finally {
|
||||
setIsConfigLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
setIsConfigSaving(true);
|
||||
try {
|
||||
const res = await window.api.invoke('project:save-config', {
|
||||
host: p.host,
|
||||
serverType: p.serverType,
|
||||
content: configText
|
||||
});
|
||||
if (res.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: t('common.success'),
|
||||
text: t('common.saved_successfully'),
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
background: '#1e1e1e',
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: t('common.error'),
|
||||
text: e.message,
|
||||
background: '#1e1e1e',
|
||||
color: '#fff'
|
||||
});
|
||||
} finally {
|
||||
setIsConfigSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!p) {
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: 0.3 }}>
|
||||
@@ -202,91 +262,147 @@ export default function ProjectDetails({
|
||||
<Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary">
|
||||
<Tab label={t('projects.overview')} />
|
||||
<Tab label={t('projects.database')} />
|
||||
<Tab label={t('projects.server_settings')} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ p: 4, flexGrow: 1, overflowY: 'auto' }}>
|
||||
<Box sx={{ p: 0, flexGrow: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
{projectDetailTab === 0 && (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.site_domain')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.host}.local</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
<Box sx={{ p: 4 }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.site_domain')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.host}.local</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.web_server')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.serverType.toUpperCase()}</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.web_server')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.serverType.toUpperCase()}</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.php_version')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.phpVersion}</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.php_version')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.phpVersion}</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TuningIcon fontSize="small" color="primary" /> {t('dashboard.status')}
|
||||
</Typography>
|
||||
<ProjectResourceMonitor
|
||||
serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'}
|
||||
serverStats={p.serverType === 'apache' ? status.apache : status.nginx}
|
||||
phpStats={status[`php:${p.phpVersion}`]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TuningIcon fontSize="small" color="primary" /> {t('dashboard.status')}
|
||||
</Typography>
|
||||
<ProjectResourceMonitor
|
||||
serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'}
|
||||
serverStats={p.serverType === 'apache' ? status.apache : status.nginx}
|
||||
phpStats={status[`php:${p.phpVersion}`]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{projectDetailTab === 1 && (
|
||||
<Box sx={{ maxWidth: 600 }}>
|
||||
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.mariadb_version')}</Typography>
|
||||
<Typography sx={{ mt: 0.5, fontWeight: 600 }}>MariaDB {p.mariadbVersion}</Typography>
|
||||
<Box sx={{ p: 4 }}>
|
||||
<Box sx={{ maxWidth: 600 }}>
|
||||
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.mariadb_version')}</Typography>
|
||||
<Typography sx={{ mt: 0.5, fontWeight: 600 }}>MariaDB {p.mariadbVersion}</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<OpenIcon />}
|
||||
onClick={onManageDb}
|
||||
>
|
||||
{t('mariadb_manager.title')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3, opacity: 0.1 }} />
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Host</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>localhost</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Port</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{settings.mariaDbPort}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Username</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>root</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Password</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>(no password)</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{projectDetailTab === 2 && (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<CodeIcon color="primary" />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||
{p.serverType.toUpperCase()} {t('projects.server_settings')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<OpenIcon />}
|
||||
onClick={onManageDb}
|
||||
startIcon={<TuningIcon />}
|
||||
onClick={loadConfig}
|
||||
sx={{ color: 'rgba(255,255,255,0.5)' }}
|
||||
>
|
||||
{t('mariadb_manager.title')}
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="primary"
|
||||
startIcon={isConfigSaving ? <CircularProgress size={16} /> : <SaveIcon />}
|
||||
disabled={isConfigSaving || isConfigLoading}
|
||||
onClick={handleSaveConfig}
|
||||
sx={{ px: 3 }}
|
||||
>
|
||||
{t('common.save').toUpperCase()}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Alert severity="info" sx={{ borderRadius: 0, py: 0.5, bgcolor: 'rgba(57, 167, 255, 0.05)', color: 'primary.light', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
{t('projects.manual_edit_warning')}
|
||||
</Alert>
|
||||
|
||||
<Divider sx={{ mb: 3, opacity: 0.1 }} />
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Host</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>localhost</Typography>
|
||||
<Box sx={{ flexGrow: 1, position: 'relative', overflow: 'hidden' }}>
|
||||
{isConfigLoading ? (
|
||||
<Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'rgba(0,0,0,0.5)', zIndex: 10 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Port</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{settings.mariaDbPort}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Username</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>root</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Password</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>(no password)</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : null}
|
||||
<ProjectConfigEditor
|
||||
value={configText}
|
||||
onChange={setConfigText}
|
||||
serverType={p.serverType}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -85,6 +85,8 @@
|
||||
"open_site": "SITE ÖFFNEN",
|
||||
"stop_site": "SITE STOPPEN",
|
||||
"start_site": "SITE STARTEN",
|
||||
"server_settings": "SERVER-EINSTELLUNGEN",
|
||||
"manual_edit_warning": "Wenn Sie diese Einstellungen manuell bearbeiten, wird oben # MANUAL_EDIT hinzugefügt und das System beendet automatische Aktualisierungen.",
|
||||
"no_project_selected": "Bitte wählen Sie ein Projekt aus der Liste aus oder erstellen Sie ein neues."
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
"open_site": "OPEN SITE",
|
||||
"stop_site": "STOP SITE",
|
||||
"start_site": "START SITE",
|
||||
"server_settings": "SERVER SETTINGS",
|
||||
"manual_edit_warning": "When you manually edit these settings, # MANUAL_EDIT is added to the top and the system stops automatic updates.",
|
||||
"no_project_selected": "Please select a project from the list or create a new one."
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -92,6 +92,9 @@
|
||||
"open_site": "SİTEYİ AÇ",
|
||||
"stop_site": "SİTEYİ DURDUR",
|
||||
"start_site": "SİTEYİ BAŞLAT",
|
||||
"no_site_selected": "Lütfen önce bir site seçin.",
|
||||
"server_settings": "SUNUCU AYARLARI",
|
||||
"manual_edit_warning": "Bu ayarları manuel olarak düzenlediğinizde, en tepeye # MANUAL_EDIT eklenir ve sistem otomatik güncellemeleri durdurur.",
|
||||
"no_project_selected": "Lütfen listeden bir proje seçin veya yeni bir tane oluşturun."
|
||||
},
|
||||
"settings": {
|
||||
|
||||
Reference in New Issue
Block a user