feat: implement process management service and IPC bridge with initial renderer UI structure
This commit is contained in:
+38
-4
@@ -6,6 +6,7 @@ import { downloadService } from '../services/DownloadService'
|
||||
import { projectService } from '../services/ProjectService'
|
||||
import { phpManagerService } from '../services/PhpManagerService'
|
||||
import { mySqlManagerService } from '../services/MySqlManagerService'
|
||||
import { nginxManagerService } from '../services/NginxManagerService'
|
||||
import path from 'path'
|
||||
|
||||
function findExecutable(dir: string, name: string): string | null {
|
||||
@@ -49,14 +50,16 @@ export function registerIpcHandlers(): void {
|
||||
}
|
||||
|
||||
if (serviceName === 'php') {
|
||||
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {})
|
||||
// Find any installed PHP version
|
||||
const binPath = findExecutable(binDir, 'php-cgi.exe')
|
||||
|
||||
if (!binPath) return { success: false, message: 'PHP executable bulunamadı. Lütfen en az bir sürüm indirin.' }
|
||||
|
||||
const extDir = path.join(path.dirname(binPath), 'ext')
|
||||
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {
|
||||
EXT_DIR: extDir.replace(/\\/g, '\\\\') // PHP ini needs escaped backslashes or forward slashes
|
||||
})
|
||||
|
||||
const success = await processManager.startService('php', binPath, ['-b', `127.0.0.1:${settings.phpPort}`, '-c', configPath])
|
||||
return { success, message: success ? 'PHP başlatıldı.' : 'PHP dosyaları bulunamadı veya port meşgul.' }
|
||||
return { success, message: success ? 'PHP başlatıldı.' : 'PHP devreye alınamadı (Hata loglarını kontrol edin).' }
|
||||
}
|
||||
|
||||
if (serviceName === 'mysql') {
|
||||
@@ -98,6 +101,10 @@ export function registerIpcHandlers(): void {
|
||||
return processManager.getStatuses()
|
||||
})
|
||||
|
||||
ipcMain.handle('services:logs', async (_event, serviceName: string) => {
|
||||
return processManager.getLogs(serviceName)
|
||||
})
|
||||
|
||||
// Binary management
|
||||
ipcMain.handle('binaries:list-php', async () => {
|
||||
return phpManagerService.getVersions()
|
||||
@@ -153,6 +160,33 @@ export function registerIpcHandlers(): void {
|
||||
}
|
||||
})
|
||||
|
||||
// Nginx Versions
|
||||
ipcMain.handle('nginx:versions', async () => {
|
||||
return await nginxManagerService.getVersions()
|
||||
})
|
||||
|
||||
ipcMain.handle('nginx:download', async (event, id: string) => {
|
||||
const versions = await nginxManagerService.getVersions()
|
||||
const version = versions.find(v => v.id === id)
|
||||
if (!version) return { success: false, message: 'Geçersiz Nginx sürümü.' }
|
||||
|
||||
try {
|
||||
nginxManagerService.updateProgress(id, 0, 'downloading')
|
||||
event.sender.send('nginx:progress', { id, progress: 0 })
|
||||
|
||||
await downloadService.downloadAndExtract(version.url, id, (p) => {
|
||||
nginxManagerService.updateProgress(id, p)
|
||||
event.sender.send('nginx:progress', { id, progress: p })
|
||||
})
|
||||
|
||||
nginxManagerService.updateProgress(id, 100, 'installed')
|
||||
return { success: true, message: 'Nginx indirme tamamlandı.' }
|
||||
} catch (error: any) {
|
||||
nginxManagerService.updateProgress(id, 0, 'available')
|
||||
return { success: false, message: `Nginx hatası: ${error.message}` }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:select-directory', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory']
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
|
||||
export interface NginxVersion {
|
||||
id: string
|
||||
version: string
|
||||
url: string
|
||||
status: 'available' | 'downloading' | 'installed'
|
||||
progress: number
|
||||
}
|
||||
|
||||
export class NginxManagerService {
|
||||
private versions: NginxVersion[] = []
|
||||
private binDir: string
|
||||
|
||||
constructor() {
|
||||
this.binDir = path.join(app.getPath('userData'), 'bin')
|
||||
this.init()
|
||||
}
|
||||
|
||||
private async init() {
|
||||
if (!fs.existsSync(this.binDir)) {
|
||||
fs.mkdirSync(this.binDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
async getVersions(): Promise<NginxVersion[]> {
|
||||
// For Nginx, we can stick to one stable version or a small list
|
||||
const stableVersion = '1.26.2'
|
||||
const url = `https://nginx.org/download/nginx-${stableVersion}.zip`
|
||||
|
||||
const id = 'nginx'
|
||||
const targetDir = path.join(this.binDir, id)
|
||||
|
||||
// Check if installed (look for nginx.exe anywhere in the zip extraction folder)
|
||||
const isInstalled = this.isInstalled(targetDir)
|
||||
|
||||
const v: NginxVersion = {
|
||||
id,
|
||||
version: stableVersion,
|
||||
url,
|
||||
status: isInstalled ? 'installed' : 'available',
|
||||
progress: isInstalled ? 100 : 0
|
||||
}
|
||||
|
||||
// Maintain internal state for progress updates
|
||||
const existing = this.versions.find(ev => ev.id === id)
|
||||
if (existing && existing.status === 'downloading') {
|
||||
return [existing]
|
||||
}
|
||||
|
||||
this.versions = [v]
|
||||
return this.versions
|
||||
}
|
||||
|
||||
private isInstalled(dir: string): boolean {
|
||||
if (!fs.existsSync(dir)) return false
|
||||
|
||||
const findNginx = (d: string): boolean => {
|
||||
const files = fs.readdirSync(d)
|
||||
for (const f of files) {
|
||||
const p = path.join(d, f)
|
||||
const s = fs.statSync(p)
|
||||
if (s.isDirectory()) {
|
||||
if (findNginx(p)) return true
|
||||
} else if (f.toLowerCase() === 'nginx.exe') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return findNginx(dir)
|
||||
}
|
||||
|
||||
updateProgress(id: string, progress: number, status: NginxVersion['status'] = 'downloading') {
|
||||
const v = this.versions.find(ev => ev.id === id)
|
||||
if (v) {
|
||||
v.progress = progress
|
||||
v.status = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const nginxManagerService = new NginxManagerService()
|
||||
@@ -1,22 +1,80 @@
|
||||
import { spawn, ChildProcess } from 'child_process'
|
||||
import { join } from 'path'
|
||||
import { spawn, ChildProcess, exec } from 'child_process'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
|
||||
export class ProcessManager {
|
||||
private processes: Map<string, ChildProcess> = new Map()
|
||||
private logs: Map<string, string[]> = new Map()
|
||||
private logDir: string
|
||||
|
||||
constructor() {
|
||||
this.processes = new Map()
|
||||
this.logDir = ''
|
||||
}
|
||||
|
||||
private initLogs() {
|
||||
if (this.logDir) return
|
||||
try {
|
||||
this.logDir = path.join(app.getPath('userData'), 'logs')
|
||||
if (!fs.existsSync(this.logDir)) {
|
||||
fs.mkdirSync(this.logDir, { recursive: true })
|
||||
}
|
||||
// Initialize with default logs
|
||||
['nginx', 'php', 'mysql'].forEach(s => {
|
||||
if (!this.logs.has(s)) {
|
||||
this.logs.set(s, [`[${new Date().toLocaleTimeString()}] Sistem hazır. İşlem bekleniyor...`])
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to init logs:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private addLog(name: string, data: string) {
|
||||
this.initLogs()
|
||||
if (!this.logs.has(name)) this.logs.set(name, [])
|
||||
const serviceLogs = this.logs.get(name)!
|
||||
const lines = data.split(/\r?\n/).filter(l => l.trim())
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
|
||||
lines.forEach(line => {
|
||||
const formattedLine = `[${timestamp}] ${line}`
|
||||
console.log(`[LOG:${name}] ${formattedLine}`)
|
||||
serviceLogs.push(formattedLine)
|
||||
// Persist to file
|
||||
if (this.logDir) {
|
||||
try {
|
||||
fs.appendFileSync(path.join(this.logDir, `${name}.log`), formattedLine + '\n')
|
||||
} catch (e) {
|
||||
// Ignore write errors to file if it happens
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Keep last 100 lines in memory
|
||||
if (serviceLogs.length > 100) {
|
||||
this.logs.set(name, serviceLogs.slice(-100))
|
||||
}
|
||||
}
|
||||
|
||||
getLogs(name: string): string[] {
|
||||
this.initLogs()
|
||||
return this.logs.get(name) || []
|
||||
}
|
||||
|
||||
async runOnce(binPath: string, args: string[]): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(binPath, args, { stdio: 'inherit' })
|
||||
const child = spawn(binPath, args, { stdio: 'pipe' })
|
||||
|
||||
child.stdout?.on('data', (data) => console.log(`[INIT] ${data}`))
|
||||
child.stderr?.on('data', (data) => console.error(`[INIT ERROR] ${data}`))
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve(code === 0)
|
||||
})
|
||||
child.on('error', () => {
|
||||
child.on('error', (err) => {
|
||||
console.error('RunOnce error:', err)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
@@ -24,43 +82,47 @@ export class ProcessManager {
|
||||
|
||||
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
|
||||
if (this.processes.has(name)) {
|
||||
console.log(`Service ${name} is already running.`)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!fs.existsSync(binPath)) {
|
||||
console.warn(`[DEV] Executable not found: ${binPath}. Mocking service ${name}.`)
|
||||
const mockChild = {
|
||||
kill: () => { },
|
||||
on: () => { },
|
||||
unref: () => { }
|
||||
} as unknown as ChildProcess
|
||||
this.processes.set(name, mockChild)
|
||||
return true
|
||||
this.addLog(name, `Executable not found: ${binPath}`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
this.addLog(name, `Starting service with: ${binPath} ${args.join(' ')}`)
|
||||
|
||||
const child = spawn(binPath, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
cwd: path.dirname(binPath)
|
||||
})
|
||||
|
||||
console.log(`[PROCESS] Started ${name} with PID ${child.pid}`)
|
||||
this.addLog(name, `Process started (PID: ${child.pid})`)
|
||||
|
||||
child.stdout?.on('data', (data) => this.addLog(name, data.toString()))
|
||||
child.stderr?.on('data', (data) => this.addLog(name, data.toString()))
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`Failed to start service ${name}:`, err)
|
||||
this.addLog(name, `Failed to start: ${err.message}`)
|
||||
this.processes.delete(name)
|
||||
})
|
||||
|
||||
child.on('exit', (code) => {
|
||||
console.log(`Service ${name} exited with code ${code}`)
|
||||
this.addLog(name, `Exited with code ${code}`)
|
||||
this.processes.delete(name)
|
||||
})
|
||||
|
||||
this.processes.set(name, child)
|
||||
child.unref() // Allow the parent to exit independently
|
||||
child.unref()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Error starting service ${name}:`, error)
|
||||
} catch (error: any) {
|
||||
this.addLog(name, `Spawn Error: ${error.message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -68,11 +130,25 @@ export class ProcessManager {
|
||||
async stopService(name: string): Promise<boolean> {
|
||||
const child = this.processes.get(name)
|
||||
if (child) {
|
||||
this.addLog(name, 'Stopping service...')
|
||||
child.kill()
|
||||
this.processes.delete(name)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
// Also force kill lingering processes
|
||||
return this.forceKillAll(name)
|
||||
}
|
||||
|
||||
async forceKillAll(name: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'php' ? 'php-cgi.exe' : 'mysqld.exe'
|
||||
this.addLog(name, `Force killing all ${executableName} processes...`)
|
||||
|
||||
exec(`taskkill /F /IM ${executableName} /T`, (_err) => {
|
||||
// If it fails, usually it means the process isn't running, which is fine
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
isServiceRunning(name: string): boolean {
|
||||
|
||||
@@ -6,6 +6,7 @@ const api = {
|
||||
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
|
||||
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
||||
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
||||
getServiceLogs: (name: string) => electronAPI.ipcRenderer.invoke('services:logs'),
|
||||
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
||||
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
|
||||
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
|
||||
@@ -19,7 +20,10 @@ const api = {
|
||||
onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)),
|
||||
selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'),
|
||||
getSettings: () => electronAPI.ipcRenderer.invoke('config:get'),
|
||||
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings)
|
||||
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings),
|
||||
getNginxVersions: () => electronAPI.ipcRenderer.invoke('nginx:versions'),
|
||||
downloadNginxVersion: (id: string) => electronAPI.ipcRenderer.invoke('nginx:download', id),
|
||||
onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data))
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
+190
-13
@@ -53,7 +53,9 @@ import {
|
||||
Refresh as RefreshIcon,
|
||||
FolderOpen as FolderOpenIcon,
|
||||
Save as SaveIcon,
|
||||
Terminal as TerminalIcon
|
||||
LanguageOutlined as WebIcon,
|
||||
Terminal as TerminalIcon,
|
||||
ContentCopy as CopyIcon
|
||||
} from '@mui/icons-material'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -90,6 +92,7 @@ function App(): JSX.Element {
|
||||
const [projects, setProjects] = useState<any[]>([])
|
||||
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
||||
const [mySqlVersions, setMySqlVersions] = useState<any[]>([])
|
||||
const [nginxVersions, setNginxVersions] = useState<any[]>([])
|
||||
const [loadingVersions, setLoadingVersions] = useState(false)
|
||||
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
|
||||
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' })
|
||||
@@ -105,6 +108,11 @@ function App(): JSX.Element {
|
||||
severity: 'success'
|
||||
})
|
||||
|
||||
// Log States
|
||||
const [logDialogOpen, setLogDialogOpen] = useState(false)
|
||||
const [selectedService, setSelectedService] = useState('')
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
|
||||
const fetchStatus = async () => {
|
||||
if (window.api && window.api.getServiceStatus) {
|
||||
const currentStatus = await window.api.getServiceStatus()
|
||||
@@ -140,10 +148,24 @@ function App(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNginxVersions = async () => {
|
||||
if (window.api && window.api.getNginxVersions) {
|
||||
const list = await window.api.getNginxVersions()
|
||||
setNginxVersions(list || [])
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLogs = async (service: string) => {
|
||||
if (window.api && window.api.getServiceLogs) {
|
||||
const list = await window.api.getServiceLogs(service)
|
||||
setLogs(list || [])
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshVersions = async () => {
|
||||
setLoadingVersions(true)
|
||||
try {
|
||||
await Promise.all([fetchPhpVersions(), fetchMySqlVersions()])
|
||||
await Promise.all([fetchPhpVersions(), fetchMySqlVersions(), fetchNginxVersions()])
|
||||
} finally {
|
||||
setLoadingVersions(false)
|
||||
}
|
||||
@@ -165,6 +187,7 @@ function App(): JSX.Element {
|
||||
fetchProjects()
|
||||
fetchPhpVersions()
|
||||
fetchMySqlVersions()
|
||||
fetchNginxVersions()
|
||||
fetchSettings()
|
||||
|
||||
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
|
||||
@@ -179,19 +202,35 @@ function App(): JSX.Element {
|
||||
setMySqlVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
|
||||
}) : null
|
||||
|
||||
const removeNginxListener = window.api.onNginxProgress ? window.api.onNginxProgress((data) => {
|
||||
if (!data) return
|
||||
const { id, progress } = data
|
||||
setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
|
||||
}) : null
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchStatus()
|
||||
fetchProjects()
|
||||
fetchPhpVersions()
|
||||
fetchMySqlVersions()
|
||||
fetchNginxVersions()
|
||||
}, 5000)
|
||||
|
||||
let logInterval: NodeJS.Timeout | null = null
|
||||
if (logDialogOpen && selectedService) {
|
||||
logInterval = setInterval(() => {
|
||||
fetchLogs(selectedService)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
if (logInterval) clearInterval(logInterval)
|
||||
if (typeof removePhpListener === 'function') removePhpListener()
|
||||
if (typeof removeMySqlListener === 'function') removeMySqlListener()
|
||||
if (typeof removeNginxListener === 'function') removeNginxListener()
|
||||
}
|
||||
}, [])
|
||||
}, [logDialogOpen, selectedService])
|
||||
|
||||
const handleSelectPath = async () => {
|
||||
if (window.api && window.api.selectDirectory) {
|
||||
@@ -204,10 +243,19 @@ function App(): JSX.Element {
|
||||
|
||||
const handleToggleService = async (service: keyof ServiceStatus) => {
|
||||
if (!window.api) return
|
||||
|
||||
const isStarting = status[service] === 'stopped'
|
||||
if (isStarting) {
|
||||
setStatus(prev => ({ ...prev, [service]: 'starting' }))
|
||||
}
|
||||
|
||||
const result = status[service] === 'running'
|
||||
? await window.api.stopService(service)
|
||||
: await window.api.startService(service)
|
||||
|
||||
// Refresh status after operation
|
||||
fetchStatus()
|
||||
|
||||
setNotification({
|
||||
open: true,
|
||||
message: result.message,
|
||||
@@ -215,6 +263,12 @@ function App(): JSX.Element {
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenLogs = async (service: string) => {
|
||||
setSelectedService(service)
|
||||
setLogDialogOpen(true)
|
||||
await fetchLogs(service)
|
||||
}
|
||||
|
||||
const handleSaveSettings = async () => {
|
||||
if (window.api && window.api.saveSettings) {
|
||||
const result = await window.api.saveSettings(settings)
|
||||
@@ -232,11 +286,17 @@ function App(): JSX.Element {
|
||||
const handleStartAll = async () => {
|
||||
if (!window.api) return
|
||||
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
|
||||
for (const s of services) {
|
||||
if (status[s] === 'stopped') {
|
||||
setStatus(prev => ({ ...prev, [s]: 'starting' }))
|
||||
}
|
||||
}
|
||||
for (const s of services) {
|
||||
if (status[s] !== 'running') {
|
||||
await window.api.startService(s)
|
||||
}
|
||||
}
|
||||
fetchStatus()
|
||||
setNotification({ open: true, message: 'Tüm servisler başlatıldı.', severity: 'success' })
|
||||
}
|
||||
|
||||
@@ -248,10 +308,14 @@ function App(): JSX.Element {
|
||||
await window.api.stopService(s)
|
||||
}
|
||||
}
|
||||
fetchStatus()
|
||||
setNotification({ open: true, message: 'Tüm servisler durduruldu.', severity: 'success' })
|
||||
}
|
||||
|
||||
const getStatusChip = (s: string) => {
|
||||
if (s === 'starting') {
|
||||
return <Chip label="BAŞLATILIYOR..." color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
|
||||
}
|
||||
const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning'
|
||||
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
||||
}
|
||||
@@ -294,6 +358,17 @@ function App(): JSX.Element {
|
||||
fetchMySqlVersions()
|
||||
}
|
||||
|
||||
const handleDownloadNginx = async (id: string) => {
|
||||
if (!window.api) return
|
||||
const result = await window.api.downloadNginxVersion(id)
|
||||
setNotification({
|
||||
open: true,
|
||||
message: result.message,
|
||||
severity: result.success ? 'success' : 'error'
|
||||
})
|
||||
fetchNginxVersions()
|
||||
}
|
||||
|
||||
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
|
||||
|
||||
return (
|
||||
@@ -359,7 +434,7 @@ function App(): JSX.Element {
|
||||
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
|
||||
Genel Ayarlar
|
||||
</Typography>
|
||||
{settingsTab < 2 && (
|
||||
{settingsTab < 3 && (
|
||||
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
|
||||
Listeyi Yenile
|
||||
</Button>
|
||||
@@ -368,8 +443,9 @@ function App(): JSX.Element {
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
|
||||
<Tab label="PHP Sürümleri" icon={<PhpIcon />} iconPosition="start" />
|
||||
<Tab label="MySQL Sürümleri" icon={<DbIcon />} iconPosition="start" />
|
||||
<Tab label="PHP" icon={<PhpIcon />} iconPosition="start" />
|
||||
<Tab label="MySQL" icon={<DbIcon />} iconPosition="start" />
|
||||
<Tab label="Nginx" icon={<WebIcon />} iconPosition="start" />
|
||||
<Tab label="Sunucu Ayarları" icon={<ServerIcon />} iconPosition="start" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
@@ -464,6 +540,44 @@ function App(): JSX.Element {
|
||||
)}
|
||||
|
||||
{settingsTab === 2 && (
|
||||
<Paper sx={{ bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>Nginx Web Sunucusu</Typography>
|
||||
<Typography variant="body2" color="text.secondary">Sunucu için gerekli Nginx binary dosyalarını buradan indirebilirsiniz.</Typography>
|
||||
</Box>
|
||||
<List>
|
||||
{nginxVersions.map((v) => (
|
||||
<ListItem key={v.id} sx={{ py: 2 }}>
|
||||
<ListItemIcon>
|
||||
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`Nginx ${v.version} (Stable)`}
|
||||
secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Box sx={{ minWidth: 120, textAlign: 'right' }}>
|
||||
{v.status === 'available' && (
|
||||
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadNginx(v.id)}>
|
||||
İNDİR
|
||||
</Button>
|
||||
)}
|
||||
{v.status === 'downloading' && (
|
||||
<Box sx={{ width: '100%', mt: 1 }}>
|
||||
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
|
||||
</Box>
|
||||
)}
|
||||
{v.status === 'installed' && (
|
||||
<Chip label="HAZIR" color="success" variant="outlined" size="small" />
|
||||
)}
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{settingsTab === 3 && (
|
||||
<Box component={Stack} spacing={3}>
|
||||
<Paper sx={{ p: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Port Ayarları</Typography>
|
||||
@@ -511,29 +625,50 @@ function App(): JSX.Element {
|
||||
<Paper elevation={4} sx={{ mb: 4, borderRadius: 3, overflow: 'hidden', bgcolor: 'rgba(255, 255, 255, 0.03)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<List dense>
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><ServerIcon color="primary" /></ListItemIcon>
|
||||
<ListItemIcon><WebIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="Nginx Web Sunucusu" secondary={`Port: ${settings.nginxPort}`} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton size="small" onClick={() => handleOpenLogs('nginx')} title="Logları Görüntüle">
|
||||
<TerminalIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{getStatusChip(status.nginx)}
|
||||
<Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} />
|
||||
<Switch
|
||||
checked={status.nginx === 'running' || status.nginx === 'starting'}
|
||||
onChange={() => handleToggleService('nginx')}
|
||||
disabled={status.nginx === 'starting'}
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
<Divider variant="inset" />
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="PHP FastCGI" secondary={`Port: ${settings.phpPort}`} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton size="small" onClick={() => handleOpenLogs('php')} title="Logları Görüntüle">
|
||||
<TerminalIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{getStatusChip(status.php)}
|
||||
<Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} />
|
||||
<Switch
|
||||
checked={status.php === 'running' || status.php === 'starting'}
|
||||
onChange={() => handleToggleService('php')}
|
||||
disabled={status.php === 'starting'}
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
<Divider variant="inset" />
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><DbIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="MySQL / MariaDB" secondary={`Port: ${settings.mysqlPort}`} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton size="small" onClick={() => handleOpenLogs('mysql')} title="Logları Görüntüle">
|
||||
<TerminalIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{getStatusChip(status.mysql)}
|
||||
<Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} />
|
||||
<Switch
|
||||
checked={status.mysql === 'running' || status.mysql === 'starting'}
|
||||
onChange={() => handleToggleService('mysql')}
|
||||
disabled={status.mysql === 'starting'}
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
@@ -662,6 +797,48 @@ function App(): JSX.Element {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Log Dialog */}
|
||||
<Dialog open={logDialogOpen} onClose={() => setLogDialogOpen(false)} fullWidth maxWidth="md">
|
||||
<DialogTitle sx={{ bgcolor: '#1e1e1e', color: '#fff', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TerminalIcon />
|
||||
{selectedService.toUpperCase()} Konsol Çıktısı
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton size="small" sx={{ color: '#fff' }} onClick={() => navigator.clipboard.writeText(logs.join('\n'))}>
|
||||
<CopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ bgcolor: '#000', p: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
fontFamily: 'Consolas, monospace',
|
||||
fontSize: '0.85rem',
|
||||
color: '#00ff00',
|
||||
height: '400px',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<Typography variant="caption" sx={{ color: '#666' }}>Henüz log kaydı yok...</Typography>
|
||||
) : (
|
||||
logs.map((line, i) => (
|
||||
<Box key={i} sx={{ mb: 0.5, borderLeft: '2px solid #333', pl: 1 }}>
|
||||
{line}
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ bgcolor: '#1e1e1e' }}>
|
||||
<Button onClick={() => setLogDialogOpen(false)} variant="contained" size="small">Kapat</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user