feat: implement dashboard sidebar and service management infrastructure
This commit is contained in:
+1396
File diff suppressed because it is too large
Load Diff
@@ -337,6 +337,26 @@ async function restartApache(): Promise<{ success: boolean; message: string }> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerIpcHandlers(): void {
|
export function registerIpcHandlers(): void {
|
||||||
|
console.log('Registering config:template IPC handlers...')
|
||||||
|
ipcMain.handle('config:template:get', async (_event, templateName: string) => {
|
||||||
|
try {
|
||||||
|
console.log('Fetching template:', templateName)
|
||||||
|
return { success: true, content: configService.getTemplate(templateName) }
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to get template:', error)
|
||||||
|
return { success: false, message: error.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('config:template:save', async (_event, { templateName, content }) => {
|
||||||
|
try {
|
||||||
|
configService.saveTemplate(templateName, content)
|
||||||
|
return { success: true, message: 'Şablon başarıyla kaydedildi.' }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: error.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('mariadb:processlist', async (_event, versionId) => {
|
ipcMain.handle('mariadb:processlist', async (_event, versionId) => {
|
||||||
try {
|
try {
|
||||||
return await mariaDbManagerService.getProcessList(versionId)
|
return await mariaDbManagerService.getProcessList(versionId)
|
||||||
|
|||||||
@@ -71,6 +71,26 @@ export class ConfigService {
|
|||||||
return path.join(this.configDir, targetName)
|
return path.join(this.configDir, targetName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTemplate(templateName: string): string {
|
||||||
|
const templatePath = path.join(this.templateDir, templateName)
|
||||||
|
if (!fs.existsSync(templatePath)) {
|
||||||
|
throw new Error(`Template not found: ${templatePath}`)
|
||||||
|
}
|
||||||
|
return fs.readFileSync(templatePath, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTemplate(templateName: string, content: string): void {
|
||||||
|
const templatePath = path.join(this.templateDir, templateName)
|
||||||
|
const backupPath = `${templatePath}.bak`
|
||||||
|
|
||||||
|
// Simple backup
|
||||||
|
if (fs.existsSync(templatePath)) {
|
||||||
|
fs.copyFileSync(templatePath, backupPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(templatePath, content)
|
||||||
|
}
|
||||||
|
|
||||||
getSettings(): any {
|
getSettings(): any {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(this.settingsPath)) {
|
if (fs.existsSync(this.settingsPath)) {
|
||||||
|
|||||||
+67
-428
@@ -71,7 +71,6 @@ import {
|
|||||||
Close as CloseIcon,
|
Close as CloseIcon,
|
||||||
ContentCopy as ContentCopyIcon,
|
ContentCopy as ContentCopyIcon,
|
||||||
OpenInNew as OpenIcon,
|
OpenInNew as OpenIcon,
|
||||||
Restore as ResetIcon,
|
|
||||||
Tune as TuningIcon,
|
Tune as TuningIcon,
|
||||||
History as HistoryIcon,
|
History as HistoryIcon,
|
||||||
MenuBook as GuideIcon,
|
MenuBook as GuideIcon,
|
||||||
@@ -90,6 +89,7 @@ import ServerTuning from './components/ServerTuning'
|
|||||||
import ServiceEventMonitor from './components/ServiceEventMonitor'
|
import ServiceEventMonitor from './components/ServiceEventMonitor'
|
||||||
import UserGuide from './components/UserGuide'
|
import UserGuide from './components/UserGuide'
|
||||||
import ProjectManager from './components/ProjectManager/ProjectManager'
|
import ProjectManager from './components/ProjectManager/ProjectManager'
|
||||||
|
import DashboardManager from './components/Dashboard/DashboardManager'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -135,7 +135,6 @@ function App(): JSX.Element {
|
|||||||
const [projectSearch, setProjectSearch] = useState('')
|
const [projectSearch, setProjectSearch] = useState('')
|
||||||
const [cliConfig, setCliConfig] = useState<any>({ enabled: true, defaultPhp: '', defaultMariaDb: '', binDir: '' })
|
const [cliConfig, setCliConfig] = useState<any>({ enabled: true, defaultPhp: '', defaultMariaDb: '', binDir: '' })
|
||||||
const [settingsTab, setSettingsTab] = useState(0)
|
const [settingsTab, setSettingsTab] = useState(0)
|
||||||
const [dashboardTab, setDashboardTab] = useState(0)
|
|
||||||
const [projects, setProjects] = useState<any[]>([])
|
const [projects, setProjects] = useState<any[]>([])
|
||||||
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
||||||
const [mariaDbVersions, setMariaDbVersions] = useState<any[]>([])
|
const [mariaDbVersions, setMariaDbVersions] = useState<any[]>([])
|
||||||
@@ -190,6 +189,11 @@ function App(): JSX.Element {
|
|||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||||
const [projectDetailTab, setProjectDetailTab] = useState(0)
|
const [projectDetailTab, setProjectDetailTab] = useState(0)
|
||||||
|
|
||||||
|
const handleSelectProject = (projectId: string) => {
|
||||||
|
setSelectedProjectId(projectId)
|
||||||
|
setActiveTab('projects')
|
||||||
|
}
|
||||||
|
|
||||||
const parseNginxError = (message: string) => {
|
const parseNginxError = (message: string) => {
|
||||||
const match = message.match(/in (.*):(\d+)/)
|
const match = message.match(/in (.*):(\d+)/)
|
||||||
if (!match) return null
|
if (!match) return null
|
||||||
@@ -270,9 +274,10 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchApacheVersions = async () => {
|
const fetchApacheVersions = async () => {
|
||||||
if (!window.api || !window.api.getApacheVersions) return
|
if (window.api && window.api.getApacheVersions) {
|
||||||
const v = await window.api.getApacheVersions()
|
const list = await window.api.getApacheVersions()
|
||||||
setApacheVersions(v || [])
|
setApacheVersions(list || [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchLogs = async (service: string) => {
|
const fetchLogs = async (service: string) => {
|
||||||
@@ -385,6 +390,49 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [logs, logDialogOpen])
|
}, [logs, logDialogOpen])
|
||||||
|
|
||||||
|
const handleOpenNginxConfig = async () => {
|
||||||
|
if (window.api && window.api.invoke) {
|
||||||
|
const config = await window.api.invoke('nginx:config:get')
|
||||||
|
setNginxConfig(config)
|
||||||
|
setIsNginxConfigOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveNginxConfig = async () => {
|
||||||
|
if (window.api && window.api.invoke) {
|
||||||
|
const result = await window.api.invoke('nginx:config:save', nginxConfig)
|
||||||
|
if (result.success) {
|
||||||
|
Swal.fire({ title: t('common.success'), text: t('server.nginx.config_saved'), icon: 'success', background: '#1e1e1e', color: '#fff' })
|
||||||
|
setIsNginxConfigOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenApacheConfig = async () => {
|
||||||
|
if (window.api && window.api.invoke) {
|
||||||
|
const config = await window.api.invoke('apache:config:get')
|
||||||
|
setApacheConfig(config)
|
||||||
|
setIsApacheConfigOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveApacheConfig = async () => {
|
||||||
|
if (window.api && window.api.invoke) {
|
||||||
|
const result = await window.api.invoke('apache:config:save', apacheConfig)
|
||||||
|
if (result.success) {
|
||||||
|
Swal.fire({ title: t('common.success'), text: t('server.apache.config_saved'), icon: 'success', background: '#1e1e1e', color: '#fff' })
|
||||||
|
setIsApacheConfigOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTestApacheConfig = async () => {
|
||||||
|
if (window.api && window.api.invoke) {
|
||||||
|
const result = await window.api.invoke('apache:config:test')
|
||||||
|
Swal.fire({ title: result.success ? t('common.success') : t('common.error'), text: result.message, icon: result.success ? 'success' : 'error', background: '#1e1e1e', color: '#fff' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelectPath = async () => {
|
const handleSelectPath = async () => {
|
||||||
if (window.api && window.api.selectDirectory) {
|
if (window.api && window.api.selectDirectory) {
|
||||||
const pathStr = await window.api.selectDirectory()
|
const pathStr = await window.api.selectDirectory()
|
||||||
@@ -396,76 +444,6 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleService = async (service: string) => {
|
|
||||||
if (!window.api) return
|
|
||||||
|
|
||||||
const isStarting = ((status[service] as any)?.status || status[service] || 'stopped') === 'stopped'
|
|
||||||
if (isStarting) {
|
|
||||||
setStatus(prev => ({ ...prev, [service]: { status: 'starting' } }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = ((status[service] as any)?.status || status[service] || 'stopped') === 'running'
|
|
||||||
? await window.api.stopService(service)
|
|
||||||
: await window.api.startService(service)
|
|
||||||
|
|
||||||
// Refresh status after operation
|
|
||||||
fetchStatus()
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
Swal.fire({
|
|
||||||
title: t('common.error'),
|
|
||||||
text: result.message,
|
|
||||||
icon: 'error',
|
|
||||||
confirmButtonText: t('common.ok'),
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff',
|
|
||||||
confirmButtonColor: '#153E5E',
|
|
||||||
customClass: {
|
|
||||||
popup: 'swal2-dark-popup'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setNotification({
|
|
||||||
open: true,
|
|
||||||
message: result.message,
|
|
||||||
severity: 'success'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpenLogs = async (service: string) => {
|
|
||||||
setSelectedService(service)
|
|
||||||
setLogDialogOpen(true)
|
|
||||||
await fetchLogs(service)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResetData = async (service: string) => {
|
|
||||||
const confirmResult = await Swal.fire({
|
|
||||||
title: t('common.reset_confirm_title'),
|
|
||||||
text: t('common.reset_confirm_text', { service }),
|
|
||||||
icon: 'warning',
|
|
||||||
showCancelButton: true,
|
|
||||||
confirmButtonText: t('common.reset_btn'),
|
|
||||||
cancelButtonText: t('common.cancel'),
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff',
|
|
||||||
confirmButtonColor: '#d33',
|
|
||||||
cancelButtonColor: '#3085d6'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (confirmResult.isConfirmed) {
|
|
||||||
const result = await window.api.invoke('services:reset-data', service)
|
|
||||||
Swal.fire({
|
|
||||||
title: result.success ? t('common.success') : t('common.error'),
|
|
||||||
text: result.message,
|
|
||||||
icon: result.success ? 'success' : 'error',
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff'
|
|
||||||
})
|
|
||||||
fetchStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveSettings = async () => {
|
const handleSaveSettings = async () => {
|
||||||
if (window.api && window.api.saveSettings) {
|
if (window.api && window.api.saveSettings) {
|
||||||
const result = await window.api.saveSettings(settings)
|
const result = await window.api.saveSettings(settings)
|
||||||
@@ -532,74 +510,6 @@ function App(): JSX.Element {
|
|||||||
return `${Math.round(mb)} MB`
|
return `${Math.round(mb)} MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusChip = (s: string | { status: string, cpu?: number, memory?: number }) => {
|
|
||||||
const state = typeof s === 'string' ? s : s.status
|
|
||||||
|
|
||||||
if (state === 'starting') {
|
|
||||||
return <Chip label={t('common.starting')} color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
|
|
||||||
}
|
|
||||||
if (state === 'stopping') {
|
|
||||||
return <Chip label={t('common.stopping')} color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
|
|
||||||
}
|
|
||||||
const color = state === 'running' ? 'success' : state === 'stopped' ? 'error' : 'warning'
|
|
||||||
|
|
||||||
return <Chip label={t(`common.${state}`).toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResourceMonitor = ({ cpu, memory }: { cpu?: number; memory?: number }) => {
|
|
||||||
if (cpu === undefined && memory === undefined) return null;
|
|
||||||
|
|
||||||
const ramMB = memory ? Math.round(memory / (1024 * 1024)) : 0;
|
|
||||||
const ramLimit = 1024; // 1GB scale for visual
|
|
||||||
const cpuValue = cpu || 0;
|
|
||||||
const ramPercent = Math.min((ramMB / ramLimit) * 100, 100);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ mt: 1.5, mb: 1 }}>
|
|
||||||
<Box sx={{ mb: 1.5 }}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
|
||||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.7rem', fontWeight: 'bold' }}>CPU</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: 'primary.light', fontSize: '0.7rem', fontWeight: 'bold' }}>{cpuValue}%</Typography>
|
|
||||||
</Box>
|
|
||||||
<LinearProgress
|
|
||||||
variant="determinate"
|
|
||||||
value={cpuValue}
|
|
||||||
sx={{
|
|
||||||
height: 4,
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: 'rgba(255,255,255,0.05)',
|
|
||||||
'.MuiLinearProgress-bar': {
|
|
||||||
borderRadius: 2,
|
|
||||||
backgroundImage: `linear-gradient(90deg, #33d9b2 ${cpuValue}%, #ff5252 100%)`,
|
|
||||||
bgcolor: 'transparent'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
|
||||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.7rem', fontWeight: 'bold' }}>RAM</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: 'secondary.light', fontSize: '0.7rem', fontWeight: 'bold' }}>{formatMemory(memory as number)}</Typography>
|
|
||||||
</Box>
|
|
||||||
<LinearProgress
|
|
||||||
variant="determinate"
|
|
||||||
value={ramPercent}
|
|
||||||
sx={{
|
|
||||||
height: 4,
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: 'rgba(255,255,255,0.05)',
|
|
||||||
'.MuiLinearProgress-bar': {
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: 'secondary.main'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleAddProject = async () => {
|
const handleAddProject = async () => {
|
||||||
if (!window.api) return
|
if (!window.api) return
|
||||||
let result;
|
let result;
|
||||||
@@ -638,77 +548,11 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenNginxConfig = async () => {
|
const handleOpenProcessList = async (id: string) => {
|
||||||
|
setSelectedMariaDbId(id)
|
||||||
|
setIsProcessListOpen(true)
|
||||||
if (window.api && window.api.invoke) {
|
if (window.api && window.api.invoke) {
|
||||||
const config = await window.api.invoke('nginx:config:get')
|
await fetchProcessList(id)
|
||||||
setNginxConfig(config)
|
|
||||||
setIsNginxConfigOpen(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveNginxConfig = async () => {
|
|
||||||
if (window.api && window.api.invoke) {
|
|
||||||
const result = await window.api.invoke('nginx:config:save', nginxConfig)
|
|
||||||
if (result.success) {
|
|
||||||
Swal.fire({
|
|
||||||
title: t('common.success'),
|
|
||||||
text: t('server.nginx.config_saved'),
|
|
||||||
icon: 'success',
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff',
|
|
||||||
confirmButtonColor: '#153E5E',
|
|
||||||
customClass: { popup: 'swal2-dark-popup' }
|
|
||||||
})
|
|
||||||
setIsNginxConfigOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpenApacheConfig = async () => {
|
|
||||||
if (window.api && window.api.invoke) {
|
|
||||||
const config = await window.api.invoke('apache:config:get')
|
|
||||||
setApacheConfig(config)
|
|
||||||
setIsApacheConfigOpen(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveApacheConfig = async () => {
|
|
||||||
if (window.api && window.api.invoke) {
|
|
||||||
const result = await window.api.invoke('apache:config:save', apacheConfig)
|
|
||||||
if (result.success) {
|
|
||||||
Swal.fire({
|
|
||||||
title: t('common.success'),
|
|
||||||
text: t('server.apache.config_saved'),
|
|
||||||
icon: 'success',
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff',
|
|
||||||
confirmButtonColor: '#153E5E',
|
|
||||||
customClass: { popup: 'swal2-dark-popup' }
|
|
||||||
})
|
|
||||||
setIsApacheConfigOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTestApacheConfig = async () => {
|
|
||||||
if (window.api && window.api.invoke) {
|
|
||||||
const result = await window.api.invoke('apache:config:test')
|
|
||||||
const formattedMessage = result.message
|
|
||||||
.replace(/failed/g, '<span style="color: #ff5252;">failed</span>')
|
|
||||||
.replace(/successful/g, '<span style="color: #33d9b2;">successful</span>')
|
|
||||||
|
|
||||||
Swal.fire({
|
|
||||||
title: result.success ? 'Başarılı' : 'Yapılandırma Hatası',
|
|
||||||
html: `
|
|
||||||
<div class="nginx-error-container">
|
|
||||||
${formattedMessage}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
icon: result.success ? 'success' : 'error',
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff',
|
|
||||||
width: '600px'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,10 +693,6 @@ function App(): JSX.Element {
|
|||||||
setIsPhpExtensionsOpen(true)
|
setIsPhpExtensionsOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenProcessList = async (id: string) => {
|
|
||||||
setSelectedMariaDbId(id)
|
|
||||||
setActiveTab('db_monitor')
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchProcessList = async (id: string) => {
|
const fetchProcessList = async (id: string) => {
|
||||||
if (!window.api) return
|
if (!window.api) return
|
||||||
@@ -1530,217 +1370,16 @@ function App(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'dashboard' && (
|
{activeTab === 'dashboard' && (
|
||||||
<>
|
<DashboardManager
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
status={status}
|
||||||
<Tabs value={dashboardTab} onChange={(_e, v) => setDashboardTab(v)} textColor="primary" indicatorColor="primary">
|
settings={settings}
|
||||||
<Tab label={t('common.all')} />
|
projects={projects}
|
||||||
<Tab label="Nginx" />
|
phpVersions={phpVersions}
|
||||||
<Tab label="Apache" />
|
mariaDbVersions={mariaDbVersions}
|
||||||
<Tab label="PHP" />
|
t={t}
|
||||||
<Tab label="MariaDB" />
|
onFetchStatus={fetchStatus}
|
||||||
</Tabs>
|
onSelectProject={handleSelectProject}
|
||||||
</Box>
|
/>
|
||||||
|
|
||||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
|
||||||
{/* Nginx Box */}
|
|
||||||
{(dashboardTab === 0 || dashboardTab === 1) && (
|
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
|
||||||
<Paper elevation={4} sx={{
|
|
||||||
p: 3, borderRadius: 3,
|
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.03)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
'&:hover': { transform: 'scale(1.02)' }
|
|
||||||
}}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
|
||||||
<WebIcon color="primary" sx={{ fontSize: 40 }} />
|
|
||||||
{getStatusChip(status.nginx)}
|
|
||||||
</Box>
|
|
||||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Nginx</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">Port: {settings.nginxPort}</Typography>
|
|
||||||
<ResourceMonitor cpu={status.nginx?.cpu} memory={status.nginx?.memory} />
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
||||||
<IconButton onClick={() => handleOpenLogs('nginx')} title={t('diagnostics.logs')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
|
|
||||||
<TerminalIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleOpenNginxConfig}
|
|
||||||
title={t('server.nginx.config')}
|
|
||||||
size="small"
|
|
||||||
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
|
|
||||||
>
|
|
||||||
<SettingsIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
window.api.invoke('services:reload', 'nginx').then((res: any) => {
|
|
||||||
Swal.fire({
|
|
||||||
title: res.success ? t('common.success') : t('common.error'),
|
|
||||||
text: res.message,
|
|
||||||
icon: res.success ? 'success' : 'error',
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff',
|
|
||||||
confirmButtonColor: '#153E5E',
|
|
||||||
customClass: { popup: 'swal2-dark-popup' }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
title={t('server.nginx.save_reload')}
|
|
||||||
size="small"
|
|
||||||
disabled={status.nginx?.status !== 'running'}
|
|
||||||
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
|
|
||||||
>
|
|
||||||
<RefreshIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Switch
|
|
||||||
checked={status.nginx?.status === 'running' || status.nginx?.status === 'starting'}
|
|
||||||
onChange={() => handleToggleService('nginx')}
|
|
||||||
disabled={status.nginx?.status === 'starting'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Apache Box */}
|
|
||||||
{(dashboardTab === 0 || dashboardTab === 2) && (
|
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
|
||||||
<Paper elevation={4} sx={{
|
|
||||||
p: 3, borderRadius: 3,
|
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.03)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
'&:hover': { transform: 'scale(1.02)' }
|
|
||||||
}}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
|
||||||
<ServerIcon color="primary" sx={{ fontSize: 40 }} />
|
|
||||||
{getStatusChip(status.apache)}
|
|
||||||
</Box>
|
|
||||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Apache</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">Port: {settings.apachePort}</Typography>
|
|
||||||
<ResourceMonitor cpu={status.apache?.cpu} memory={status.apache?.memory} />
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
||||||
<IconButton onClick={() => handleOpenLogs('apache')} title={t('diagnostics.logs')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
|
|
||||||
<TerminalIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleOpenApacheConfig}
|
|
||||||
title={t('server.apache.config')}
|
|
||||||
size="small"
|
|
||||||
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
|
|
||||||
>
|
|
||||||
<SettingsIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
window.api.invoke('services:reload', 'apache').then((res: any) => {
|
|
||||||
Swal.fire({
|
|
||||||
title: res.success ? t('common.success') : t('common.error'),
|
|
||||||
text: res.message,
|
|
||||||
icon: res.success ? 'success' : 'error',
|
|
||||||
background: '#1e1e1e',
|
|
||||||
color: '#fff',
|
|
||||||
confirmButtonColor: '#153E5E',
|
|
||||||
customClass: { popup: 'swal2-dark-popup' }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
title={t('server.apache.save_reload')}
|
|
||||||
size="small"
|
|
||||||
disabled={status.apache?.status !== 'running'}
|
|
||||||
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
|
|
||||||
>
|
|
||||||
<RefreshIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Switch
|
|
||||||
checked={status.apache?.status === 'running' || status.apache?.status === 'starting'}
|
|
||||||
onChange={() => handleToggleService('apache')}
|
|
||||||
disabled={status.apache?.status === 'starting'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* PHP Boxes */}
|
|
||||||
{(dashboardTab === 0 || dashboardTab === 3) && installedPhpVersions.map(v => (
|
|
||||||
<Grid item xs={12} sm={6} md={4} key={v.id}>
|
|
||||||
<Paper elevation={4} sx={{
|
|
||||||
p: 3, borderRadius: 3,
|
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.03)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
'&:hover': { transform: 'scale(1.02)' }
|
|
||||||
}}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
|
||||||
<PhpIcon color="primary" sx={{ fontSize: 40 }} />
|
|
||||||
{getStatusChip(status[`php:${v.version}`] || { status: 'stopped' })}
|
|
||||||
</Box>
|
|
||||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>PHP {v.version}</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">Port: {v.port || '?'}</Typography>
|
|
||||||
<ResourceMonitor cpu={status[`php:${v.version}`]?.cpu} memory={status[`php:${v.version}`]?.memory} />
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<IconButton onClick={() => handleOpenLogs(`php:${v.version}`)} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
|
|
||||||
<TerminalIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<Switch
|
|
||||||
checked={status[`php:${v.version}`]?.status === 'running' || status[`php:${v.version}`]?.status === 'starting'}
|
|
||||||
onChange={() => handleToggleService(`php:${v.version}`)}
|
|
||||||
disabled={status[`php:${v.version}`]?.status === 'starting'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* MariaDB Boxes */}
|
|
||||||
{(dashboardTab === 0 || dashboardTab === 4) && mariaDbVersions.filter(v => v.status === 'installed').map(v => (
|
|
||||||
<Grid item xs={12} sm={6} md={4} key={v.id}>
|
|
||||||
<Paper elevation={4} sx={{
|
|
||||||
p: 3, borderRadius: 3,
|
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.03)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
'&:hover': { transform: 'scale(1.02)' }
|
|
||||||
}}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
|
||||||
<DbIcon color="primary" sx={{ fontSize: 40 }} />
|
|
||||||
{getStatusChip(status[`mariadb:${v.version}`] || { status: 'stopped' })}
|
|
||||||
</Box>
|
|
||||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>MariaDB {v.version}</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">Port: {settings.mariaDbPorts?.[v.id] || v.port}</Typography>
|
|
||||||
<ResourceMonitor cpu={status[`mariadb:${v.version}`]?.cpu} memory={status[`mariadb:${v.version}`]?.memory} />
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
||||||
<IconButton onClick={() => handleOpenLogs(`mariadb:${v.version}`)} title={t('diagnostics.logs')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
|
|
||||||
<TerminalIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton onClick={() => handleOpenProcessList(v.id)} title={t('mariadb_process_monitor.title')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}>
|
|
||||||
<ToolsIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton onClick={() => handleResetData(`mariadb:${v.version}`)} title={t('common.reset')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'error.main' } }}>
|
|
||||||
<ResetIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Switch
|
|
||||||
checked={status[`mariadb:${v.version}`]?.status === 'running' || status[`mariadb:${v.version}`]?.status === 'starting'}
|
|
||||||
onChange={() => handleToggleService(`mariadb:${v.version}`)}
|
|
||||||
disabled={status[`mariadb:${v.version}`]?.status === 'starting'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'db_monitor' && (
|
{activeTab === 'db_monitor' && (
|
||||||
|
|||||||
@@ -0,0 +1,462 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
Divider,
|
||||||
|
LinearProgress,
|
||||||
|
CircularProgress,
|
||||||
|
Stack
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Refresh as RefreshIcon,
|
||||||
|
PlayArrow as StartIcon,
|
||||||
|
Stop as StopIcon,
|
||||||
|
OpenInNew as OpenIcon,
|
||||||
|
Save as SaveIcon,
|
||||||
|
Folder as ProjectIcon,
|
||||||
|
Language as WebIcon,
|
||||||
|
Dns as ServerIcon,
|
||||||
|
Code as PhpIcon,
|
||||||
|
Storage as DbIcon
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import 'prismjs/themes/prism-tomorrow.css'
|
||||||
|
import 'prismjs/components/prism-nginx'
|
||||||
|
import 'prismjs/components/prism-apacheconf'
|
||||||
|
import 'prismjs/components/prism-ini'
|
||||||
|
import Editor from 'react-simple-code-editor'
|
||||||
|
import Prism from 'prismjs'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
|
||||||
|
interface DashboardDetailsProps {
|
||||||
|
serviceId: string | null;
|
||||||
|
status: any;
|
||||||
|
settings: any;
|
||||||
|
projects: any[];
|
||||||
|
t: any;
|
||||||
|
onToggleService: (id: string) => void;
|
||||||
|
onReloadService: (id: string) => void;
|
||||||
|
onSelectProject: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardDetails({
|
||||||
|
serviceId,
|
||||||
|
status,
|
||||||
|
settings,
|
||||||
|
projects,
|
||||||
|
t,
|
||||||
|
onToggleService,
|
||||||
|
onReloadService,
|
||||||
|
onSelectProject
|
||||||
|
}: DashboardDetailsProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [configContent, setConfigContent] = useState('');
|
||||||
|
const [projectConfigContent, setProjectConfigContent] = useState('');
|
||||||
|
const [isLoadingConfig, setIsLoadingConfig] = useState(false);
|
||||||
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
|
const logEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const getServiceType = () => {
|
||||||
|
if (!serviceId) return null;
|
||||||
|
if (serviceId === 'nginx') return 'nginx';
|
||||||
|
if (serviceId === 'apache') return 'apache';
|
||||||
|
if (serviceId.startsWith('php:')) return 'php';
|
||||||
|
if (serviceId.startsWith('mariadb:')) return 'mariadb';
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceType = getServiceType();
|
||||||
|
|
||||||
|
const getTemplateName = (): string => {
|
||||||
|
if (serviceId === 'nginx') return 'nginx.conf.template';
|
||||||
|
if (serviceType === 'apache') return 'httpd.conf.template';
|
||||||
|
if (serviceType === 'php') return 'php.ini.template';
|
||||||
|
if (serviceType === 'mariadb') return 'mariadb.ini.template';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectTemplateNames = () => {
|
||||||
|
if (serviceId === 'nginx' || serviceType === 'php') return ['nginx_project_location.conf.template'];
|
||||||
|
if (serviceId === 'apache') return ['apache_project.conf.template'];
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchConfig = async (templateName: string, setter: (v: string) => void) => {
|
||||||
|
if (!templateName) return;
|
||||||
|
setIsLoadingConfig(true);
|
||||||
|
try {
|
||||||
|
const res = await window.api.invoke('config:template:get', templateName);
|
||||||
|
if (res && res.success) {
|
||||||
|
setter(res.content || '');
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch config:', res?.message);
|
||||||
|
setter('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching config:', error);
|
||||||
|
setter('');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingConfig(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConfig = async (templateName: string, content: string) => {
|
||||||
|
const res = await window.api.invoke('config:template:save', { templateName, content });
|
||||||
|
if (res.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: t('common.success'),
|
||||||
|
text: res.message,
|
||||||
|
icon: 'success',
|
||||||
|
background: '#1e1e1e',
|
||||||
|
color: '#fff'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
title: t('common.error'),
|
||||||
|
text: res.message,
|
||||||
|
icon: 'error',
|
||||||
|
background: '#1e1e1e',
|
||||||
|
color: '#fff'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
if (!serviceId) return;
|
||||||
|
const res = await window.api.invoke('services:logs', serviceId);
|
||||||
|
setLogs(res || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHighlightLanguage = (templateName: string) => {
|
||||||
|
if (!templateName) return Prism.languages.clike;
|
||||||
|
if (templateName.includes('nginx')) return Prism.languages.nginx;
|
||||||
|
if (templateName.includes('httpd') || templateName.includes('apache')) return Prism.languages.apacheconf;
|
||||||
|
if (templateName.includes('.ini') || templateName.includes('.conf')) return Prism.languages.ini;
|
||||||
|
return Prism.languages.clike;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHighlightLabel = (templateName: string) => {
|
||||||
|
if (!templateName) return 'clike';
|
||||||
|
if (templateName.includes('nginx')) return 'nginx';
|
||||||
|
if (templateName.includes('httpd') || templateName.includes('apache')) return 'apacheconf';
|
||||||
|
if (templateName.includes('.ini') || templateName.includes('.conf')) return 'ini';
|
||||||
|
return 'clike';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 1) fetchConfig(getTemplateName(), setConfigContent);
|
||||||
|
if (activeTab === 2) {
|
||||||
|
const templates = getProjectTemplateNames();
|
||||||
|
if (templates.length > 0) fetchConfig(templates[0], setProjectConfigContent);
|
||||||
|
}
|
||||||
|
if (activeTab === 3) {
|
||||||
|
fetchLogs();
|
||||||
|
const interval = setInterval(fetchLogs, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [activeTab, serviceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 3 && logEndRef.current) {
|
||||||
|
logEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, [logs, activeTab]);
|
||||||
|
|
||||||
|
if (!serviceId) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}>
|
||||||
|
<Typography variant="h6">{t('dashboard.select_service', 'Lütfen bir servis seçin')}</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStatus = status[serviceId] || { status: 'stopped' };
|
||||||
|
const isRunning = currentStatus.status === 'running';
|
||||||
|
|
||||||
|
const serviceProjects = projects.filter(p => {
|
||||||
|
if (serviceId === 'nginx') return !p.serverType || p.serverType === 'nginx';
|
||||||
|
if (serviceId === 'apache') return p.serverType === 'apache';
|
||||||
|
if (serviceType === 'php') return p.phpVersion === serviceId.split(':')[1];
|
||||||
|
if (serviceType === 'mariadb') return true; // MariaDB is shared (or filter if multi-instance)
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ResourceMonitor = ({ cpu, memory }: { cpu?: number; memory?: number }) => (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Box sx={{ mb: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 'bold' }}>CPU</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 'bold' }}>{cpu || 0}%</Typography>
|
||||||
|
</Box>
|
||||||
|
<LinearProgress variant="determinate" value={cpu || 0} sx={{ height: 6, borderRadius: 3, bgcolor: 'rgba(255,255,255,0.05)' }} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 'bold' }}>RAM</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: 'secondary.light', fontWeight: 'bold' }}>
|
||||||
|
{memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<LinearProgress variant="determinate" value={Math.min(((memory || 0) / (1024 * 1024) / 1024) * 100, 100)} sx={{ height: 6, borderRadius: 3, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { bgcolor: 'secondary.main' } }} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||||
|
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Paper sx={{ p: 1.5, bgcolor: 'rgba(255, 255, 255, 0.05)', borderRadius: 3 }}>
|
||||||
|
{serviceType === 'nginx' && <WebIcon color="primary" fontSize="large" />}
|
||||||
|
{serviceType === 'apache' && <ServerIcon color="primary" fontSize="large" />}
|
||||||
|
{serviceType === 'php' && <PhpIcon color="primary" fontSize="large" />}
|
||||||
|
{serviceType === 'mariadb' && <DbIcon color="primary" fontSize="large" />}
|
||||||
|
</Paper>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 800 }}>{serviceId.toUpperCase()}</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.4)' }}>
|
||||||
|
{isRunning ? t('common.running', 'Çalışıyor') : t('common.stopped', 'Durduruldu')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
{isRunning && (
|
||||||
|
<IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main' }}>
|
||||||
|
<RefreshIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color={isRunning ? 'error' : 'success'}
|
||||||
|
startIcon={isRunning ? <StopIcon /> : <StartIcon />}
|
||||||
|
onClick={() => onToggleService(serviceId)}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
{isRunning ? t('common.stop', 'Durdur') : t('common.start', 'Başlat')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}>
|
||||||
|
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)}>
|
||||||
|
<Tab label={t('common.overview', 'Genel Bakış')} />
|
||||||
|
<Tab label={t('dashboard.service_settings', 'Servis Ayarları')} />
|
||||||
|
{(serviceId === 'nginx' || serviceId === 'apache' || serviceType === 'php') && (
|
||||||
|
<Tab label={t('dashboard.project_settings', 'Proje Ayarları')} />
|
||||||
|
)}
|
||||||
|
<Tab label={t('diagnostics.logs', 'Hata Logları')} />
|
||||||
|
<Tab label={t('projects.title', 'Projeler')} />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3 }}>
|
||||||
|
{activeTab === 0 && (
|
||||||
|
<Grid container spacing={4}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>SYSTEM STATS</Typography>
|
||||||
|
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<ResourceMonitor cpu={currentStatus.cpu} memory={currentStatus.memory} />
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>SERVICE INFO</Typography>
|
||||||
|
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Typography variant="body2" sx={{ opacity: 0.5 }}>Port</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{currentStatus.port || (() => {
|
||||||
|
if (serviceId === 'nginx') return settings.nginxPort;
|
||||||
|
if (serviceId === 'apache') return settings.apachePort;
|
||||||
|
if (serviceType === 'php') return settings.phpPort;
|
||||||
|
if (serviceType === 'mariadb') {
|
||||||
|
const verId = serviceId.split(':')[1];
|
||||||
|
return (settings.mariaDbPorts && settings.mariaDbPorts[verId]) || settings.mariaDbPort;
|
||||||
|
}
|
||||||
|
return 'n/a';
|
||||||
|
})()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Typography variant="body2" sx={{ opacity: 0.5 }}>Status</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: isRunning ? 'success.main' : 'error.main' }}>
|
||||||
|
{currentStatus.status.toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 1 && (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Typography variant="overline" sx={{ fontWeight: 800, color: 'primary.main', letterSpacing: 2 }}>
|
||||||
|
{getTemplateName().toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
onClick={() => saveConfig(getTemplateName(), configContent)}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
{t('common.save', 'KAYDET')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
bgcolor: '#1e1e1e',
|
||||||
|
borderRadius: 3,
|
||||||
|
border: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}>
|
||||||
|
{isLoadingConfig ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||||
|
<Editor
|
||||||
|
value={configContent}
|
||||||
|
onValueChange={setConfigContent}
|
||||||
|
highlight={code => Prism.highlight(code, getHighlightLanguage(getTemplateName()), getHighlightLabel(getTemplateName()))}
|
||||||
|
padding={20}
|
||||||
|
style={{
|
||||||
|
fontFamily: '"Fira Code", "Consolas", monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
minHeight: '100%',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
textareaClassName="editor-textarea"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 2 && (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Typography variant="overline" sx={{ fontWeight: 800, color: 'primary.main', letterSpacing: 2 }}>
|
||||||
|
{getProjectTemplateNames()[0]?.toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
onClick={() => saveConfig(getProjectTemplateNames()[0], projectConfigContent)}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
{t('common.save', 'KAYDET')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
bgcolor: '#1e1e1e',
|
||||||
|
borderRadius: 3,
|
||||||
|
border: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}>
|
||||||
|
{isLoadingConfig ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||||
|
<Editor
|
||||||
|
value={projectConfigContent}
|
||||||
|
onValueChange={setProjectConfigContent}
|
||||||
|
highlight={code => Prism.highlight(code, getHighlightLanguage(getProjectTemplateNames()[0]), getHighlightLabel(getProjectTemplateNames()[0]))}
|
||||||
|
padding={20}
|
||||||
|
style={{
|
||||||
|
fontFamily: '"Fira Code", "Consolas", monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
minHeight: '100%',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
textareaClassName="editor-textarea"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 3 && (
|
||||||
|
<Box sx={{ bgcolor: '#000', borderRadius: 2, p: 2, height: '60vh', overflowY: 'auto' }}>
|
||||||
|
{logs.map((log, i) => (
|
||||||
|
<Typography key={i} sx={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
color: log.toLowerCase().includes('error') ? '#f44336' : (log.toLowerCase().includes('warn') ? '#ff9800' : '#33d9b2'),
|
||||||
|
mb: 0.5
|
||||||
|
}}>
|
||||||
|
{log}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
<div ref={logEndRef} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 4 && (
|
||||||
|
<Paper sx={{ bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)', overflow: 'hidden' }}>
|
||||||
|
<List disablePadding>
|
||||||
|
{serviceProjects.length === 0 ? (
|
||||||
|
<Box sx={{ p: 4, textAlign: 'center', opacity: 0.3 }}><Typography>{t('projects.empty')}</Typography></Box>
|
||||||
|
) : (
|
||||||
|
serviceProjects.map((p, idx) => (
|
||||||
|
<Box key={p.id}>
|
||||||
|
<ListItem>
|
||||||
|
<Paper sx={{ p: 1, mr: 2, bgcolor: 'rgba(255,255,255,0.03)' }}><ProjectIcon sx={{ opacity: 0.5 }} /></Paper>
|
||||||
|
<ListItemText
|
||||||
|
primary={p.name}
|
||||||
|
secondary={`${p.host} • PHP ${p.phpVersion}`}
|
||||||
|
primaryTypographyProps={{ fontWeight: 700 }}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton onClick={() => onSelectProject(p.id)} color="primary">
|
||||||
|
<OpenIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
{idx < serviceProjects.length - 1 && <Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />}
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<style>{`
|
||||||
|
.editor-textarea {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
.token.comment { color: #6a9955; }
|
||||||
|
.token.directive { color: #569cd6; font-weight: bold; }
|
||||||
|
.token.variable { color: #9cdcfe; }
|
||||||
|
.token.string { color: #ce9178; }
|
||||||
|
.token.number { color: #b5cea8; }
|
||||||
|
.token.boolean { color: #569cd6; }
|
||||||
|
.token.operator { color: #d4d4d4; }
|
||||||
|
.token.punctuation { color: #d4d4d4; }
|
||||||
|
.token.keyword { color: #569cd6; }
|
||||||
|
.token.function { color: #dcdcaa; }
|
||||||
|
.token.class-name { color: #4ec9b0; }
|
||||||
|
.token.attribute-name { color: #9cdcfe; }
|
||||||
|
.token.attribute-value { color: #ce9178; }
|
||||||
|
`}</style>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Box } from '@mui/material'
|
||||||
|
import DashboardSidebar from './DashboardSidebar'
|
||||||
|
import DashboardDetails from './DashboardDetails'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
|
||||||
|
interface DashboardManagerProps {
|
||||||
|
status: any;
|
||||||
|
settings: any;
|
||||||
|
projects: any[];
|
||||||
|
phpVersions: any[];
|
||||||
|
mariaDbVersions: any[];
|
||||||
|
t: any;
|
||||||
|
onFetchStatus: () => void;
|
||||||
|
onSelectProject: (projectId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardManager({
|
||||||
|
status,
|
||||||
|
settings,
|
||||||
|
projects,
|
||||||
|
phpVersions,
|
||||||
|
mariaDbVersions,
|
||||||
|
t,
|
||||||
|
onFetchStatus,
|
||||||
|
onSelectProject
|
||||||
|
}: DashboardManagerProps) {
|
||||||
|
const [selectedServiceId, setSelectedServiceId] = useState<string | null>('nginx');
|
||||||
|
|
||||||
|
const handleToggleService = async (service: string) => {
|
||||||
|
if (!window.api) return;
|
||||||
|
|
||||||
|
const currentStatus = status[service]?.status || 'stopped';
|
||||||
|
const isRunning = currentStatus === 'running';
|
||||||
|
|
||||||
|
const result = isRunning
|
||||||
|
? await window.api.stopService(service)
|
||||||
|
: await window.api.startService(service);
|
||||||
|
|
||||||
|
if (result && !result.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: t('common.error'),
|
||||||
|
text: result.message,
|
||||||
|
icon: 'error',
|
||||||
|
background: '#1e1e1e',
|
||||||
|
color: '#fff'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onFetchStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReloadService = async (service: string) => {
|
||||||
|
if (!window.api) return;
|
||||||
|
const result = await window.api.invoke('services:reload', service);
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: result && result.success ? t('common.success') : t('common.error'),
|
||||||
|
text: result ? result.message : 'Error',
|
||||||
|
icon: result && result.success ? 'success' : 'error',
|
||||||
|
background: '#1e1e1e',
|
||||||
|
color: '#fff'
|
||||||
|
});
|
||||||
|
|
||||||
|
onFetchStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', height: 'calc(100vh - 120px)', bgcolor: 'background.default', overflow: 'hidden', borderRadius: 4, border: '1px solid rgba(255, 255, 255, 0.05)', boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}>
|
||||||
|
<DashboardSidebar
|
||||||
|
status={status}
|
||||||
|
phpVersions={phpVersions}
|
||||||
|
mariaDbVersions={mariaDbVersions}
|
||||||
|
selectedServiceId={selectedServiceId}
|
||||||
|
setSelectedServiceId={setSelectedServiceId}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
<DashboardDetails
|
||||||
|
serviceId={selectedServiceId}
|
||||||
|
status={status}
|
||||||
|
settings={settings}
|
||||||
|
projects={projects}
|
||||||
|
t={t}
|
||||||
|
onToggleService={handleToggleService}
|
||||||
|
onReloadService={handleReloadService}
|
||||||
|
onSelectProject={onSelectProject}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
List,
|
||||||
|
ListItemButton,
|
||||||
|
Chip,
|
||||||
|
CircularProgress
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Language as WebIcon,
|
||||||
|
Dns as ServerIcon,
|
||||||
|
Code as PhpIcon,
|
||||||
|
Storage as DbIcon
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
|
interface DashboardSidebarProps {
|
||||||
|
status: any;
|
||||||
|
phpVersions: any[];
|
||||||
|
mariaDbVersions: any[];
|
||||||
|
selectedServiceId: string | null;
|
||||||
|
setSelectedServiceId: (v: string | null) => void;
|
||||||
|
t: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardSidebar({
|
||||||
|
status,
|
||||||
|
phpVersions,
|
||||||
|
mariaDbVersions,
|
||||||
|
selectedServiceId,
|
||||||
|
setSelectedServiceId,
|
||||||
|
t
|
||||||
|
}: DashboardSidebarProps) {
|
||||||
|
|
||||||
|
const installedPhp = phpVersions.filter(v => v.status === 'installed');
|
||||||
|
const installedMariaDb = mariaDbVersions.filter(v => v.status === 'installed');
|
||||||
|
|
||||||
|
const getStatusInfo = (service: string) => {
|
||||||
|
const state = typeof status[service] === 'string' ? status[service] : status[service]?.status || 'stopped';
|
||||||
|
const color = state === 'running' ? 'success' : state === 'stopped' ? 'error' : 'warning';
|
||||||
|
return { state, color };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServiceItem = ({ id, label, icon: Icon, serviceKey }: { id: string, label: string, icon: any, serviceKey: string }) => {
|
||||||
|
const { state } = getStatusInfo(serviceKey);
|
||||||
|
const isRunning = state === 'running';
|
||||||
|
const isStarting = state === 'starting' || state === 'stopping';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
selected={selectedServiceId === id}
|
||||||
|
onClick={() => setSelectedServiceId(id)}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
mb: 0.5,
|
||||||
|
border: selectedServiceId === id ? '1px solid rgba(57, 167, 255, 0.3)' : '1px solid transparent',
|
||||||
|
'&.Mui-selected': { bgcolor: 'rgba(57, 167, 255, 0.08)' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%', gap: 1.5 }}>
|
||||||
|
<Icon sx={{ color: selectedServiceId === id ? 'primary.main' : 'rgba(255,255,255,0.4)', fontSize: '1.2rem' }} />
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
|
<Typography variant="body2" noWrap sx={{ fontWeight: 600 }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: isStarting ? 'warning.main' : (isRunning ? 'success.main' : 'error.main'),
|
||||||
|
boxShadow: isRunning ? '0 0 8px rgba(76, 175, 80, 0.5)' : 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
{isStarting && <CircularProgress size={8} thickness={6} color="inherit" />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
width: 280,
|
||||||
|
borderRight: '1px solid rgba(255, 255, 255, 0.05)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.01)',
|
||||||
|
height: '100%',
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.1rem' }}>
|
||||||
|
{t('dashboard.services', 'Servisler')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
|
||||||
|
WEB SERVERS
|
||||||
|
</Typography>
|
||||||
|
<List dense disablePadding sx={{ mb: 2 }}>
|
||||||
|
<ServiceItem id="nginx" label="Nginx" icon={WebIcon} serviceKey="nginx" />
|
||||||
|
<ServiceItem id="apache" label="Apache" icon={ServerIcon} serviceKey="apache" />
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
|
||||||
|
PHP VERSIONS
|
||||||
|
</Typography>
|
||||||
|
<List dense disablePadding sx={{ mb: 2 }}>
|
||||||
|
{installedPhp.map(v => (
|
||||||
|
<ServiceItem
|
||||||
|
key={v.id}
|
||||||
|
id={`php:${v.version}`}
|
||||||
|
label={`PHP ${v.version}`}
|
||||||
|
icon={PhpIcon}
|
||||||
|
serviceKey={`php:${v.version}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
|
||||||
|
MARIADB VERSIONS
|
||||||
|
</Typography>
|
||||||
|
<List dense disablePadding>
|
||||||
|
{installedMariaDb.map(v => (
|
||||||
|
<ServiceItem
|
||||||
|
key={v.id}
|
||||||
|
id={`mariadb:${v.version}`}
|
||||||
|
label={`MariaDB ${v.version}`}
|
||||||
|
icon={DbIcon}
|
||||||
|
serviceKey={`mariadb:${v.version}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user