feat: implement IPC handlers for service management and add MySQL operation service support

This commit is contained in:
Ümit Tunç
2026-03-30 07:17:39 +03:00
parent bd566eea1c
commit 47c322294c
5 changed files with 611 additions and 3 deletions
+83
View File
@@ -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}` }
}
})
} }
+136
View File
@@ -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()
+9 -1
View File
@@ -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
+37 -2
View File
@@ -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 }}>
@@ -1085,7 +1113,7 @@ function App(): JSX.Element {
</Box> </Box>
)} )}
</Container> </Container>
</Box> </Box >
<Snackbar <Snackbar
open={notification.open} open={notification.open}
@@ -1295,8 +1323,15 @@ function App(): JSX.Element {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</Box>
<MySqlImportWizard
open={isMySqlWizardOpen}
onClose={() => setIsMySqlWizardOpen(false)}
mySqlVersions={mySqlVersions}
/>
</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