feat: implement MySQL operation service and add export wizard UI with localization support
This commit is contained in:
+54
-1
@@ -1,4 +1,4 @@
|
||||
import { ipcMain, app, dialog } from 'electron'
|
||||
import { ipcMain, app, dialog, shell } from 'electron'
|
||||
import fs from 'fs'
|
||||
import { processManager } from '../services/ProcessManager'
|
||||
import { configService } from '../services/ConfigService'
|
||||
@@ -393,6 +393,21 @@ export function registerIpcHandlers(): void {
|
||||
return result.filePaths[0]
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:save-file', async (_event, filters: any[]) => {
|
||||
const result = await dialog.showSaveDialog({
|
||||
filters: filters || [
|
||||
{ name: 'SQL File', extensions: ['sql'] },
|
||||
{ name: 'Compressed SQL', extensions: ['sql.gz'] }
|
||||
]
|
||||
})
|
||||
if (result.canceled) return null
|
||||
return result.filePath
|
||||
})
|
||||
|
||||
ipcMain.handle('shell:open-path', async (_event, pathStr: string) => {
|
||||
return await shell.openPath(pathStr)
|
||||
})
|
||||
|
||||
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
||||
return downloadService.downloadAndExtract(url, name, (p) => {
|
||||
// TODO: Emit progress to renderer via webContents.send
|
||||
@@ -646,4 +661,42 @@ export function registerIpcHandlers(): void {
|
||||
return { success: false, message: `Import error: ${error.message}` }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('mysql:export-wizard:list-dbs', async (_event, { versionId, rootPass }) => {
|
||||
const versions = await mySqlManagerService.getVersions()
|
||||
const v = versions.find(v => v.id === versionId)
|
||||
if (!v) return []
|
||||
|
||||
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
|
||||
const mysqlBin = findExecutable(binDir, 'mysql.exe')
|
||||
if (!mysqlBin) return []
|
||||
|
||||
return await mySqlOperationService.listDatabases({
|
||||
binPath: mysqlBin,
|
||||
host: '127.0.0.1',
|
||||
port: v.port,
|
||||
rootUser: 'root',
|
||||
rootPass: rootPass || ''
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('mysql:export-wizard:start', async (event, { versionId, dbName, savePath, compress, rootPass }) => {
|
||||
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 log = (msg: string) => event.sender.send('mysql:export:log', msg)
|
||||
|
||||
return await mySqlOperationService.exportSql({
|
||||
binPath: mysqlBin,
|
||||
host: '127.0.0.1',
|
||||
port: v.port,
|
||||
rootUser: 'root',
|
||||
rootPass: rootPass || ''
|
||||
}, dbName, savePath, compress, (msg) => log(msg))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,6 +162,68 @@ export class MySqlOperationService {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async listDatabases(config: MySqlConfig): Promise<string[]> {
|
||||
const result = await this.runQuery(config, 'SHOW DATABASES')
|
||||
if (!result.success) return []
|
||||
|
||||
return result.output
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && line !== 'Database' && !line.match(/^information_schema|performance_schema|mysql|sys$/i))
|
||||
}
|
||||
|
||||
async exportSql(config: MySqlConfig, dbName: string, targetPath: string, compress: boolean, onProgress?: (log: string) => void): Promise<{ success: boolean; message: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const dumpPath = config.binPath.replace(/mysql\.exe$/i, 'mysqldump.exe')
|
||||
const args = [
|
||||
`--host=${config.host}`,
|
||||
`--port=${config.port}`,
|
||||
`--user=${config.rootUser}`,
|
||||
dbName,
|
||||
'--routines',
|
||||
'--triggers'
|
||||
]
|
||||
|
||||
if (config.rootPass) {
|
||||
args.push(`--password=${config.rootPass}`)
|
||||
}
|
||||
|
||||
onProgress?.(`Executing: "${dumpPath}" ${args.join(' ')} > "${targetPath}"`)
|
||||
|
||||
const child = spawn(dumpPath, args, { windowsHide: true })
|
||||
const writeStream = fs.createWriteStream(targetPath)
|
||||
|
||||
if (compress) {
|
||||
const gzip = zlib.createGzip()
|
||||
child.stdout.pipe(gzip).pipe(writeStream)
|
||||
} else {
|
||||
child.stdout.pipe(writeStream)
|
||||
}
|
||||
|
||||
let errorOutput = ''
|
||||
child.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString()
|
||||
onProgress?.(data.toString())
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
onProgress?.(`Process error: ${err.message}`)
|
||||
resolve({ success: false, message: `Process error: ${err.message}` })
|
||||
})
|
||||
|
||||
child.on('close', (code) => {
|
||||
writeStream.end()
|
||||
if (code === 0) {
|
||||
onProgress?.('Export completed successfully.')
|
||||
resolve({ success: true, message: 'Export completed successfully.' })
|
||||
} else {
|
||||
onProgress?.(`Export failed with exit code ${code}`)
|
||||
resolve({ success: false, message: `Export failed: ${errorOutput}` })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const mySqlOperationService = new MySqlOperationService()
|
||||
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
import Swal from 'sweetalert2'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MySqlImportWizard from './components/MySqlImportWizard'
|
||||
import MySqlExportWizard from './components/MySqlExportWizard'
|
||||
import NginxCodeEditor from './components/NginxCodeEditor'
|
||||
|
||||
declare global {
|
||||
@@ -131,6 +132,7 @@ function App(): JSX.Element {
|
||||
const [pmaLoading, setPmaLoading] = useState(false)
|
||||
const [pmaProgress, setPmaProgress] = useState(0)
|
||||
const [isMySqlWizardOpen, setIsMySqlWizardOpen] = useState(false)
|
||||
const [isMySqlExportWizardOpen, setIsMySqlExportWizardOpen] = useState(false)
|
||||
const [isNginxConfigOpen, setIsNginxConfigOpen] = useState(false)
|
||||
const [nginxConfig, setNginxConfig] = useState('')
|
||||
const [isProjectNginxConfigOpen, setIsProjectNginxConfigOpen] = useState(false)
|
||||
@@ -1085,13 +1087,13 @@ function App(): JSX.Element {
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2, border: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<Paper sx={{ p: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2, border: '1px solid rgba(255, 255, 255, 0.05)', mb: 3 }}>
|
||||
<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="h6" sx={{ fontWeight: 'bold' }}>MySQL Veri İçe Aktar</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>
|
||||
@@ -1104,7 +1106,33 @@ function App(): JSX.Element {
|
||||
startIcon={<DbIcon />}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
SİHİRBAZI BAŞLAT
|
||||
İÇE AKTAR
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</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="secondary" sx={{ fontSize: 32 }} />
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>MySQL Veri Dışa Aktar</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Veritabanlarını .sql veya .sql.gz olarak güvenle yedekleyin.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
size="large"
|
||||
onClick={() => setIsMySqlExportWizardOpen(true)}
|
||||
startIcon={<DbIcon />}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
DIŞA AKTAR
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -1432,6 +1460,12 @@ function App(): JSX.Element {
|
||||
mySqlVersions={mySqlVersions}
|
||||
/>
|
||||
|
||||
<MySqlExportWizard
|
||||
open={isMySqlExportWizardOpen}
|
||||
onClose={() => setIsMySqlExportWizardOpen(false)}
|
||||
mySqlVersions={mySqlVersions}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={isNginxConfigOpen}
|
||||
onClose={() => setIsNginxConfigOpen(false)}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
Typography,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Paper,
|
||||
InputAdornment,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Stack,
|
||||
FormControlLabel,
|
||||
Checkbox
|
||||
} from '@mui/material'
|
||||
import {
|
||||
CheckCircle as SuccessIcon,
|
||||
Error as ErrorIcon,
|
||||
Save as SaveIcon
|
||||
} from '@mui/icons-material'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
interface MySqlExportWizardProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
mySqlVersions: any[]
|
||||
}
|
||||
|
||||
const steps = [
|
||||
'Sunucu Seçimi',
|
||||
'Veritabanı Seçimi',
|
||||
'Dışa Aktarma Ayarları',
|
||||
'Özet ve İşlem'
|
||||
]
|
||||
|
||||
const MySqlExportWizard: React.FC<MySqlExportWizardProps> = ({ open, onClose, mySqlVersions }) => {
|
||||
const [activeStep, setActiveStep] = useState(0)
|
||||
const [selectedVersion, setSelectedVersion] = useState('')
|
||||
const [databases, setDatabases] = useState<string[]>([])
|
||||
const [selectedDb, setSelectedDb] = useState('')
|
||||
const [savePath, setSavePath] = useState('')
|
||||
const [compress, setCompress] = useState(true)
|
||||
const [rootPass, setRootPass] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const logEndRef = useRef<HTMLDivElement>(null)
|
||||
const [exportResult, setExportResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setActiveStep(0)
|
||||
setSelectedDb('')
|
||||
setSavePath('')
|
||||
setLogs([])
|
||||
setExportResult(null)
|
||||
const installedVersion = mySqlVersions.find(v => v.status === 'installed')
|
||||
if (installedVersion) setSelectedVersion(installedVersion.id)
|
||||
}
|
||||
}, [open, mySqlVersions])
|
||||
|
||||
useEffect(() => {
|
||||
if (logEndRef.current) {
|
||||
logEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}, [logs])
|
||||
|
||||
const nextStep = () => setActiveStep(prev => prev + 1)
|
||||
const prevStep = () => setActiveStep(prev => prev - 1)
|
||||
|
||||
const handleStep1Next = async () => {
|
||||
if (!selectedVersion) {
|
||||
Swal.fire('Hata', 'Lütfen bir MySQL sunucusu seçin.', 'error')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const dbs = await window.api.invoke('mysql:export-wizard:list-dbs', { versionId: selectedVersion, rootPass })
|
||||
setDatabases(dbs)
|
||||
nextStep()
|
||||
} catch (err: any) {
|
||||
Swal.fire('Hata', 'Veritabanları listelenemedi: ' + err.message, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectSavePath = async () => {
|
||||
const path = await window.api.invoke('dialog:save-file', [
|
||||
{ name: compress ? 'Compressed SQL' : 'SQL File', extensions: [compress ? 'sql.gz' : 'sql'] }
|
||||
])
|
||||
if (path) setSavePath(path)
|
||||
}
|
||||
|
||||
const startExport = async () => {
|
||||
setLoading(true)
|
||||
setLogs(['[Sistem] Dışa aktarma işlemi başlatılıyor...'])
|
||||
|
||||
const removeListener = window.api.onMySqlExportLog((msg: string) => {
|
||||
setLogs(prev => [...prev, msg])
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await window.api.invoke('mysql:export-wizard:start', {
|
||||
versionId: selectedVersion,
|
||||
dbName: selectedDb,
|
||||
savePath,
|
||||
compress,
|
||||
rootPass
|
||||
})
|
||||
setExportResult(result)
|
||||
} catch (err: any) {
|
||||
setExportResult({ success: false, message: err.message })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (removeListener) removeListener()
|
||||
}
|
||||
}
|
||||
|
||||
const renderStepContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body1" gutterBottom>Veritabanın hangi MySQL sunucusunda olduğunu 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>
|
||||
<TextField
|
||||
label="Root Şifresi (Eğer varsa)"
|
||||
fullWidth
|
||||
sx={{ mt: 3 }}
|
||||
value={rootPass}
|
||||
type="password"
|
||||
onChange={(e) => setRootPass(e.target.value)}
|
||||
helperText="MySQL 'root' kullanıcısının şifresi. Boşsa boş bırakın."
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
case 1:
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body1" gutterBottom>Dışa aktarmak istediğiniz veritabanını seçin.</Typography>
|
||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel>Veritabanı</InputLabel>
|
||||
<Select
|
||||
value={selectedDb}
|
||||
label="Veritabanı"
|
||||
onChange={(e) => setSelectedDb(e.target.value)}
|
||||
>
|
||||
{databases.map(db => (
|
||||
<MenuItem key={db} value={db}>{db}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{databases.length === 0 && (
|
||||
<Typography variant="caption" color="error" sx={{ mt: 1, display: 'block' }}>
|
||||
Seçili sunucuda erişilebilir veritabanı bulunamadı.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
case 2:
|
||||
return (
|
||||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<Typography variant="body1">Dosyanın kaydedileceği yeri ve formatını belirleyin.</Typography>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={compress} onChange={(e) => setCompress(e.target.checked)} />}
|
||||
label="Gzip ile Sıkıştır (.sql.gz)"
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={savePath}
|
||||
placeholder="Kaydedilecek dosya..."
|
||||
disabled
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SaveIcon color="primary" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" onClick={handleSelectSavePath} sx={{ minWidth: '120px' }}>SEÇ</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
case 3:
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{!exportResult ? (
|
||||
<>
|
||||
<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>Sunucu:</strong> {selectedVersion}</Typography>
|
||||
<Typography variant="body2"><strong>Veritabanı:</strong> {selectedDb}</Typography>
|
||||
<Typography variant="body2"><strong>Hedef:</strong> {savePath}</Typography>
|
||||
<Typography variant="body2"><strong>Sıkıştırma:</strong> {compress ? 'Açık (.gz)' : 'Kapalı (.sql)'}</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('error') ? 'error.main' : 'success.main' }}>
|
||||
{log}
|
||||
</Typography>
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
{exportResult.success ? (
|
||||
<>
|
||||
<SuccessIcon color="success" sx={{ fontSize: 60 }} />
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>İşlem Başarılı!</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{exportResult.message}</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{ mt: 3 }}
|
||||
onClick={() => window.api.invoke('shell:open-path', savePath.substring(0, savePath.lastIndexOf('\\')))}
|
||||
>
|
||||
DOSYAYI GÖSTER
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ErrorIcon color="error" sx={{ fontSize: 60 }} />
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>Hata Oluştu!</Typography>
|
||||
<Typography variant="body2" color="error">{exportResult.message}</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
default:
|
||||
return 'Bilinmeyen adım'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={loading ? undefined : onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>MySQL Veritabanı Dışa Aktarma 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 && !exportResult && (
|
||||
<Button onClick={prevStep} disabled={loading}>GERİ</Button>
|
||||
)}
|
||||
{activeStep === 0 && (
|
||||
<Button variant="contained" onClick={handleStep1Next} disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : 'İLERİ'}
|
||||
</Button>
|
||||
)}
|
||||
{activeStep === 1 && (
|
||||
<Button variant="contained" onClick={nextStep} disabled={!selectedDb}>İLERİ</Button>
|
||||
)}
|
||||
{activeStep === 2 && (
|
||||
<Button variant="contained" onClick={nextStep} disabled={!savePath}>İLERİ</Button>
|
||||
)}
|
||||
{activeStep === 3 && !exportResult && (
|
||||
<Button variant="contained" color="primary" onClick={startExport} disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : 'EXPORTU BAŞLAT'}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default MySqlExportWizard
|
||||
@@ -9,7 +9,9 @@
|
||||
"quit": "Çıkış",
|
||||
"save": "Kaydet",
|
||||
"port": "Port",
|
||||
"all": "Tümü"
|
||||
"all": "Tümü",
|
||||
"export": "Dışa Aktar",
|
||||
"import": "İçe Aktar"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Trunçgil Multi-PHP Server",
|
||||
|
||||
Reference in New Issue
Block a user