feat: implement MariaDB management interface for database and user operations

This commit is contained in:
Ümit Tunç
2026-04-02 14:59:32 +03:00
parent bf24818b83
commit a3d662423f
8 changed files with 624 additions and 1 deletions
+112
View File
@@ -1,4 +1,116 @@
[ [
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "e224nli4b",
"timestamp": 1775131071408
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "uylwtxbbj",
"timestamp": 1775131071388
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "2s4hcn046",
"timestamp": 1775131071369
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "6jyn1ulp5",
"timestamp": 1775131071348
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "rwjbq8erc",
"timestamp": 1775131070886
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "ckyqrf43d",
"timestamp": 1775131070866
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "a91gjnuiz",
"timestamp": 1775131070360
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "6hzh7wm5t",
"timestamp": 1775131070285
},
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "e7dkj5bii",
"timestamp": 1775130858539
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "osp7uacka",
"timestamp": 1775130858516
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "w98vrvad6",
"timestamp": 1775130858493
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "kjbfmzipg",
"timestamp": 1775130858466
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "src638nzz",
"timestamp": 1775130857997
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "l5izsomjp",
"timestamp": 1775130857977
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "tjho3pnv0",
"timestamp": 1775130857478
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "0l999aqql",
"timestamp": 1775130857409
},
{ {
"service": "mariadb:10.11.8", "service": "mariadb:10.11.8",
"action": "start", "action": "start",
+54
View File
@@ -276,6 +276,60 @@ export function registerIpcHandlers(): void {
} }
}) })
ipcMain.handle('mariadb:databases-with-sizes', async (_event, versionId) => {
try {
return await mariaDbManagerService.getDatabasesWithSizes(versionId)
} catch (error: any) {
console.error('Failed to get databases:', error)
return []
}
})
ipcMain.handle('mariadb:create-db', async (_event, { versionId, dbName }) => {
try {
return await mariaDbManagerService.createDatabase(versionId, dbName)
} catch (error: any) {
console.error('Failed to create database:', error)
return { success: false, message: error.message }
}
})
ipcMain.handle('mariadb:drop-db', async (_event, { versionId, dbName }) => {
try {
return await mariaDbManagerService.dropDatabase(versionId, dbName)
} catch (error: any) {
console.error('Failed to drop database:', error)
return { success: false, message: error.message }
}
})
ipcMain.handle('mariadb:users', async (_event, versionId) => {
try {
return await mariaDbManagerService.getUsers(versionId)
} catch (error: any) {
console.error('Failed to get users:', error)
return []
}
})
ipcMain.handle('mariadb:create-user', async (_event, { versionId, user, host, password }) => {
try {
return await mariaDbManagerService.createUser(versionId, { user, host, password })
} catch (error: any) {
console.error('Failed to create user:', error)
return { success: false, message: error.message }
}
})
ipcMain.handle('mariadb:drop-user', async (_event, { versionId, user, host }) => {
try {
return await mariaDbManagerService.dropUser(versionId, { user, host })
} catch (error: any) {
console.error('Failed to drop user:', error)
return { success: false, message: error.message }
}
})
// CLI & Environment // CLI & Environment
ipcMain.handle('cli:get-config', async () => { ipcMain.handle('cli:get-config', async () => {
const settings = configService.getSettings() const settings = configService.getSettings()
@@ -215,6 +215,54 @@ export class MariaDbManagerService {
return true return true
} }
private async getMariaDbConfig(versionId: string) {
const version = this.versions.find(v => v.id === versionId)
if (!version) throw new Error('Version not found')
const binaryPath = await this.getMariaDbBinaryPath(version.version)
if (!binaryPath) throw new Error('MariaDB binary not found')
const settings = configService.getSettings()
return {
binPath: binaryPath,
host: '127.0.0.1',
port: version.port,
rootUser: 'root',
rootPass: settings.mariaDbPasswords?.[versionId] || ''
}
}
async getDatabasesWithSizes(versionId: string) {
const config = await this.getMariaDbConfig(versionId)
return await mariaDbOperationService.getDatabasesWithSizes(config)
}
async createDatabase(versionId: string, dbName: string) {
const config = await this.getMariaDbConfig(versionId)
const result = await mariaDbOperationService.runQuery(config, `CREATE DATABASE IF NOT EXISTS \`${dbName}\` CHARACTER SET utf8 COLLATE utf8_unicode_ci`)
return { success: result.success, message: result.success ? 'Database created' : result.error }
}
async dropDatabase(versionId: string, dbName: string) {
const config = await this.getMariaDbConfig(versionId)
return await mariaDbOperationService.dropDatabase(config, dbName)
}
async getUsers(versionId: string) {
const config = await this.getMariaDbConfig(versionId)
return await mariaDbOperationService.listUsers(config)
}
async createUser(versionId: string, { user, host, password }: any) {
const config = await this.getMariaDbConfig(versionId)
return await mariaDbOperationService.createUser(config, user, host, password)
}
async dropUser(versionId: string, { user, host }: any) {
const config = await this.getMariaDbConfig(versionId)
return await mariaDbOperationService.dropUser(config, user, host)
}
} }
export const mariaDbManagerService = new MariaDbManagerService() export const mariaDbManagerService = new MariaDbManagerService()
@@ -216,6 +216,52 @@ export class MariaDbOperationService {
.filter(line => line && line !== 'Database' && !line.match(/^information_schema|performance_schema|mysql|sys$/i)) .filter(line => line && line !== 'Database' && !line.match(/^information_schema|performance_schema|mysql|sys$/i))
} }
async getDatabasesWithSizes(config: MariaDbConfig): Promise<{ name: string; size: number }[]> {
const query = "SELECT table_schema AS 'db', SUM(data_length + index_length) / 1024 / 1024 AS 'size' FROM information_schema.TABLES GROUP BY table_schema";
const result = await this.runQuery(config, query)
if (!result.success) return []
const lines = result.output.split('\n').filter(l => l.trim().length > 0)
if (lines.length <= 1) return []
return lines.slice(1).map(line => {
const [name, size] = line.split('\t')
return { name, size: parseFloat(size) || 0 }
}).filter(db => !db.name.match(/^information_schema|performance_schema|mysql|sys$/i))
}
async listUsers(config: MariaDbConfig): Promise<{ user: string; host: string }[]> {
const result = await this.runQuery(config, "SELECT user, host FROM mysql.user")
if (!result.success) return []
const lines = result.output.split('\n').filter(l => l.trim().length > 0)
if (lines.length <= 1) return []
return lines.slice(1).map(line => {
const [user, host] = line.split('\t')
return { user, host }
})
}
async createUser(config: MariaDbConfig, user: string, host: string, pass: string): Promise<{ success: boolean; message: string }> {
const queries = [
`CREATE USER IF NOT EXISTS '${user}'@'${host}' IDENTIFIED BY '${pass}';`,
`GRANT ALL PRIVILEGES ON *.* TO '${user}'@'${host}' WITH GRANT OPTION;`,
`FLUSH PRIVILEGES;`
]
for (const query of queries) {
const result = await this.runQuery(config, query)
if (!result.success) return { success: false, message: result.error || 'Query failed' }
}
return { success: true, message: 'User created successfully' }
}
async dropUser(config: MariaDbConfig, user: string, host: string): Promise<{ success: boolean; message: string }> {
const result = await this.runQuery(config, `DROP USER '${user}'@'${host}'`)
return { success: result.success, message: result.success ? 'User dropped' : result.error || 'Drop failed' }
}
async exportSql(config: MariaDbConfig, dbName: string, targetPath: string, compress: boolean, onProgress?: (log: string) => void): Promise<{ success: boolean; message: string }> { async exportSql(config: MariaDbConfig, dbName: string, targetPath: string, compress: boolean, onProgress?: (log: string) => void): Promise<{ success: boolean; message: string }> {
return new Promise((resolve) => { return new Promise((resolve) => {
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
+9 -1
View File
@@ -86,6 +86,7 @@ import { useTranslation } from 'react-i18next'
import { supportedLngs } from './i18n' import { supportedLngs } from './i18n'
import MariaDbImportWizard from './components/MariaDbImportWizard' import MariaDbImportWizard from './components/MariaDbImportWizard'
import MariaDbExportWizard from './components/MariaDbExportWizard' import MariaDbExportWizard from './components/MariaDbExportWizard'
import MariaDbDatabaseManager from './components/MariaDbDatabaseManager'
import NginxCodeEditor from './components/NginxCodeEditor' import NginxCodeEditor from './components/NginxCodeEditor'
import ApacheCodeEditor from './components/ApacheCodeEditor' import ApacheCodeEditor from './components/ApacheCodeEditor'
import PhpExtensionManager from './components/PhpExtensionManager' import PhpExtensionManager from './components/PhpExtensionManager'
@@ -1119,9 +1120,15 @@ function App(): JSX.Element {
<ListItemText primary={t('common.tools')} /> <ListItemText primary={t('common.tools')} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'db_manager'} onClick={() => setActiveTab('db_manager')}>
<ListItemIcon><DbIcon color={activeTab === 'db_manager' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_manager.title')} />
</ListItemButton>
</ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton selected={activeTab === 'db_monitor'} onClick={() => setActiveTab('db_monitor')}> <ListItemButton selected={activeTab === 'db_monitor'} onClick={() => setActiveTab('db_monitor')}>
<ListItemIcon><DbIcon color={activeTab === 'db_monitor' ? 'primary' : 'inherit'} /></ListItemIcon> <ListItemIcon><TerminalIcon color={activeTab === 'db_monitor' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_process_monitor.title')} /> <ListItemText primary={t('mariadb_process_monitor.title')} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
@@ -1142,6 +1149,7 @@ function App(): JSX.Element {
<Toolbar /> <Toolbar />
<Container maxWidth={false} sx={{ py: 2 }}> <Container maxWidth={false} sx={{ py: 2 }}>
{activeTab === 'tuning' && <ServerTuning />} {activeTab === 'tuning' && <ServerTuning />}
{activeTab === 'db_manager' && <MariaDbDatabaseManager versions={mariaDbVersions} />}
{activeTab === 'settings' && ( {activeTab === 'settings' && (
<Box> <Box>
@@ -0,0 +1,321 @@
import React, { useState, useEffect } from 'react'
import {
Box,
Paper,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
IconButton,
Button,
Select,
MenuItem,
FormControl,
InputLabel,
Stack,
Divider,
Tooltip,
CircularProgress,
Alert
} from '@mui/material'
import {
Delete as DeleteIcon,
Add as AddIcon,
Refresh as RefreshIcon,
Storage as DbIcon,
Person as UserIcon,
Language as HostIcon
} from '@mui/icons-material'
import Swal from 'sweetalert2'
import { useTranslation } from 'react-i18next'
interface DatabaseInfo {
name: string
size: number
}
interface UserInfo {
user: string
host: string
}
interface MariaDbDatabaseManagerProps {
versions: any[]
}
const MariaDbDatabaseManager: React.FC<MariaDbDatabaseManagerProps> = ({ versions }) => {
const { t } = useTranslation()
const [selectedVersion, setSelectedVersion] = useState<string>('')
const [databases, setDatabases] = useState<DatabaseInfo[]>([])
const [users, setUsers] = useState<UserInfo[]>([])
const [loading, setLoading] = useState(false)
const installedVersions = versions.filter(v => v.status === 'installed')
useEffect(() => {
if (!selectedVersion && installedVersions.length > 0) {
setSelectedVersion(installedVersions[0].id)
}
}, [installedVersions])
useEffect(() => {
if (selectedVersion) {
fetchData()
}
}, [selectedVersion])
const fetchData = async () => {
if (!selectedVersion) return
setLoading(true)
try {
const dbList = await window.api.invoke('mariadb:databases-with-sizes', selectedVersion)
const userList = await window.api.invoke('mariadb:users', selectedVersion)
setDatabases(dbList || [])
setUsers(userList || [])
} catch (error) {
console.error('Fetch data error:', error)
} finally {
setLoading(false)
}
}
const handleCreateDb = async () => {
const { value: dbName } = await Swal.fire({
title: t('mariadb_manager.create_db_title'),
input: 'text',
inputLabel: t('mariadb_manager.db_name_label'),
showCancelButton: true,
background: '#1e1e1e',
color: '#fff',
inputValidator: (value) => {
if (!value) return t('common.error_required')
return null
}
})
if (dbName) {
const result = await window.api.invoke('mariadb:create-db', { versionId: selectedVersion, dbName })
if (result.success) {
Swal.fire({ icon: 'success', title: t('common.ok'), background: '#1e1e1e', color: '#fff' })
fetchData()
} else {
Swal.fire({ icon: 'error', title: t('common.error'), text: result.message, background: '#1e1e1e', color: '#fff' })
}
}
}
const handleDeleteDb = async (dbName: string) => {
const result = await Swal.fire({
title: t('common.are_you_sure'),
text: t('mariadb_manager.delete_db_confirm', { db: dbName }),
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
background: '#1e1e1e',
color: '#fff'
})
if (result.isConfirmed) {
const res = await window.api.invoke('mariadb:drop-db', { versionId: selectedVersion, dbName })
if (res.success) {
fetchData()
} else {
Swal.fire({ icon: 'error', title: t('common.error'), text: res.message, background: '#1e1e1e', color: '#fff' })
}
}
}
const handleCreateUser = async () => {
const { value: formValues } = await Swal.fire({
title: t('mariadb_manager.create_user_title'),
html:
`<input id="swal-user" class="swal2-input" placeholder="${t('mariadb_manager.user_label')}">` +
`<input id="swal-host" class="swal2-input" placeholder="Host" value="%">` +
`<input id="swal-pass" type="password" class="swal2-input" placeholder="${t('mariadb_manager.password_label')}">`,
focusConfirm: false,
background: '#1e1e1e',
color: '#fff',
preConfirm: () => {
const user = (document.getElementById('swal-user') as HTMLInputElement).value
const host = (document.getElementById('swal-host') as HTMLInputElement).value
const password = (document.getElementById('swal-pass') as HTMLInputElement).value
if (!user || !host) {
Swal.showValidationMessage(t('common.error_required'))
return null
}
return { user, host, password }
}
})
if (formValues) {
const result = await window.api.invoke('mariadb:create-user', { versionId: selectedVersion, ...formValues })
if (result.success) {
Swal.fire({ icon: 'success', title: t('common.ok'), background: '#1e1e1e', color: '#fff' })
fetchData()
} else {
Swal.fire({ icon: 'error', title: t('common.error'), text: result.message, background: '#1e1e1e', color: '#fff' })
}
}
}
const handleDeleteUser = async (user: string, host: string) => {
const result = await Swal.fire({
title: t('common.are_you_sure'),
text: t('mariadb_manager.delete_user_confirm', { user: `${user}@${host}` }),
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
background: '#1e1e1e',
color: '#fff'
})
if (result.isConfirmed) {
const res = await window.api.invoke('mariadb:drop-user', { versionId: selectedVersion, user, host })
if (res.success) {
fetchData()
} else {
Swal.fire({ icon: 'error', title: t('common.error'), text: res.message, background: '#1e1e1e', color: '#fff' })
}
}
}
return (
<Box>
<Paper sx={{ p: 3, mb: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between" sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<DbIcon color="primary" />
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>{t('mariadb_manager.title')}</Typography>
</Box>
<Stack direction="row" spacing={2}>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel id="version-select-label">{t('mariadb.version')}</InputLabel>
<Select
labelId="version-select-label"
value={selectedVersion}
label={t('mariadb.version')}
onChange={(e) => setSelectedVersion(e.target.value)}
>
{installedVersions.map(v => (
<MenuItem key={v.id} value={v.id}>MariaDB {v.version}</MenuItem>
))}
</Select>
</FormControl>
<Button startIcon={<RefreshIcon />} onClick={fetchData} disabled={loading}>
{t('common.refresh')}
</Button>
</Stack>
</Stack>
{!selectedVersion && (
<Alert severity="info" sx={{ mt: 2 }}>{t('mariadb_manager.no_active_version')}</Alert>
)}
{selectedVersion && (
<Stack spacing={4}>
{/* Databases Section */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'primary.light' }}>
{t('mariadb_manager.databases_title')}
</Typography>
<Button variant="contained" size="small" startIcon={<AddIcon />} onClick={handleCreateDb}>
{t('mariadb_manager.create_db_btn')}
</Button>
</Box>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 400 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.db_name')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.db_size')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold', textAlign: 'right' }}>{t('common.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={24} sx={{ my: 2 }} /></TableCell></TableRow>
) : databases.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 3, color: 'text.secondary' }}>{t('common.no_data')}</TableCell></TableRow>
) : databases.map(db => (
<TableRow key={db.name}>
<TableCell sx={{ fontWeight: 'bold' }}>{db.name}</TableCell>
<TableCell>{db.size.toFixed(2)} MB</TableCell>
<TableCell align="right">
<Tooltip title={t('common.delete')}>
<IconButton color="error" size="small" onClick={() => handleDeleteDb(db.name)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
<Divider sx={{ opacity: 0.1 }} />
{/* Users Section */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'secondary.light' }}>
{t('mariadb_manager.users_title')}
</Typography>
<Button variant="contained" color="secondary" size="small" startIcon={<AddIcon />} onClick={handleCreateUser}>
{t('mariadb_manager.create_user_btn')}
</Button>
</Box>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 400 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.user_label')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>Host</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold', textAlign: 'right' }}>{t('common.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={24} sx={{ my: 2 }} /></TableCell></TableRow>
) : users.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 3, color: 'text.secondary' }}>{t('common.no_data')}</TableCell></TableRow>
) : users.map(user => (
<TableRow key={`${user.user}-${user.host}`}>
<TableCell sx={{ fontWeight: 'bold' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<UserIcon fontSize="inherit" sx={{ opacity: 0.5 }} />
{user.user}
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<HostIcon fontSize="inherit" sx={{ opacity: 0.5 }} />
{user.host}
</Box>
</TableCell>
<TableCell align="right">
<Tooltip title={t('common.delete')}>
<IconButton color="error" size="small" onClick={() => handleDeleteUser(user.user, user.host)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
</Stack>
)}
</Paper>
</Box>
)
}
export default MariaDbDatabaseManager
+17
View File
@@ -322,5 +322,22 @@
"timestamp": "Time", "timestamp": "Time",
"details": "Details" "details": "Details"
} }
},
"mariadb_manager": {
"title": "MariaDB Management",
"databases_title": "Databases",
"users_title": "Users",
"create_db_btn": "New Database",
"create_db_title": "Create New Database",
"db_name_label": "Database Name",
"db_name": "Database",
"db_size": "Size",
"create_user_btn": "New User",
"create_user_title": "Create New User",
"user_label": "Username",
"password_label": "Password",
"delete_db_confirm": "Are you sure you want to delete database '{{db}}'? This action cannot be undone!",
"delete_user_confirm": "Are you sure you want to delete user '{{user}}'?",
"no_active_version": "Please select an installed MariaDB version first or download one."
} }
} }
+17
View File
@@ -317,5 +317,22 @@
"timestamp": "Zaman", "timestamp": "Zaman",
"details": "Detay" "details": "Detay"
} }
},
"mariadb_manager": {
"title": "MariaDB Yönetimi",
"databases_title": "Veritabanları",
"users_title": "Kullanıcılar",
"create_db_btn": "Yeni Veritabanı",
"create_db_title": "Yeni Veritabanı Oluştur",
"db_name_label": "Veritabanı Adı",
"db_name": "Veritabanı",
"db_size": "Boyut",
"create_user_btn": "Yeni Kullanıcı",
"create_user_title": "Yeni Kullanıcı Oluştur",
"user_label": "Kullanıcı Adı",
"password_label": "Şifre",
"delete_db_confirm": "'{{db}}' veritabanını silmek istediğinize emin misiniz? Bu işlem geri alınamaz!",
"delete_user_confirm": "'{{user}}' kullanıcısını silmek istediğinize emin misiniz?",
"no_active_version": "Lütfen önce yüklü bir MariaDB sürümü seçin veya bir sürüm indirin."
} }
} }