feat: implement core architecture and dashboard UI for service and project management
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
[mysqld]
|
||||
port=3306
|
||||
port={{PORT}}
|
||||
datadir="{{DATADIR}}"
|
||||
socket="{{SOCKET}}"
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
|
||||
[client]
|
||||
port=3306
|
||||
port={{PORT}}
|
||||
socket="{{SOCKET}}"
|
||||
default-character-set=utf8mb4
|
||||
|
||||
@@ -19,7 +19,7 @@ http {
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass 127.0.0.1:9001;
|
||||
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
|
||||
+53
-11
@@ -1,4 +1,5 @@
|
||||
import { ipcMain, app, dialog } from 'electron'
|
||||
import fs from 'fs'
|
||||
import { processManager } from '../services/ProcessManager'
|
||||
import { configService } from '../services/ConfigService'
|
||||
import { downloadService } from '../services/DownloadService'
|
||||
@@ -7,37 +8,78 @@ import { phpManagerService } from '../services/PhpManagerService'
|
||||
import { mySqlManagerService } from '../services/MySqlManagerService'
|
||||
import path from 'path'
|
||||
|
||||
function findExecutable(dir: string, name: string): string | null {
|
||||
if (!fs.existsSync(dir)) return null
|
||||
const files = fs.readdirSync(dir)
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file)
|
||||
const stat = fs.statSync(fullPath)
|
||||
if (stat.isDirectory()) {
|
||||
const found = findExecutable(fullPath, name)
|
||||
if (found) return found
|
||||
} else if (file.toLowerCase() === name.toLowerCase()) {
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
// Service management
|
||||
ipcMain.handle('services:start', async (_event, serviceName: string) => {
|
||||
try {
|
||||
const settings = configService.getSettings()
|
||||
const binDir = path.join(app.getPath('userData'), 'bin')
|
||||
|
||||
if (serviceName === 'nginx') {
|
||||
const rootPath = path.join(app.getAppPath(), 'www')
|
||||
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
|
||||
PORT: '80',
|
||||
PORT: settings.nginxPort,
|
||||
PHP_PORT: settings.phpPort,
|
||||
ROOT: rootPath
|
||||
})
|
||||
const binPath = downloadService.getBinPath('nginx', 'nginx.exe')
|
||||
|
||||
const nginxDir = path.join(binDir, 'nginx')
|
||||
const binPath = findExecutable(nginxDir, 'nginx.exe')
|
||||
|
||||
if (!binPath) return { success: false, message: 'Nginx executable bulunamadı. Lütfen indirin.' }
|
||||
|
||||
const success = await processManager.startService('nginx', binPath, ['-c', configPath])
|
||||
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx başlatılamadı.' }
|
||||
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx dosyaları bulunamadı veya port meşgul.' }
|
||||
}
|
||||
|
||||
if (serviceName === 'php') {
|
||||
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {})
|
||||
const binPath = downloadService.getBinPath('php8.2', 'php-cgi.exe')
|
||||
const success = await processManager.startService('php', binPath, ['-b', '127.0.0.1:9001', '-c', configPath])
|
||||
return { success, message: success ? 'PHP başlatıldı.' : 'PHP başlatılamadı.' }
|
||||
// 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 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.' }
|
||||
}
|
||||
|
||||
if (serviceName === 'mysql') {
|
||||
const dataDir = path.join(app.getPath('userData'), 'mysql_data')
|
||||
const binPath = findExecutable(binDir, 'mysqld.exe')
|
||||
|
||||
if (!binPath) return { success: false, message: 'MySQL executable bulunamadı. Lütfen indirin.' }
|
||||
|
||||
// Initialize if data directory is empty
|
||||
if (!fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0) {
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
|
||||
console.log('Initializing MySQL data directory...')
|
||||
await processManager.runOnce(binPath, ['--initialize-insecure', `--datadir=${dataDir.replace(/\\/g, '/')}`])
|
||||
}
|
||||
|
||||
const configPath = await configService.generateConfig('my.ini.template', 'my.ini', {
|
||||
PORT: settings.mysqlPort,
|
||||
DATADIR: dataDir,
|
||||
SOCKET: '/tmp/mysql.sock'
|
||||
})
|
||||
const binPath = downloadService.getBinPath('mysql', 'bin/mysqld.exe')
|
||||
|
||||
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath])
|
||||
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL başlatılamadı.' }
|
||||
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL dosyaları bulunamadı veya port meşgul.' }
|
||||
}
|
||||
|
||||
return { success: false, message: 'Bilinmeyen servis.' }
|
||||
@@ -140,11 +182,11 @@ export function registerIpcHandlers(): void {
|
||||
})
|
||||
|
||||
// Config management
|
||||
ipcMain.handle('config:get', async (_event, key: string) => {
|
||||
return null
|
||||
ipcMain.handle('config:get', async () => {
|
||||
return configService.getSettings()
|
||||
})
|
||||
|
||||
ipcMain.handle('config:save', async (_event, config: any) => {
|
||||
return { success: true }
|
||||
return { success: true, settings: configService.updateSettings(config) }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,14 +5,26 @@ import { app } from 'electron'
|
||||
export class ConfigService {
|
||||
private configDir: string
|
||||
private templateDir: string
|
||||
private settingsPath: string
|
||||
private defaultSettings = {
|
||||
nginxPort: '80',
|
||||
phpPort: '9001',
|
||||
mysqlPort: '3306',
|
||||
language: 'tr'
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.templateDir = path.join(app.getAppPath(), 'config')
|
||||
this.configDir = path.join(app.getPath('userData'), 'generated_configs')
|
||||
this.settingsPath = path.join(app.getPath('userData'), 'settings.json')
|
||||
|
||||
if (!fs.existsSync(this.configDir)) {
|
||||
fs.mkdirSync(this.configDir, { recursive: true })
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.settingsPath)) {
|
||||
fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> {
|
||||
@@ -37,6 +49,30 @@ export class ConfigService {
|
||||
getGeneratedPath(targetName: string): string {
|
||||
return path.join(this.configDir, targetName)
|
||||
}
|
||||
|
||||
getSettings(): any {
|
||||
try {
|
||||
if (fs.existsSync(this.settingsPath)) {
|
||||
const data = fs.readFileSync(this.settingsPath, 'utf-8')
|
||||
return { ...this.defaultSettings, ...JSON.parse(data) }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to read settings:', e)
|
||||
}
|
||||
return this.defaultSettings
|
||||
}
|
||||
|
||||
updateSettings(newSettings: any) {
|
||||
try {
|
||||
const current = this.getSettings()
|
||||
const updated = { ...current, ...newSettings }
|
||||
fs.writeFileSync(this.settingsPath, JSON.stringify(updated, null, 2))
|
||||
return updated
|
||||
} catch (e) {
|
||||
console.error('Failed to save settings:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const configService = new ConfigService()
|
||||
|
||||
@@ -10,6 +10,18 @@ export class ProcessManager {
|
||||
this.processes = new Map()
|
||||
}
|
||||
|
||||
async runOnce(binPath: string, args: string[]): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(binPath, args, { stdio: 'inherit' })
|
||||
child.on('close', (code) => {
|
||||
resolve(code === 0)
|
||||
})
|
||||
child.on('error', () => {
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
|
||||
if (this.processes.has(name)) {
|
||||
console.log(`Service ${name} is already running.`)
|
||||
|
||||
@@ -17,7 +17,9 @@ const api = {
|
||||
getMySqlVersions: () => electronAPI.ipcRenderer.invoke('mysql:versions'),
|
||||
downloadMySqlVersion: (id: string) => electronAPI.ipcRenderer.invoke('mysql:download', id),
|
||||
onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)),
|
||||
selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory')
|
||||
selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'),
|
||||
getSettings: () => electronAPI.ipcRenderer.invoke('config:get'),
|
||||
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -33,7 +33,8 @@ import {
|
||||
Tabs,
|
||||
Tab,
|
||||
InputAdornment,
|
||||
CircularProgress
|
||||
CircularProgress,
|
||||
Stack
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Dashboard as DashboardIcon,
|
||||
@@ -50,7 +51,9 @@ import {
|
||||
CheckCircle as InstalledIcon,
|
||||
CloudDownload as AvailableIcon,
|
||||
Refresh as RefreshIcon,
|
||||
FolderOpen as FolderOpenIcon
|
||||
FolderOpen as FolderOpenIcon,
|
||||
Save as SaveIcon,
|
||||
Terminal as TerminalIcon
|
||||
} from '@mui/icons-material'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -68,6 +71,13 @@ interface ServiceStatus {
|
||||
mysql: string
|
||||
}
|
||||
|
||||
interface AppSettings {
|
||||
nginxPort: string
|
||||
phpPort: string
|
||||
mysqlPort: string
|
||||
language: string
|
||||
}
|
||||
|
||||
function App(): JSX.Element {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [status, setStatus] = useState<ServiceStatus>({
|
||||
@@ -83,6 +93,12 @@ function App(): JSX.Element {
|
||||
const [loadingVersions, setLoadingVersions] = useState(false)
|
||||
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
|
||||
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' })
|
||||
const [settings, setSettings] = useState<AppSettings>({
|
||||
nginxPort: '80',
|
||||
phpPort: '9001',
|
||||
mysqlPort: '3306',
|
||||
language: 'tr'
|
||||
})
|
||||
const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
||||
open: false,
|
||||
message: '',
|
||||
@@ -96,6 +112,13 @@ function App(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSettings = async () => {
|
||||
if (window.api && window.api.getSettings) {
|
||||
const s = await window.api.getSettings()
|
||||
if (s) setSettings(s)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchProjects = async () => {
|
||||
if (window.api && window.api.getProjects) {
|
||||
const list = await window.api.getProjects()
|
||||
@@ -142,6 +165,7 @@ function App(): JSX.Element {
|
||||
fetchProjects()
|
||||
fetchPhpVersions()
|
||||
fetchMySqlVersions()
|
||||
fetchSettings()
|
||||
|
||||
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
|
||||
if (!data) return
|
||||
@@ -160,7 +184,7 @@ function App(): JSX.Element {
|
||||
fetchProjects()
|
||||
fetchPhpVersions()
|
||||
fetchMySqlVersions()
|
||||
}, 3000)
|
||||
}, 5000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
@@ -191,6 +215,15 @@ function App(): JSX.Element {
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveSettings = async () => {
|
||||
if (window.api && window.api.saveSettings) {
|
||||
const result = await window.api.saveSettings(settings)
|
||||
if (result.success) {
|
||||
setNotification({ open: true, message: 'Ayarlar kaydedildi.', severity: 'success' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language === 'tr' ? 'en' : i18n.language === 'en' ? 'de' : 'tr'
|
||||
i18n.changeLanguage(newLang)
|
||||
@@ -326,15 +359,18 @@ function App(): JSX.Element {
|
||||
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
|
||||
Genel Ayarlar
|
||||
</Typography>
|
||||
{settingsTab < 2 && (
|
||||
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
|
||||
Listeyi Yenile
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary">
|
||||
<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="Sunucu Ayarları" icon={<ServerIcon />} iconPosition="start" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -426,6 +462,45 @@ function App(): JSX.Element {
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{settingsTab === 2 && (
|
||||
<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>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3, mt: 2 }}>
|
||||
<TextField
|
||||
label="Nginx Portu"
|
||||
value={settings.nginxPort}
|
||||
onChange={(e) => setSettings({ ...settings, nginxPort: e.target.value })}
|
||||
variant="filled"
|
||||
size="small"
|
||||
helperText="Varsayılan: 80"
|
||||
/>
|
||||
<TextField
|
||||
label="PHP FastCGI Portu"
|
||||
value={settings.phpPort}
|
||||
onChange={(e) => setSettings({ ...settings, phpPort: e.target.value })}
|
||||
variant="filled"
|
||||
size="small"
|
||||
helperText="Varsayılan: 9001"
|
||||
/>
|
||||
<TextField
|
||||
label="MySQL Portu"
|
||||
value={settings.mysqlPort}
|
||||
onChange={(e) => setSettings({ ...settings, mysqlPort: e.target.value })}
|
||||
variant="filled"
|
||||
size="small"
|
||||
helperText="Varsayılan: 3306"
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button variant="contained" startIcon={<SaveIcon />} size="large" onClick={handleSaveSettings}>
|
||||
Ayarları Kaydet
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{activeTab === 'dashboard' && (
|
||||
@@ -437,7 +512,7 @@ function App(): JSX.Element {
|
||||
<List dense>
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><ServerIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="Nginx Web Sunucusu" secondary="Port: 80, 443" />
|
||||
<ListItemText primary="Nginx Web Sunucusu" secondary={`Port: ${settings.nginxPort}`} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getStatusChip(status.nginx)}
|
||||
<Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} />
|
||||
@@ -446,7 +521,7 @@ function App(): JSX.Element {
|
||||
<Divider variant="inset" />
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="PHP FastCGI" secondary="Sürüm: Seçilmedi" />
|
||||
<ListItemText primary="PHP FastCGI" secondary={`Port: ${settings.phpPort}`} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getStatusChip(status.php)}
|
||||
<Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} />
|
||||
@@ -455,7 +530,7 @@ function App(): JSX.Element {
|
||||
<Divider variant="inset" />
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><DbIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="MySQL / MariaDB" secondary="Port: 3306" />
|
||||
<ListItemText primary="MySQL / MariaDB" secondary={`Port: ${settings.mysqlPort}`} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getStatusChip(status.mysql)}
|
||||
<Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} />
|
||||
|
||||
Reference in New Issue
Block a user