feat: implement core application UI and backend services for PHP environment management

This commit is contained in:
Ümit Tunç
2026-03-28 20:34:33 +03:00
parent f336bd2e7c
commit ab037b7bd9
14 changed files with 542 additions and 143 deletions
+12
View File
@@ -20,6 +20,18 @@ http {
try_files $uri $uri/ /index.php?$query_string; try_files $uri $uri/ /index.php?$query_string;
} }
location /phpmyadmin {
alias "{{PHPMYADMIN_DIR}}";
index index.php;
location ~ ^/phpmyadmin/(.+\.php)$ {
alias "{{PHPMYADMIN_DIR}}/$1";
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $request_filename;
include "{{CONF_DIR}}/fastcgi_params";
}
}
location ~ \.php$ { location ~ \.php$ {
fastcgi_pass 127.0.0.1:{{PHP_PORT}}; fastcgi_pass 127.0.0.1:{{PHP_PORT}};
fastcgi_index index.php; fastcgi_index index.php;
+8
View File
@@ -65,6 +65,14 @@ multiphp/
--- ---
## phpMyAdmin Entegrasyonu
- phpMyAdmin'in en son sürümünün (all-languages.zip) otomatik indirilmesi ve ayıklanması.
- Nginx üzerinde `/phpmyadmin` aliası ile yapılandırma.
- `config.inc.php` dosyasının yerel MySQL ile uyumlu şekilde otomatik oluşturulması.
- Dashboard üzerinde phpMyAdmin'e hızlı erişim butonu.
---
## Doğrulama ve Test Planı ## Doğrulama ve Test Planı
### Otomatik Testler ### Otomatik Testler
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.
+17 -1
View File
@@ -7,6 +7,7 @@ import { projectService } from '../services/ProjectService'
import { phpManagerService } from '../services/PhpManagerService' import { phpManagerService } from '../services/PhpManagerService'
import { mySqlManagerService } from '../services/MySqlManagerService' import { mySqlManagerService } from '../services/MySqlManagerService'
import { nginxManagerService } from '../services/NginxManagerService' import { nginxManagerService } from '../services/NginxManagerService'
import { phpMyAdminService } from '../services/PhpMyAdminService'
import path from 'path' import path from 'path'
function findExecutable(dir: string, name: string): string | null { function findExecutable(dir: string, name: string): string | null {
@@ -58,7 +59,8 @@ export function registerIpcHandlers(): void {
PHP_PORT: settings.phpPort, PHP_PORT: settings.phpPort,
ROOT: rootPath, ROOT: rootPath,
LOG_DIR: logDir.replace(/\\/g, '/'), LOG_DIR: logDir.replace(/\\/g, '/'),
CONF_DIR: confDir.replace(/\\/g, '/') CONF_DIR: confDir.replace(/\\/g, '/'),
PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/')
}) })
// Test config first with prefix // Test config first with prefix
@@ -241,6 +243,9 @@ export function registerIpcHandlers(): void {
ipcMain.handle('projects:remove', async (_event, id) => { ipcMain.handle('projects:remove', async (_event, id) => {
return projectService.removeProject(id) return projectService.removeProject(id)
}) })
ipcMain.handle('projects:update', async (_event, project) => {
return projectService.updateProject(project)
})
// Config management // Config management
ipcMain.handle('config:get', async () => { ipcMain.handle('config:get', async () => {
@@ -250,4 +255,15 @@ export function registerIpcHandlers(): void {
ipcMain.handle('config:save', async (_event, config: any) => { ipcMain.handle('config:save', async (_event, config: any) => {
return { success: true, settings: configService.updateSettings(config) } return { success: true, settings: configService.updateSettings(config) }
}) })
// phpMyAdmin
ipcMain.handle('pma:status', async () => {
return await phpMyAdminService.isInstalled()
})
ipcMain.handle('pma:setup', async (event, mysqlPort: string) => {
return await phpMyAdminService.setup(mysqlPort, (p) => {
event.sender.send('pma:progress', p)
})
})
} }
+6 -11
View File
@@ -15,19 +15,14 @@ export class DownloadService {
} }
} }
async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void): Promise<string> { async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void, customTargetDir?: string): Promise<string> {
const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`) const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`)
const targetDir = path.join(this.binDir, targetName) const targetDir = customTargetDir || path.join(this.binDir, targetName)
const phpExe = path.join(targetDir, 'php.exe')
// If already installed and working, skip // Cleanup failed/previous attempts if we want a fresh start
if (fs.existsSync(phpExe)) { // For phpMyAdmin, we always want a fresh extract to avoid version mix
return targetDir if (fs.existsSync(targetDir) && !customTargetDir) {
} // For PHP/MySQL we might skip, but let's make it smarter
// Cleanup failed attempts
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true })
} }
const writer = fs.createWriteStream(tempFile) const writer = fs.createWriteStream(tempFile)
+75
View File
@@ -0,0 +1,75 @@
import path from 'path'
import fs from 'fs'
import { app } from 'electron'
import { downloadService } from './DownloadService'
export class PhpMyAdminService {
private pmaDir: string
private downloadUrl = 'https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.zip'
constructor() {
this.pmaDir = path.join(app.getPath('userData'), 'phpmyadmin')
}
async isInstalled(): Promise<boolean> {
return fs.existsSync(path.join(this.pmaDir, 'index.php'))
}
async setup(mysqlPort: string, onProgress?: (progress: number) => void): Promise<boolean> {
if (!fs.existsSync(this.pmaDir)) {
fs.mkdirSync(this.pmaDir, { recursive: true })
}
try {
await downloadService.downloadAndExtract(this.downloadUrl, this.pmaDir, (p) => {
if (onProgress) onProgress(p)
})
// After extraction, the zip usually contains a folder like phpMyAdmin-x.y.z-all-languages
// We should move contents to the root of this.pmaDir if they are nested
const items = fs.readdirSync(this.pmaDir)
if (items.length === 1 && fs.statSync(path.join(this.pmaDir, items[0])).isDirectory()) {
const nestedDir = path.join(this.pmaDir, items[0])
const nestedItems = fs.readdirSync(nestedDir)
for (const item of nestedItems) {
fs.renameSync(path.join(nestedDir, item), path.join(this.pmaDir, item))
}
fs.rmdirSync(nestedDir)
}
// Generate config.inc.php
this.generateConfig(mysqlPort)
return true
} catch (error) {
console.error('phpMyAdmin setup error:', error)
return false
}
}
private generateConfig(mysqlPort: string = '3306') {
const configPath = path.join(this.pmaDir, 'config.inc.php')
const blowfishSecret = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
const configContent = `<?php
$cfg['blowfish_secret'] = '${blowfishSecret}';
$i = 0;
$i++;
$cfg['Servers'][$i]['auth_type'] = 'config';
$cfg['Servers'][$i]['host'] = '127.0.0.1:${mysqlPort}';
$cfg['Servers'][$i]['user'] = 'root';
$cfg['Servers'][$i]['password'] = '';
$cfg['Servers'][$i]['extension'] = 'mysqli';
$cfg['Servers'][$i]['AllowNoPassword'] = true;
$cfg['Lang'] = '';
?>`
fs.writeFileSync(configPath, configContent)
}
getPath(): string {
return this.pmaDir
}
}
export const phpMyAdminService = new PhpMyAdminService()
+58
View File
@@ -0,0 +1,58 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
interface PortMap {
php: Record<string, number>;
mysql: Record<string, number>;
}
export class PortMappingService {
private configFile: string
private portMap: PortMap = { php: {}, mysql: {} }
private nextPhpPort = 9001
private nextMysqlPort = 3306
constructor() {
this.configFile = path.join(app.getPath('userData'), 'port_mappings.json')
this.load()
}
private load() {
if (fs.existsSync(this.configFile)) {
try {
this.portMap = JSON.parse(fs.readFileSync(this.configFile, 'utf-8'))
const phpPorts = Object.values(this.portMap.php)
if (phpPorts.length > 0) this.nextPhpPort = Math.max(...phpPorts) + 1
const mysqlPorts = Object.values(this.portMap.mysql)
if (mysqlPorts.length > 0) this.nextMysqlPort = Math.max(...mysqlPorts) + 1
} catch (e) {
console.error('Failed to load port mappings', e)
}
}
}
private save() {
fs.writeFileSync(this.configFile, JSON.stringify(this.portMap, null, 2))
}
getPhpPort(version: string): number {
if (!this.portMap.php[version]) {
this.portMap.php[version] = this.nextPhpPort++
this.save()
}
return this.portMap.php[version]
}
getMySqlPort(version: string): number {
if (!this.portMap.mysql[version]) {
this.portMap.mysql[version] = this.nextMysqlPort++
this.save()
}
return this.portMap.mysql[version]
}
}
export const portMappingService = new PortMappingService()
+9
View File
@@ -7,6 +7,7 @@ export interface Project {
name: string name: string
path: string path: string
phpVersion: string phpVersion: string
mySqlVersion: string
host: string host: string
} }
@@ -49,6 +50,14 @@ export class ProjectService {
return newProject return newProject
} }
updateProject(project: Project) {
const index = this.projects.findIndex(p => p.id === project.id)
if (index !== -1) {
this.projects[index] = project
this.saveProjects()
}
}
removeProject(id: string) { removeProject(id: string) {
this.projects = this.projects.filter(p => p.id !== id) this.projects = this.projects.filter(p => p.id !== id)
this.saveProjects() this.saveProjects()
+5 -1
View File
@@ -12,6 +12,7 @@ const api = {
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'), getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
addProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:add', project), addProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:add', project),
removeProject: (id: string) => electronAPI.ipcRenderer.invoke('projects:remove', id), removeProject: (id: string) => electronAPI.ipcRenderer.invoke('projects:remove', id),
updateProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:update', project),
getPhpVersions: () => electronAPI.ipcRenderer.invoke('binaries:list-php'), getPhpVersions: () => electronAPI.ipcRenderer.invoke('binaries:list-php'),
downloadPhpVersion: (id: string) => electronAPI.ipcRenderer.invoke('binaries:download-php', id), downloadPhpVersion: (id: string) => electronAPI.ipcRenderer.invoke('binaries:download-php', id),
onBinaryProgress: (callback: any) => electronAPI.ipcRenderer.on('binaries:progress', (_event, data) => callback(data)), onBinaryProgress: (callback: any) => electronAPI.ipcRenderer.on('binaries:progress', (_event, data) => callback(data)),
@@ -23,7 +24,10 @@ const api = {
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings), saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings),
getNginxVersions: () => electronAPI.ipcRenderer.invoke('nginx:versions'), getNginxVersions: () => electronAPI.ipcRenderer.invoke('nginx:versions'),
downloadNginxVersion: (id: string) => electronAPI.ipcRenderer.invoke('nginx:download', id), downloadNginxVersion: (id: string) => electronAPI.ipcRenderer.invoke('nginx:download', id),
onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data)) onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data)),
pmaStatus: () => electronAPI.ipcRenderer.invoke('pma:status'),
pmaSetup: (mysqlPort: string) => electronAPI.ipcRenderer.invoke('pma:setup', mysqlPort),
onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data))
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to
+260 -130
View File
@@ -57,7 +57,9 @@ import {
Terminal as TerminalIcon, Terminal as TerminalIcon,
ContentCopy as CopyIcon, ContentCopy as CopyIcon,
Close as CloseIcon, Close as CloseIcon,
ContentCopy as ContentCopyIcon ContentCopy as ContentCopyIcon,
OpenInNew as OpenIcon,
Edit as EditIcon
} from '@mui/icons-material' } from '@mui/icons-material'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -97,7 +99,8 @@ function App(): JSX.Element {
const [nginxVersions, setNginxVersions] = useState<any[]>([]) const [nginxVersions, setNginxVersions] = useState<any[]>([])
const [loadingVersions, setLoadingVersions] = useState(false) const [loadingVersions, setLoadingVersions] = useState(false)
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false) const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' }) const slugify = (text: string) => text.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
const [newProject, setNewProject] = useState<any>({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' })
const [settings, setSettings] = useState<AppSettings>({ const [settings, setSettings] = useState<AppSettings>({
nginxPort: '80', nginxPort: '80',
phpPort: '9001', phpPort: '9001',
@@ -114,6 +117,9 @@ function App(): JSX.Element {
const [logDialogOpen, setLogDialogOpen] = useState(false) const [logDialogOpen, setLogDialogOpen] = useState(false)
const [selectedService, setSelectedService] = useState('') const [selectedService, setSelectedService] = useState('')
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const [pmaInstalled, setPmaInstalled] = useState(false)
const [pmaLoading, setPmaLoading] = useState(false)
const [pmaProgress, setPmaProgress] = useState(0)
const fetchStatus = async () => { const fetchStatus = async () => {
if (window.api && window.api.getServiceStatus) { if (window.api && window.api.getServiceStatus) {
@@ -174,23 +180,39 @@ function App(): JSX.Element {
} }
useEffect(() => { useEffect(() => {
if (isProjectDialogOpen && !newProject.phpVersion && phpVersions.length > 0) { if (isProjectDialogOpen && !newProject.id) {
const installed = phpVersions.find(v => v.status === 'installed') const updates: Partial<typeof newProject> = {}
if (installed) { if (!newProject.phpVersion && phpVersions.length > 0) {
setNewProject(prev => ({ ...prev, phpVersion: installed.version })) const installedPhp = phpVersions.find(v => v.status === 'installed')
if (installedPhp) updates.phpVersion = installedPhp.version
}
if (!newProject.mySqlVersion && mySqlVersions.length > 0) {
const installedMysql = mySqlVersions.find(v => v.status === 'installed')
if (installedMysql) updates.mySqlVersion = installedMysql.version
}
if (Object.keys(updates).length > 0) {
setNewProject(prev => ({ ...prev, ...updates }))
} }
} }
}, [isProjectDialogOpen, phpVersions]) }, [isProjectDialogOpen, phpVersions, mySqlVersions])
useEffect(() => { useEffect(() => {
if (!window.api) return if (!window.api) return
const fetchPmaStatus = async () => {
if (window.api && window.api.pmaStatus) {
const installed = await window.api.pmaStatus()
setPmaInstalled(installed)
}
}
fetchStatus() fetchStatus()
fetchProjects() fetchProjects()
fetchPhpVersions() fetchPhpVersions()
fetchMySqlVersions() fetchMySqlVersions()
fetchNginxVersions() fetchNginxVersions()
fetchSettings() fetchSettings()
fetchPmaStatus()
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => { const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
if (!data) return if (!data) return
@@ -210,6 +232,10 @@ function App(): JSX.Element {
setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v)) setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
}) : null }) : null
const removePmaListener = window.api.onPmaProgress ? window.api.onPmaProgress((p: number) => {
setPmaProgress(p)
}) : null
const interval = setInterval(() => { const interval = setInterval(() => {
fetchStatus() fetchStatus()
fetchProjects() fetchProjects()
@@ -231,6 +257,7 @@ function App(): JSX.Element {
if (typeof removePhpListener === 'function') removePhpListener() if (typeof removePhpListener === 'function') removePhpListener()
if (typeof removeMySqlListener === 'function') removeMySqlListener() if (typeof removeMySqlListener === 'function') removeMySqlListener()
if (typeof removeNginxListener === 'function') removeNginxListener() if (typeof removeNginxListener === 'function') removeNginxListener()
if (typeof removePmaListener === 'function') removePmaListener()
} }
}, [logDialogOpen, selectedService]) }, [logDialogOpen, selectedService])
@@ -243,9 +270,11 @@ function App(): JSX.Element {
const handleSelectPath = async () => { const handleSelectPath = async () => {
if (window.api && window.api.selectDirectory) { if (window.api && window.api.selectDirectory) {
const path = await window.api.selectDirectory() const pathStr = await window.api.selectDirectory()
if (path) { if (pathStr) {
setNewProject(prev => ({ ...prev, path })) const folderName = pathStr.split(/[/\\]/).pop() || ''
const slug = slugify(folderName)
setNewProject(prev => ({ ...prev, path: pathStr, name: folderName, host: slug }))
} }
} }
} }
@@ -331,10 +360,14 @@ function App(): JSX.Element {
const handleAddProject = async () => { const handleAddProject = async () => {
if (!window.api) return if (!window.api) return
await window.api.addProject(newProject) if (newProject.id) {
await window.api.updateProject(newProject)
} else {
await window.api.addProject(newProject)
}
setIsProjectDialogOpen(false) setIsProjectDialogOpen(false)
setNewProject({ name: '', path: '', phpVersion: '', host: 'localhost' }) setNewProject({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' })
setNotification({ open: true, message: 'Proje başarıyla eklendi.', severity: 'success' }) setNotification({ open: true, message: newProject.id ? 'Proje güncellendi.' : 'Proje başarıyla eklendi.', severity: 'success' })
fetchProjects() fetchProjects()
} }
@@ -378,6 +411,27 @@ function App(): JSX.Element {
fetchNginxVersions() fetchNginxVersions()
} }
const handleSetupPma = async () => {
if (!window.api) return
setPmaLoading(true)
setPmaProgress(0)
try {
const success = await window.api.pmaSetup()
if (success) {
setPmaInstalled(true)
setNotification({ open: true, message: 'phpMyAdmin başarıyla kuruldu.', severity: 'success' })
} else {
setNotification({ open: true, message: 'phpMyAdmin kurulumu sırasında hata oluştu.', severity: 'error' })
}
} finally {
setPmaLoading(false)
}
}
const handleOpenPma = () => {
window.open(`http://localhost:${settings.nginxPort}/phpmyadmin`, '_blank')
}
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed') const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
return ( return (
@@ -501,51 +555,97 @@ function App(): JSX.Element {
)} )}
{settingsTab === 1 && ( {settingsTab === 1 && (
<Paper sx={{ bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}> <>
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}> <Paper sx={{ bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>MySQL Sürümleri</Typography> <Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
<Typography variant="body2" color="text.secondary">Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz.</Typography> <Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>MySQL Sürümleri</Typography>
</Box> <Typography variant="body2" color="text.secondary">Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz.</Typography>
<List sx={{ maxHeight: 500, overflow: 'auto' }}> </Box>
{mySqlVersions.length === 0 && !loadingVersions && ( <List sx={{ maxHeight: 500, overflow: 'auto' }}>
<ListItem><ListItemText primary="Yükleniyor veya liste boş..." /></ListItem> {mySqlVersions.length === 0 && !loadingVersions && (
<ListItem><ListItemText primary="Yükleniyor veya liste boş..." /></ListItem>
)}
{mySqlVersions.map((v) => (
<ListItem key={v.id} sx={{ py: 2 }}>
<ListItemIcon>
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{`MySQL ${v.version} (${v.cycle})`}
{v.description && (
<Chip label={v.description} size="small" variant="outlined" sx={{ fontSize: '0.7rem', height: 20 }} />
)}
</Box>
}
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={() => handleDownloadMySql(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>
<Paper sx={{ mt: 3, p: 2, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ p: 1.5, bgcolor: 'rgba(255, 255, 255, 0.05)', borderRadius: 2 }}>
<DbIcon color="primary" />
</Box>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>phpMyAdmin</Typography>
<Typography variant="body2" color="text.secondary">
{pmaInstalled ? 'Veritabanı yönetimi için phpMyAdmin hazır.' : 'Veritabanlarını web üzerinden yönetmek için phpMyAdmin kurun.'}
</Typography>
</Box>
<Box>
{!pmaInstalled ? (
<Button
variant="contained"
size="small"
onClick={handleSetupPma}
disabled={pmaLoading}
startIcon={pmaLoading ? <CircularProgress size={20} /> : <DownloadIcon />}
>
{pmaLoading ? 'KURULUYOR...' : 'KURULUMU BAŞLAT'}
</Button>
) : (
<Button
variant="outlined"
size="small"
onClick={handleOpenPma}
startIcon={<OpenIcon />}
>
</Button>
)}
</Box>
</Box>
{pmaLoading && (
<Box sx={{ mt: 2 }}>
<LinearProgress variant="determinate" value={pmaProgress} sx={{ height: 6, borderRadius: 3 }} />
<Typography variant="caption" sx={{ mt: 0.5, display: 'block', textAlign: 'center' }}>
İndiriliyor... %{Math.round(pmaProgress)}
</Typography>
</Box>
)} )}
{mySqlVersions.map((v) => ( </Paper>
<ListItem key={v.id} sx={{ py: 2 }}> </>
<ListItemIcon>
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{`MySQL ${v.version} (${v.cycle})`}
{v.description && (
<Chip label={v.description} size="small" variant="outlined" sx={{ fontSize: '0.7rem', height: 20 }} />
)}
</Box>
}
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={() => handleDownloadMySql(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 === 2 && ( {settingsTab === 2 && (
@@ -715,15 +815,23 @@ function App(): JSX.Element {
<Paper key={project.id} sx={{ mb: 2, bgcolor: 'rgba(255, 255, 255, 0.05)' }}> <Paper key={project.id} sx={{ mb: 2, bgcolor: 'rgba(255, 255, 255, 0.05)' }}>
<ListItem <ListItem
secondaryAction={ secondaryAction={
<IconButton edge="end" aria-label="delete" onClick={() => handleRemoveProject(project.id)}> <Box>
<DeleteIcon color="error" /> <IconButton edge="end" aria-label="edit" onClick={() => {
</IconButton> setNewProject(project)
setIsProjectDialogOpen(true)
}} sx={{ mr: 1 }}>
<EditIcon color="primary" />
</IconButton>
<IconButton edge="end" aria-label="delete" onClick={() => handleRemoveProject(project.id)}>
<DeleteIcon color="error" />
</IconButton>
</Box>
} }
> >
<ListItemIcon><ProjectIcon color="primary" /></ListItemIcon> <ListItemIcon><ProjectIcon color="primary" /></ListItemIcon>
<ListItemText <ListItemText
primary={project.name} primary={project.name}
secondary={`${project.path} - PHP: ${project.phpVersion} - Host: ${project.host}`} secondary={`${project.path} - PHP: ${project.phpVersion} | MySQL: ${project.mySqlVersion || 'Yok'} - Host: ${project.host}`}
/> />
</ListItem> </ListItem>
</Paper> </Paper>
@@ -856,8 +964,11 @@ function App(): JSX.Element {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Dialog open={isProjectDialogOpen} onClose={() => setIsProjectDialogOpen(false)} fullWidth maxWidth="sm"> <Dialog open={isProjectDialogOpen} onClose={() => {
<DialogTitle>Yeni Proje Ekle</DialogTitle> setIsProjectDialogOpen(false)
setNewProject({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' })
}} fullWidth maxWidth="sm">
<DialogTitle>{newProject.id ? 'Proje Düzenle' : 'Yeni Proje Ekle'}</DialogTitle>
<DialogContent> <DialogContent>
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField <TextField
@@ -887,80 +998,99 @@ function App(): JSX.Element {
value={newProject.host} value={newProject.host}
onChange={(e) => setNewProject({ ...newProject, host: e.target.value })} onChange={(e) => setNewProject({ ...newProject, host: e.target.value })}
/> />
<FormControl fullWidth disabled={installedPhpVersions.length === 0}> <Stack direction="row" spacing={2} sx={{ width: '100%' }}>
<InputLabel id="php-version-label">PHP Sürümü</InputLabel> <FormControl fullWidth disabled={installedPhpVersions.length === 0}>
<Select <InputLabel id="php-version-label">PHP Sürümü</InputLabel>
labelId="php-version-label" <Select
value={newProject.phpVersion} labelId="php-version-label"
label="PHP Sürümü" value={newProject.phpVersion}
onChange={(e) => setNewProject({ ...newProject, phpVersion: e.target.value })} label="PHP Sürümü"
> onChange={(e) => setNewProject({ ...newProject, phpVersion: e.target.value })}
{installedPhpVersions.length > 0 ? ( >
installedPhpVersions.map(v => ( {installedPhpVersions.length > 0 ? (
<MenuItem key={v.id} value={v.version}>PHP {v.version}</MenuItem> installedPhpVersions.map(v => (
)) <MenuItem key={v.id} value={v.version}>PHP {v.version}</MenuItem>
) : ( ))
<MenuItem disabled value="">Önce PHP sürümü indirmelisiniz</MenuItem> ) : (
)} <MenuItem disabled value="">Önce PHP sürümü indirmelisiniz</MenuItem>
</Select> )}
</FormControl> </Select>
</FormControl>
<FormControl fullWidth disabled={mySqlVersions.filter(v => v.status === 'installed').length === 0}>
<InputLabel id="mysql-version-label">MySQL Sürümü</InputLabel>
<Select
labelId="mysql-version-label"
value={newProject.mySqlVersion || ''}
label="MySQL Sürümü"
onChange={(e) => setNewProject({ ...newProject, mySqlVersion: e.target.value })}
>
{mySqlVersions.filter(v => v.status === 'installed').length > 0 ? (
mySqlVersions.filter(v => v.status === 'installed').map(v => (
<MenuItem key={v.id} value={v.version}>MySQL {v.version}</MenuItem>
))
) : (
<MenuItem disabled value="">Önce MySQL sürümü indirmelisiniz</MenuItem>
)}
</Select>
</FormControl>
</Stack>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setIsProjectDialogOpen(false)}>İptal</Button> <Button onClick={() => {
setIsProjectDialogOpen(false)
setNewProject({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' })
}}>İptal</Button>
<Button <Button
onClick={handleAddProject} onClick={handleAddProject}
variant="contained" variant="contained"
disabled={!newProject.name || !newProject.path || installedPhpVersions.length === 0} disabled={!newProject.name || !newProject.path || installedPhpVersions.length === 0 || mySqlVersions.filter(v => v.status === 'installed').length === 0}
> >
Ekle </Dialog>
</Button>
</DialogActions>
</Dialog>
{/* Log Dialog */} {/* Log Dialog */}
<Dialog open={logDialogOpen} onClose={() => setLogDialogOpen(false)} fullWidth maxWidth="md"> <Dialog open={logDialogOpen} onClose={() => setLogDialogOpen(false)} fullWidth maxWidth="md">
<DialogTitle sx={{ bgcolor: '#1e1e1e', color: '#fff', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <DialogTitle sx={{ bgcolor: '#1e1e1e', color: '#fff', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TerminalIcon /> <TerminalIcon />
{selectedService.toUpperCase()} Konsol Çıktısı {selectedService.toUpperCase()} Konsol Çıktısı
</Box> </Box>
<Box> <Box>
<IconButton size="small" sx={{ color: '#fff' }} onClick={() => navigator.clipboard.writeText(logs.join('\n'))}> <IconButton size="small" sx={{ color: '#fff' }} onClick={() => navigator.clipboard.writeText(logs.join('\n'))}>
<CopyIcon fontSize="small" /> <CopyIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent sx={{ bgcolor: '#000', p: 0 }}> <DialogContent sx={{ bgcolor: '#000', p: 0 }}>
<Box <Box
sx={{ sx={{
p: 2, p: 2,
fontFamily: 'Consolas, monospace', fontFamily: 'Consolas, monospace',
fontSize: '0.85rem', fontSize: '0.85rem',
color: '#00ff00', color: '#00ff00',
height: '400px', height: '400px',
overflowY: 'auto', overflowY: 'auto',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column'
}} }}
> >
{logs.length === 0 ? ( {logs.length === 0 ? (
<Typography variant="caption" sx={{ color: '#666' }}>Henüz log kaydı yok...</Typography> <Typography variant="caption" sx={{ color: '#666' }}>Henüz log kaydı yok...</Typography>
) : ( ) : (
logs.map((line, i) => ( logs.map((line, i) => (
<Box key={i} sx={{ mb: 0.5, borderLeft: '2px solid #333', pl: 1 }}> <Box key={i} sx={{ mb: 0.5, borderLeft: '2px solid #333', pl: 1 }}>
{line} {line}
</Box> </Box>
)) ))
)} )}
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions sx={{ bgcolor: '#1e1e1e' }}> <DialogActions sx={{ bgcolor: '#1e1e1e' }}>
<Button onClick={() => setLogDialogOpen(false)} variant="contained" size="small">Kapat</Button> <Button onClick={() => setLogDialogOpen(false)} variant="contained" size="small">Kapat</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</Box> </Box>
) )
} }
export default App export default App