feat: implement dashboard sidebar and service management infrastructure
This commit is contained in:
@@ -337,6 +337,26 @@ async function restartApache(): Promise<{ success: boolean; message: string }> {
|
||||
}
|
||||
|
||||
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) => {
|
||||
try {
|
||||
return await mariaDbManagerService.getProcessList(versionId)
|
||||
|
||||
@@ -71,6 +71,26 @@ export class ConfigService {
|
||||
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 {
|
||||
try {
|
||||
if (fs.existsSync(this.settingsPath)) {
|
||||
|
||||
+67
-428
@@ -71,7 +71,6 @@ import {
|
||||
Close as CloseIcon,
|
||||
ContentCopy as ContentCopyIcon,
|
||||
OpenInNew as OpenIcon,
|
||||
Restore as ResetIcon,
|
||||
Tune as TuningIcon,
|
||||
History as HistoryIcon,
|
||||
MenuBook as GuideIcon,
|
||||
@@ -90,6 +89,7 @@ import ServerTuning from './components/ServerTuning'
|
||||
import ServiceEventMonitor from './components/ServiceEventMonitor'
|
||||
import UserGuide from './components/UserGuide'
|
||||
import ProjectManager from './components/ProjectManager/ProjectManager'
|
||||
import DashboardManager from './components/Dashboard/DashboardManager'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -135,7 +135,6 @@ function App(): JSX.Element {
|
||||
const [projectSearch, setProjectSearch] = useState('')
|
||||
const [cliConfig, setCliConfig] = useState<any>({ enabled: true, defaultPhp: '', defaultMariaDb: '', binDir: '' })
|
||||
const [settingsTab, setSettingsTab] = useState(0)
|
||||
const [dashboardTab, setDashboardTab] = useState(0)
|
||||
const [projects, setProjects] = useState<any[]>([])
|
||||
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
||||
const [mariaDbVersions, setMariaDbVersions] = useState<any[]>([])
|
||||
@@ -190,6 +189,11 @@ function App(): JSX.Element {
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||
const [projectDetailTab, setProjectDetailTab] = useState(0)
|
||||
|
||||
const handleSelectProject = (projectId: string) => {
|
||||
setSelectedProjectId(projectId)
|
||||
setActiveTab('projects')
|
||||
}
|
||||
|
||||
const parseNginxError = (message: string) => {
|
||||
const match = message.match(/in (.*):(\d+)/)
|
||||
if (!match) return null
|
||||
@@ -270,9 +274,10 @@ function App(): JSX.Element {
|
||||
}
|
||||
|
||||
const fetchApacheVersions = async () => {
|
||||
if (!window.api || !window.api.getApacheVersions) return
|
||||
const v = await window.api.getApacheVersions()
|
||||
setApacheVersions(v || [])
|
||||
if (window.api && window.api.getApacheVersions) {
|
||||
const list = await window.api.getApacheVersions()
|
||||
setApacheVersions(list || [])
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLogs = async (service: string) => {
|
||||
@@ -385,6 +390,49 @@ function App(): JSX.Element {
|
||||
}
|
||||
}, [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 () => {
|
||||
if (window.api && 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 () => {
|
||||
if (window.api && window.api.saveSettings) {
|
||||
const result = await window.api.saveSettings(settings)
|
||||
@@ -532,74 +510,6 @@ function App(): JSX.Element {
|
||||
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 () => {
|
||||
if (!window.api) return
|
||||
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) {
|
||||
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',
|
||||
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'
|
||||
})
|
||||
await fetchProcessList(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,10 +693,6 @@ function App(): JSX.Element {
|
||||
setIsPhpExtensionsOpen(true)
|
||||
}
|
||||
|
||||
const handleOpenProcessList = async (id: string) => {
|
||||
setSelectedMariaDbId(id)
|
||||
setActiveTab('db_monitor')
|
||||
}
|
||||
|
||||
const fetchProcessList = async (id: string) => {
|
||||
if (!window.api) return
|
||||
@@ -1530,217 +1370,16 @@ function App(): JSX.Element {
|
||||
)}
|
||||
|
||||
{activeTab === 'dashboard' && (
|
||||
<>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={dashboardTab} onChange={(_e, v) => setDashboardTab(v)} textColor="primary" indicatorColor="primary">
|
||||
<Tab label={t('common.all')} />
|
||||
<Tab label="Nginx" />
|
||||
<Tab label="Apache" />
|
||||
<Tab label="PHP" />
|
||||
<Tab label="MariaDB" />
|
||||
</Tabs>
|
||||
</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>
|
||||
</>
|
||||
<DashboardManager
|
||||
status={status}
|
||||
settings={settings}
|
||||
projects={projects}
|
||||
phpVersions={phpVersions}
|
||||
mariaDbVersions={mariaDbVersions}
|
||||
t={t}
|
||||
onFetchStatus={fetchStatus}
|
||||
onSelectProject={handleSelectProject}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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