feat: implement IPC handlers for service management and add MySQL operation service support
This commit is contained in:
@@ -10,6 +10,7 @@ import { nginxManagerService } from '../services/NginxManagerService'
|
|||||||
import { phpMyAdminService } from '../services/PhpMyAdminService'
|
import { phpMyAdminService } from '../services/PhpMyAdminService'
|
||||||
import { portMappingService } from '../services/PortMappingService'
|
import { portMappingService } from '../services/PortMappingService'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { mySqlOperationService } from '../services/MySqlOperationService'
|
||||||
|
|
||||||
function findExecutable(dir: string, name: string): string | null {
|
function findExecutable(dir: string, name: string): string | null {
|
||||||
if (!fs.existsSync(dir)) return null
|
if (!fs.existsSync(dir)) return null
|
||||||
@@ -349,6 +350,17 @@ export function registerIpcHandlers(): void {
|
|||||||
return result.filePaths[0]
|
return result.filePaths[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('dialog:select-file', async (_event, filters: any[]) => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: filters || [
|
||||||
|
{ name: 'SQL & Archives', extensions: ['sql', 'zip', 'gz', 'tar'] }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (result.canceled) return null
|
||||||
|
return result.filePaths[0]
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
||||||
return downloadService.downloadAndExtract(url, name, (p) => {
|
return downloadService.downloadAndExtract(url, name, (p) => {
|
||||||
// TODO: Emit progress to renderer via webContents.send
|
// TODO: Emit progress to renderer via webContents.send
|
||||||
@@ -411,4 +423,75 @@ export function registerIpcHandlers(): void {
|
|||||||
return { success: false, message: `Sıfırlama hatası: ${error.message}` }
|
return { success: false, message: `Sıfırlama hatası: ${error.message}` }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('mysql:import-wizard:check-db', async (_event, { versionId, dbName }) => {
|
||||||
|
const versions = await mySqlManagerService.getVersions()
|
||||||
|
const v = versions.find(v => v.id === versionId)
|
||||||
|
if (!v) return { exists: false, error: 'MySQL version not found' }
|
||||||
|
|
||||||
|
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
|
||||||
|
const binPath = findExecutable(binDir, 'mysql.exe')
|
||||||
|
if (!binPath) return { exists: false, error: 'mysql.exe not found' }
|
||||||
|
|
||||||
|
const exists = await mySqlOperationService.checkDatabaseExists({
|
||||||
|
binPath,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: v.port,
|
||||||
|
rootUser: 'root',
|
||||||
|
rootPass: '' // Assuming root has no password by default in this setup
|
||||||
|
}, dbName)
|
||||||
|
|
||||||
|
return { exists }
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('mysql:import-wizard:start', async (event, { versionId, filePath, dbName, user, pass, overwrite }) => {
|
||||||
|
const versions = await mySqlManagerService.getVersions()
|
||||||
|
const v = versions.find(v => v.id === versionId)
|
||||||
|
if (!v) return { success: false, message: 'MySQL version not found' }
|
||||||
|
|
||||||
|
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
|
||||||
|
const mysqlBin = findExecutable(binDir, 'mysql.exe')
|
||||||
|
if (!mysqlBin) return { success: false, message: 'mysql.exe not found' }
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
binPath: mysqlBin,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: v.port,
|
||||||
|
rootUser: 'root',
|
||||||
|
rootPass: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = (msg: string) => event.sender.send('mysql:import:log', msg)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (overwrite) {
|
||||||
|
log(`Dropping existing database: ${dbName}`)
|
||||||
|
await mySqlOperationService.dropDatabase(config, dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Creating database and user: ${dbName} / ${user}`)
|
||||||
|
const createRes = await mySqlOperationService.createDatabaseAndUser(config, dbName, user, pass)
|
||||||
|
if (!createRes.success) return createRes
|
||||||
|
|
||||||
|
let sqlFiles: string[] = []
|
||||||
|
if (filePath.toLowerCase().endsWith('.sql')) {
|
||||||
|
sqlFiles = [filePath]
|
||||||
|
} else {
|
||||||
|
log(`Extracting archive: ${path.basename(filePath)}`)
|
||||||
|
const extractDir = path.join(app.getPath('temp'), `mysql_import_${Date.now()}`)
|
||||||
|
sqlFiles = await mySqlOperationService.extractArchive(filePath, extractDir)
|
||||||
|
if (sqlFiles.length === 0) return { success: false, message: 'No .sql files found in archive.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sqlFile of sqlFiles) {
|
||||||
|
log(`Importing: ${path.basename(sqlFile)}`)
|
||||||
|
const importRes = await mySqlOperationService.importSql(config, dbName, sqlFile, (msg) => log(msg))
|
||||||
|
if (!importRes.success) return importRes
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: 'Import completed successfully.' }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `Import error: ${error.message}` }
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { spawn } from 'child_process'
|
||||||
|
import decompress from 'decompress'
|
||||||
|
|
||||||
|
export interface MySqlConfig {
|
||||||
|
binPath: string
|
||||||
|
host: string
|
||||||
|
port: string
|
||||||
|
rootUser: string
|
||||||
|
rootPass: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MySqlOperationService {
|
||||||
|
async extractArchive(archivePath: string, targetDir: string): Promise<string[]> {
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
await decompress(archivePath, targetDir)
|
||||||
|
|
||||||
|
// Find all .sql files in the extracted directory
|
||||||
|
const sqlFiles: string[] = []
|
||||||
|
const findSqlFiles = (dir: string) => {
|
||||||
|
const files = fs.readdirSync(dir)
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(dir, file)
|
||||||
|
if (fs.statSync(fullPath).isDirectory()) {
|
||||||
|
findSqlFiles(fullPath)
|
||||||
|
} else if (file.toLowerCase().endsWith('.sql')) {
|
||||||
|
sqlFiles.push(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findSqlFiles(targetDir)
|
||||||
|
return sqlFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
async runQuery(config: MySqlConfig, query: string, database?: string): Promise<{ success: boolean; output: string; error?: string }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const args = [
|
||||||
|
`--host=${config.host}`,
|
||||||
|
`--port=${config.port}`,
|
||||||
|
`--user=${config.rootUser}`,
|
||||||
|
database ? `--database=${database}` : null
|
||||||
|
].filter(Boolean) as string[]
|
||||||
|
|
||||||
|
if (config.rootPass) {
|
||||||
|
args.push(`--password=${config.rootPass}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('-e', query)
|
||||||
|
|
||||||
|
const child = spawn(config.binPath, args, { windowsHide: true })
|
||||||
|
let output = ''
|
||||||
|
let error = ''
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => output += data.toString())
|
||||||
|
child.stderr.on('data', (data) => error += data.toString())
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ success: true, output })
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, output, error })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkDatabaseExists(config: MySqlConfig, dbName: string): Promise<boolean> {
|
||||||
|
const result = await this.runQuery(config, `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`)
|
||||||
|
return result.success && result.output.includes(dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDatabaseAndUser(config: MySqlConfig, dbName: string, user: string, pass: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
const queries = [
|
||||||
|
`CREATE DATABASE IF NOT EXISTS \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`,
|
||||||
|
`CREATE USER IF NOT EXISTS '${user}'@'%' IDENTIFIED BY '${pass}';`,
|
||||||
|
`GRANT ALL PRIVILEGES ON \`${dbName}\`.* TO '${user}'@'%';`,
|
||||||
|
`FLUSH PRIVILEGES;`
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const query of queries) {
|
||||||
|
const result = await this.runQuery(config, query)
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, message: `Query failed: ${query}\nError: ${result.error}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: 'Database and user created successfully.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
async dropDatabase(config: MySqlConfig, dbName: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
const result = await this.runQuery(config, `DROP DATABASE IF EXISTS \`${dbName}\``)
|
||||||
|
return { success: result.success, message: result.success ? 'Database dropped' : `Drop failed: ${result.error}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
async importSql(config: MySqlConfig, dbName: string, sqlFilePath: string, onProgress?: (log: string) => void): Promise<{ success: boolean; message: string }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const mysqlPath = config.binPath
|
||||||
|
const args = [
|
||||||
|
`--host=${config.host}`,
|
||||||
|
`--port=${config.port}`,
|
||||||
|
`--user=${config.rootUser}`,
|
||||||
|
`--database=${dbName}`
|
||||||
|
]
|
||||||
|
|
||||||
|
if (config.rootPass) {
|
||||||
|
args.push(`--password=${config.rootPass}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using shell true to use redirecting input
|
||||||
|
const command = `"${mysqlPath}" ${args.join(' ')} < "${sqlFilePath}"`
|
||||||
|
onProgress?.(`Executing: mysql ${args.join(' ')} < ${path.basename(sqlFilePath)}`)
|
||||||
|
|
||||||
|
const child = spawn('cmd.exe', ['/c', command], { windowsHide: true })
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => onProgress?.(data.toString()))
|
||||||
|
child.stderr.on('data', (data) => onProgress?.(data.toString()))
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
onProgress?.('Import completed successfully.')
|
||||||
|
resolve({ success: true, message: 'Import completed successfully.' })
|
||||||
|
} else {
|
||||||
|
onProgress?.(`Import failed with exit code ${code}`)
|
||||||
|
resolve({ success: false, message: `Import failed (Exit Code: ${code})` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mySqlOperationService = new MySqlOperationService()
|
||||||
@@ -27,7 +27,15 @@ const api = {
|
|||||||
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'),
|
pmaStatus: () => electronAPI.ipcRenderer.invoke('pma:status'),
|
||||||
pmaSetup: (mysqlPort: string) => electronAPI.ipcRenderer.invoke('pma:setup', mysqlPort),
|
pmaSetup: (mysqlPort: string) => electronAPI.ipcRenderer.invoke('pma:setup', mysqlPort),
|
||||||
onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data))
|
onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data)),
|
||||||
|
selectFile: (filters?: any[]) => electronAPI.ipcRenderer.invoke('dialog:select-file', filters),
|
||||||
|
checkMySqlDb: (versionId: string, dbName: string) => electronAPI.ipcRenderer.invoke('mysql:import-wizard:check-db', { versionId, dbName }),
|
||||||
|
startMySqlImport: (data: any) => electronAPI.ipcRenderer.invoke('mysql:import-wizard:start', data),
|
||||||
|
onMySqlImportLog: (callback: any) => {
|
||||||
|
const listener = (_event, msg) => callback(msg)
|
||||||
|
electronAPI.ipcRenderer.on('mysql:import:log', listener)
|
||||||
|
return () => electronAPI.ipcRenderer.removeListener('mysql:import:log', listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import MySqlImportWizard from './components/MySqlImportWizard'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -129,6 +130,7 @@ function App(): JSX.Element {
|
|||||||
const [pmaInstalled, setPmaInstalled] = useState(false)
|
const [pmaInstalled, setPmaInstalled] = useState(false)
|
||||||
const [pmaLoading, setPmaLoading] = useState(false)
|
const [pmaLoading, setPmaLoading] = useState(false)
|
||||||
const [pmaProgress, setPmaProgress] = useState(0)
|
const [pmaProgress, setPmaProgress] = useState(0)
|
||||||
|
const [isMySqlWizardOpen, setIsMySqlWizardOpen] = useState(false)
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
if (window.api && window.api.getServiceStatus) {
|
if (window.api && window.api.getServiceStatus) {
|
||||||
@@ -958,9 +960,35 @@ function App(): JSX.Element {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2, border: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
|
||||||
|
<Box sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.05)', borderRadius: 3 }}>
|
||||||
|
<DbIcon color="primary" sx={{ fontSize: 32 }} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>MySQL Dump/Import Sihirbazı</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Seçtiğiniz bir .sql veya arşiv dosyasını hızlıca veritabanına yükleyin.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={() => setIsMySqlWizardOpen(true)}
|
||||||
|
startIcon={<DbIcon />}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
SİHİRBAZI BAŞLAT
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{activeTab === 'projects' && (
|
{activeTab === 'projects' && (
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
@@ -1295,8 +1323,15 @@ function App(): JSX.Element {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<MySqlImportWizard
|
||||||
|
open={isMySqlWizardOpen}
|
||||||
|
onClose={() => setIsMySqlWizardOpen(false)}
|
||||||
|
mySqlVersions={mySqlVersions}
|
||||||
|
/>
|
||||||
</Box >
|
</Box >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,346 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Stepper,
|
||||||
|
Step,
|
||||||
|
StepLabel,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Paper,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
CircularProgress,
|
||||||
|
Divider,
|
||||||
|
Stack
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
CloudUpload as FileIcon,
|
||||||
|
Refresh as RefreshIcon,
|
||||||
|
CheckCircle as SuccessIcon,
|
||||||
|
Error as ErrorIcon
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
|
||||||
|
interface MySqlImportWizardProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
mySqlVersions: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
'Veri Seçimi',
|
||||||
|
'Sunucu Seçimi',
|
||||||
|
'Veritabanı Yapılandırması',
|
||||||
|
'Özet ve İşlem'
|
||||||
|
]
|
||||||
|
|
||||||
|
const MySqlImportWizard: React.FC<MySqlImportWizardProps> = ({ open, onClose, mySqlVersions }) => {
|
||||||
|
const [activeStep, setActiveStep] = useState(0)
|
||||||
|
const [filePath, setFilePath] = useState('')
|
||||||
|
const [selectedVersion, setSelectedVersion] = useState('')
|
||||||
|
const [dbConfig, setDbConfig] = useState({
|
||||||
|
dbName: '',
|
||||||
|
user: '',
|
||||||
|
pass: ''
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
|
const logEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [importResult, setImportResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setActiveStep(0)
|
||||||
|
setFilePath('')
|
||||||
|
setLogs([])
|
||||||
|
setImportResult(null)
|
||||||
|
const installedVersion = mySqlVersions.find(v => v.status === 'installed')
|
||||||
|
if (installedVersion) setSelectedVersion(installedVersion.id)
|
||||||
|
|
||||||
|
// Generate random credentials
|
||||||
|
const randomStr = Math.random().toString(36).substring(2, 8)
|
||||||
|
setDbConfig({
|
||||||
|
dbName: `db_${randomStr}`,
|
||||||
|
user: `user_${randomStr}`,
|
||||||
|
pass: Math.random().toString(36).substring(2, 12)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [open, mySqlVersions])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logEndRef.current) {
|
||||||
|
logEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}, [logs])
|
||||||
|
|
||||||
|
const handleSelectFile = async () => {
|
||||||
|
const path = await window.api.selectFile([
|
||||||
|
{ name: 'SQL & Archives', extensions: ['sql', 'zip', 'gz', 'tar'] }
|
||||||
|
])
|
||||||
|
if (path) {
|
||||||
|
setFilePath(path)
|
||||||
|
// Try to guess DB name from file name
|
||||||
|
const fileName = path.split(/[/\\]/).pop()?.split('.')[0] || ''
|
||||||
|
if (fileName && !dbConfig.dbName.startsWith('db_')) {
|
||||||
|
setDbConfig(prev => ({ ...prev, dbName: fileName }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = () => setActiveStep(prev => prev + 1)
|
||||||
|
const prevStep = () => setActiveStep(prev => prev - 1)
|
||||||
|
|
||||||
|
const handleStep2Next = () => {
|
||||||
|
if (!selectedVersion) {
|
||||||
|
Swal.fire('Hata', 'Lütfen bir MySQL sunucusu seçin.', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextStep()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStep3Next = async () => {
|
||||||
|
if (!dbConfig.dbName || !dbConfig.user || !dbConfig.pass) {
|
||||||
|
Swal.fire('Hata', 'Lütfen tüm alanları doldurun.', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const { exists } = await window.api.checkMySqlDb(selectedVersion, dbConfig.dbName)
|
||||||
|
if (exists) {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: 'Veritabanı Mevcut!',
|
||||||
|
text: `'${dbConfig.dbName}' veritabanı zaten var. Üzerine yazmak istiyor musunuz? Mevcut tüm veriler silinecektir!`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Evet, Üzerine Yaz',
|
||||||
|
cancelButtonText: 'Hayır, Vazgeç',
|
||||||
|
confirmButtonColor: '#d33'
|
||||||
|
})
|
||||||
|
if (!result.isConfirmed) return
|
||||||
|
}
|
||||||
|
nextStep()
|
||||||
|
} catch (err: any) {
|
||||||
|
Swal.fire('Hata', err.message, 'error')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startImport = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setLogs(['[Sistem] İşlem başlatılıyor...'])
|
||||||
|
|
||||||
|
const removeListener = window.api.onMySqlImportLog((msg: string) => {
|
||||||
|
setLogs(prev => [...prev, msg])
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.api.startMySqlImport({
|
||||||
|
versionId: selectedVersion,
|
||||||
|
filePath,
|
||||||
|
dbName: dbConfig.dbName,
|
||||||
|
user: dbConfig.user,
|
||||||
|
pass: dbConfig.pass,
|
||||||
|
overwrite: true // Logic handled by checkMySqlDb before
|
||||||
|
})
|
||||||
|
setImportResult(result)
|
||||||
|
} catch (err: any) {
|
||||||
|
setImportResult({ success: false, message: err.message })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
if (removeListener) removeListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderStepContent = (step: number) => {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
||||||
|
<Typography variant="body1" gutterBottom>Lütfen yüklenecek olan .sql dosyasını veya arşivini (.zip, .tar.gz) seçin.</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 3 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={filePath}
|
||||||
|
placeholder="Dosya seçilmedi..."
|
||||||
|
disabled
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<FileIcon color="primary" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button variant="contained" onClick={handleSelectFile} sx={{ minWidth: '120px' }}>SEÇ</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="body1" gutterBottom>Dosyanın hangi MySQL sunucusuna yükleneceğini seçin.</Typography>
|
||||||
|
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||||
|
<InputLabel>MySQL Sunucusu</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={selectedVersion}
|
||||||
|
label="MySQL Sunucusu"
|
||||||
|
onChange={(e) => setSelectedVersion(e.target.value)}
|
||||||
|
>
|
||||||
|
{mySqlVersions.filter(v => v.status === 'installed').map(v => (
|
||||||
|
<MenuItem key={v.id} value={v.id}>
|
||||||
|
MySQL {v.version} (Port: {v.port})
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
<Typography variant="body1">Veritabanı ve kullanıcı bilgilerini yapılandırın.</Typography>
|
||||||
|
<TextField
|
||||||
|
label="Veritabanı Adı"
|
||||||
|
fullWidth
|
||||||
|
value={dbConfig.dbName}
|
||||||
|
onChange={(e) => setDbConfig({ ...dbConfig, dbName: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Veritabanı Kullanıcısı"
|
||||||
|
fullWidth
|
||||||
|
value={dbConfig.user}
|
||||||
|
onChange={(e) => setDbConfig({ ...dbConfig, user: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Kullanıcı Şifresi"
|
||||||
|
fullWidth
|
||||||
|
value={dbConfig.pass}
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setDbConfig({ ...dbConfig, pass: e.target.value })}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton onClick={() => setDbConfig({ ...dbConfig, pass: Math.random().toString(36).substring(2, 12) })}>
|
||||||
|
<RefreshIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
{!importResult ? (
|
||||||
|
<>
|
||||||
|
<Paper sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.05)', borderRadius: 2 }}>
|
||||||
|
<Typography variant="subtitle2" color="primary" sx={{ fontWeight: 'bold' }}>Özet Bilgiler</Typography>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="body2"><strong>Dosya:</strong> {filePath.split(/[/\\]/).pop()}</Typography>
|
||||||
|
<Typography variant="body2"><strong>Sunucu:</strong> {selectedVersion}</Typography>
|
||||||
|
<Typography variant="body2"><strong>Veritabanı:</strong> {dbConfig.dbName}</Typography>
|
||||||
|
<Typography variant="body2"><strong>Kullanıcı:</strong> {dbConfig.user}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
{loading && (
|
||||||
|
<Box sx={{ mt: 3, p: 2, bgcolor: '#000', borderRadius: 2, maxHeight: '200px', overflowY: 'auto', fontFamily: 'monospace' }}>
|
||||||
|
{logs.map((log, i) => (
|
||||||
|
<Typography key={i} variant="caption" sx={{ display: 'block', color: log.includes('fail') ? 'error.main' : 'success.main' }}>
|
||||||
|
{log}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
<div ref={logEndRef} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||||
|
{importResult.success ? (
|
||||||
|
<>
|
||||||
|
<SuccessIcon color="success" sx={{ fontSize: 60 }} />
|
||||||
|
<Typography variant="h6" sx={{ mt: 2 }}>İşlem Başarılı!</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">{importResult.message}</Typography>
|
||||||
|
<Paper sx={{ p: 2, mt: 3, textAlign: 'left', bgcolor: 'rgba(0,180,0,0.1)' }}>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>Erişim Bilgileri:</Typography>
|
||||||
|
<Typography variant="body2"><strong>Host:</strong> localhost</Typography>
|
||||||
|
<Typography variant="body2"><strong>DB:</strong> {dbConfig.dbName}</Typography>
|
||||||
|
<Typography variant="body2"><strong>User:</strong> {dbConfig.user}</Typography>
|
||||||
|
<Typography variant="body2"><strong>Pass:</strong> {dbConfig.pass}</Typography>
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ErrorIcon color="error" sx={{ fontSize: 60 }} />
|
||||||
|
<Typography variant="h6" sx={{ mt: 2 }}>Hata Oluştu!</Typography>
|
||||||
|
<Typography variant="body2" color="error">{importResult.message}</Typography>
|
||||||
|
<Box sx={{ mt: 2, textAlign: 'left', bgcolor: '#000', p: 1, maxHeight: 150, overflowY: 'auto' }}>
|
||||||
|
{logs.slice(-5).map((l, i) => <Typography key={i} variant="caption" sx={{ color: '#fff', display: 'block' }}>{l}</Typography>)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return 'Bilinmeyen adım'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={loading ? undefined : onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>MySQL Hızlı Dump/Import Sihirbazı</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Stepper activeStep={activeStep} orientation="horizontal" sx={{ pt: 2, pb: 4 }}>
|
||||||
|
{steps.map((label) => (
|
||||||
|
<Step key={label}>
|
||||||
|
<StepLabel>{label}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
|
||||||
|
{renderStepContent(activeStep)}
|
||||||
|
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose} disabled={loading}>KAPAT</Button>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
{activeStep > 0 && activeStep < 3 && !importResult && (
|
||||||
|
<Button onClick={prevStep} disabled={loading}>GERİ</Button>
|
||||||
|
)}
|
||||||
|
{activeStep === 0 && (
|
||||||
|
<Button variant="contained" onClick={nextStep} disabled={!filePath}>İLERİ</Button>
|
||||||
|
)}
|
||||||
|
{activeStep === 1 && (
|
||||||
|
<Button variant="contained" onClick={handleStep2Next}>İLERİ</Button>
|
||||||
|
)}
|
||||||
|
{activeStep === 2 && (
|
||||||
|
<Button variant="contained" onClick={handleStep3Next} disabled={loading}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'ÖZETİ GÖR'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{activeStep === 3 && !importResult && (
|
||||||
|
<Button variant="contained" color="primary" onClick={startImport} disabled={loading}>
|
||||||
|
{loading ? <CircularProgress size={24} /> : 'IMPORTU BAŞLAT'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MySqlImportWizard
|
||||||
Reference in New Issue
Block a user