feat: implement project manager dashboard with resource monitoring and multi-language support
This commit is contained in:
@@ -1,4 +1,296 @@
|
||||
[
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "lgv4wfd9z",
|
||||
"timestamp": 1775140992722
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "5dk3998of",
|
||||
"timestamp": 1775140991358
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "cbna1e6ai",
|
||||
"timestamp": 1775140987786
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "tjf4mzo3m",
|
||||
"timestamp": 1775140986436
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "7ygjwfbhs",
|
||||
"timestamp": 1775140953402
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "w02iyi0or",
|
||||
"timestamp": 1775140952366
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "0klrn76qy",
|
||||
"timestamp": 1775140941299
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "db5o443dt",
|
||||
"timestamp": 1775140941292
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: null",
|
||||
"id": "zfqnezt7g",
|
||||
"timestamp": 1775140941010
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "stop",
|
||||
"status": "info",
|
||||
"id": "70y5a19bj",
|
||||
"timestamp": 1775140940997
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: null",
|
||||
"id": "xdkf6g1n3",
|
||||
"timestamp": 1775140940604
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "stop",
|
||||
"status": "info",
|
||||
"id": "g2phz0x6z",
|
||||
"timestamp": 1775140940596
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "k3qhdtk8j",
|
||||
"timestamp": 1775140913371
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "pmz5hp51i",
|
||||
"timestamp": 1775140913348
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "d6h8pfi4x",
|
||||
"timestamp": 1775140913325
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "61svs2nni",
|
||||
"timestamp": 1775140913315
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "sh8b2psvq",
|
||||
"timestamp": 1775140913286
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "xlewxzma7",
|
||||
"timestamp": 1775140913265
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "fxljacg0r",
|
||||
"timestamp": 1775140644165
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "2x2e657x6",
|
||||
"timestamp": 1775140644143
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "bofkspx9l",
|
||||
"timestamp": 1775140644120
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "p19gbtez8",
|
||||
"timestamp": 1775140644109
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "tpwmg05yt",
|
||||
"timestamp": 1775140644091
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "exit",
|
||||
"status": "failure",
|
||||
"details": "Exit code: 1",
|
||||
"id": "rjr42oskg",
|
||||
"timestamp": 1775140644044
|
||||
},
|
||||
{
|
||||
"service": "php:5.6.40",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "q5322glbz",
|
||||
"timestamp": 1775140633948
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "hbumn81gj",
|
||||
"timestamp": 1775140633920
|
||||
},
|
||||
{
|
||||
"service": "php:8.2.30",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "jkel0o90s",
|
||||
"timestamp": 1775140633897
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "aan0ox1gd",
|
||||
"timestamp": 1775140633876
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "3uk4uaxt4",
|
||||
"timestamp": 1775140633650
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "rridkshjr",
|
||||
"timestamp": 1775140633626
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "ckihmcjmc",
|
||||
"timestamp": 1775140632885
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "b2qv8h1o2",
|
||||
"timestamp": 1775140632811
|
||||
},
|
||||
{
|
||||
"service": "php:5.6.40",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "zf5suzjqk",
|
||||
"timestamp": 1775139676530
|
||||
},
|
||||
{
|
||||
"service": "php:7.4.33",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "ilid4cesf",
|
||||
"timestamp": 1775139676510
|
||||
},
|
||||
{
|
||||
"service": "php:8.2.30",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "w42ipljsj",
|
||||
"timestamp": 1775139676489
|
||||
},
|
||||
{
|
||||
"service": "php:8.5.4",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "0nji2mx0x",
|
||||
"timestamp": 1775139676471
|
||||
},
|
||||
{
|
||||
"service": "mariadb:11.4.2",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "mi80ksddx",
|
||||
"timestamp": 1775139676130
|
||||
},
|
||||
{
|
||||
"service": "mariadb:10.11.8",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "wy2jqt3or",
|
||||
"timestamp": 1775139676106
|
||||
},
|
||||
{
|
||||
"service": "apache",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "18fd2tclr",
|
||||
"timestamp": 1775139675486
|
||||
},
|
||||
{
|
||||
"service": "nginx",
|
||||
"action": "start",
|
||||
"status": "success",
|
||||
"id": "shmd1zuyl",
|
||||
"timestamp": 1775139675417
|
||||
},
|
||||
{
|
||||
"service": "php:5.6.40",
|
||||
"action": "start",
|
||||
|
||||
+51
-250
@@ -39,10 +39,9 @@ import {
|
||||
Tabs,
|
||||
Tab,
|
||||
InputAdornment,
|
||||
Grid,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
Grid,
|
||||
Avatar,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -72,16 +71,11 @@ import {
|
||||
Close as CloseIcon,
|
||||
ContentCopy as ContentCopyIcon,
|
||||
OpenInNew as OpenIcon,
|
||||
Edit as EditIcon,
|
||||
Folder as FolderIcon,
|
||||
Language as HostIcon,
|
||||
Launch as LaunchIcon,
|
||||
Handyman as ToolsIcon,
|
||||
Restore as ResetIcon,
|
||||
Search as SearchIcon,
|
||||
Tune as TuningIcon,
|
||||
History as HistoryIcon,
|
||||
MenuBook as GuideIcon
|
||||
MenuBook as GuideIcon,
|
||||
Handyman as ToolsIcon
|
||||
} from '@mui/icons-material'
|
||||
import Swal from 'sweetalert2'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -95,6 +89,7 @@ import PhpExtensionManager from './components/PhpExtensionManager'
|
||||
import ServerTuning from './components/ServerTuning'
|
||||
import ServiceEventMonitor from './components/ServiceEventMonitor'
|
||||
import UserGuide from './components/UserGuide'
|
||||
import ProjectManager from './components/ProjectManager/ProjectManager'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -192,6 +187,8 @@ function App(): JSX.Element {
|
||||
const [selectedProjectHost, setSelectedProjectHost] = useState('')
|
||||
const [isPhpExtensionsOpen, setIsPhpExtensionsOpen] = useState(false)
|
||||
const [selectedPhpVersion, setSelectedPhpVersion] = useState('')
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||
const [projectDetailTab, setProjectDetailTab] = useState(0)
|
||||
|
||||
const parseNginxError = (message: string) => {
|
||||
const match = message.match(/in (.*):(\d+)/)
|
||||
@@ -606,70 +603,15 @@ function App(): JSX.Element {
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectResourceMonitor = ({ serverStats, phpStats, serverType }: { serverStats?: ServiceInfo; phpStats?: ServiceInfo; serverType: string }) => {
|
||||
const hasServerStat = serverStats?.cpu !== undefined || serverStats?.memory !== undefined;
|
||||
const hasPhpStat = phpStats?.cpu !== undefined || phpStats?.memory !== undefined;
|
||||
|
||||
if (!hasServerStat && !hasPhpStat) return null;
|
||||
|
||||
const renderMiniStats = (stats?: ServiceInfo, label: string = 'Service') => {
|
||||
if (!stats) return null;
|
||||
const cpu = stats.cpu || 0;
|
||||
const memory = stats.memory || 0;
|
||||
const ramMB = Math.round(memory / (1024 * 1024));
|
||||
const ramPercent = Math.min((ramMB / 1024) * 100, 100);
|
||||
|
||||
return (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.65rem', fontWeight: 700 }}>{label.toUpperCase()}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: 'primary.light', fontSize: '0.65rem', fontWeight: 700 }}>{cpu}%</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'secondary.light', fontSize: '0.65rem', fontWeight: 700 }}>{formatMemory(memory)}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: '2px' }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={cpu}
|
||||
sx={{
|
||||
height: 3,
|
||||
borderRadius: 1,
|
||||
flex: 1,
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
'.MuiLinearProgress-bar': { borderRadius: 1, backgroundImage: `linear-gradient(90deg, #33d9b2 ${cpu}%, #ff5252 100%)`, bgcolor: 'transparent' }
|
||||
}}
|
||||
/>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={ramPercent}
|
||||
sx={{
|
||||
height: 3,
|
||||
borderRadius: 1,
|
||||
flex: 1,
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
'.MuiLinearProgress-bar': { borderRadius: 1, bgcolor: 'secondary.main' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1.5, display: 'flex', gap: 2 }}>
|
||||
{renderMiniStats(serverStats, serverType)}
|
||||
{renderMiniStats(phpStats, 'PHP')}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddProject = async () => {
|
||||
if (!window.api) return
|
||||
let result;
|
||||
if (newProject.id) {
|
||||
await window.api.updateProject(newProject)
|
||||
result = await window.api.updateProject(newProject)
|
||||
} else {
|
||||
await window.api.addProject(newProject)
|
||||
result = await window.api.addProject(newProject)
|
||||
if (result && result.id) setSelectedProjectId(result.id)
|
||||
}
|
||||
setIsProjectDialogOpen(false)
|
||||
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost' })
|
||||
@@ -694,6 +636,7 @@ function App(): JSX.Element {
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await window.api.removeProject(id)
|
||||
if (selectedProjectId === id) setSelectedProjectId(null)
|
||||
setNotification({ open: true, message: t('projects.deleted'), severity: 'success' })
|
||||
fetchProjects()
|
||||
}
|
||||
@@ -2008,192 +1951,50 @@ function App(): JSX.Element {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{activeTab === 'projects' && (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 2, alignItems: { xs: 'stretch', sm: 'center' }, mb: 3 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 'medium', flexGrow: 1 }}>
|
||||
{t('projects.title')}
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder={t('projects.search')}
|
||||
value={projectSearch}
|
||||
onChange={(e) => setProjectSearch(e.target.value)}
|
||||
sx={{
|
||||
minWidth: { sm: 300 },
|
||||
bgcolor: 'rgba(255, 255, 255, 0.03)',
|
||||
borderRadius: 1,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: '#fff',
|
||||
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.1)' },
|
||||
'&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.2)' },
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ color: 'rgba(255, 255, 255, 0.3)' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: projectSearch && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton size="small" onClick={() => setProjectSearch('')} sx={{ color: 'rgba(255, 255, 255, 0.3)' }}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" startIcon={<ProjectIcon />} onClick={() => setIsProjectDialogOpen(true)}>
|
||||
{t('projects.add_new')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<Paper sx={{ p: 4, textAlign: 'center', bgcolor: 'rgba(255, 255, 255, 0.03)' }}>
|
||||
<Typography variant="body1" color="text.secondary">{t('projects.empty')}</Typography>
|
||||
</Paper>
|
||||
) : (() => {
|
||||
const filteredProjects = projects.filter(p =>
|
||||
p.name.toLowerCase().includes(projectSearch.toLowerCase()) ||
|
||||
p.host.toLowerCase().includes(projectSearch.toLowerCase())
|
||||
<ProjectManager
|
||||
projects={projects}
|
||||
status={status}
|
||||
settings={settings}
|
||||
projectSearch={projectSearch}
|
||||
setProjectSearch={setProjectSearch}
|
||||
selectedProjectId={selectedProjectId}
|
||||
setSelectedProjectId={setSelectedProjectId}
|
||||
projectDetailTab={projectDetailTab}
|
||||
setProjectDetailTab={setProjectDetailTab}
|
||||
t={t}
|
||||
onAddProject={() => {
|
||||
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost', serverType: 'nginx' })
|
||||
setIsProjectDialogOpen(true)
|
||||
}}
|
||||
onEditProject={(p) => {
|
||||
setNewProject(p)
|
||||
setIsProjectDialogOpen(true)
|
||||
}}
|
||||
onRemoveProject={handleRemoveProject}
|
||||
onToggleStatus={async (p: any) => {
|
||||
const isRunning = (
|
||||
(p.serverType === 'apache' ? status.apache?.status : status.nginx?.status) === 'running' &&
|
||||
status[`php:${p.phpVersion}`]?.status === 'running'
|
||||
);
|
||||
|
||||
if (filteredProjects.length === 0) {
|
||||
return (
|
||||
<Paper sx={{ p: 6, textAlign: 'center', bgcolor: 'rgba(255, 255, 255, 0.02)', border: '1px dashed rgba(255, 255, 255, 0.1)' }}>
|
||||
<SearchIcon sx={{ fontSize: 48, color: 'rgba(255, 255, 255, 0.1)', mb: 2 }} />
|
||||
<Typography variant="h6" sx={{ color: 'rgba(255, 255, 255, 0.4)' }}>
|
||||
{t('common.not_found', 'Eşleşen proje bulunamadı')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
if (isRunning) {
|
||||
await window.api.stopService(p.serverType === 'apache' ? 'apache' : 'nginx');
|
||||
await window.api.stopService(`php:${p.phpVersion}`);
|
||||
} else {
|
||||
await window.api.startService(p.serverType === 'apache' ? 'apache' : 'nginx');
|
||||
await window.api.startService(`php:${p.phpVersion}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{filteredProjects.map((project) => (
|
||||
<Grid item xs={12} md={6} key={project.id}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2.5,
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 3,
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 255, 255, 0.06)',
|
||||
transform: 'translateY(-4px)',
|
||||
borderColor: 'rgba(57, 167, 255, 0.3)',
|
||||
boxShadow: '0 12px 24px rgba(0,0,0,0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2, gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, minWidth: 0 }}>
|
||||
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: '#39A7FF', flexShrink: 0 }}>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
|
||||
<Typography variant="h6" noWrap sx={{ fontWeight: 700, color: '#fff', lineHeight: 1.2, textOverflow: 'ellipsis' }}>
|
||||
{project.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" noWrap sx={{ color: 'rgba(255, 255, 255, 0.4)', fontFamily: 'monospace', display: 'block', textOverflow: 'ellipsis' }}>
|
||||
{project.path.length > 40 ? '...' + project.path.slice(-37) : project.path}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexShrink: 0 }}>
|
||||
<Tooltip title={t('common.edit')}>
|
||||
<IconButton size="small" onClick={() => {
|
||||
setNewProject(project)
|
||||
setIsProjectDialogOpen(true)
|
||||
}} sx={{ color: 'rgba(255,255,255,0.4)', '&:hover': { color: '#39A7FF' } }}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{project.serverType !== 'apache' && (
|
||||
<Tooltip title={t('server.nginx.config')}>
|
||||
<IconButton size="small" onClick={() => handleOpenProjectNginxConfig(project.host)} sx={{ color: 'rgba(255,255,255,0.4)', '&:hover': { color: '#39A7FF' } }}>
|
||||
<TerminalIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Sil">
|
||||
<IconButton size="small" onClick={() => handleRemoveProject(project.id)} sx={{ color: 'rgba(255,255,255,0.4)', '&:hover': { color: '#ff4444' } }}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Stack spacing={1.5}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<PhpIcon sx={{ fontSize: '1rem !important' }} />}
|
||||
label={`PHP ${project.phpVersion}`}
|
||||
sx={{ bgcolor: 'rgba(71, 74, 255, 0.1)', color: '#777bb4', fontWeight: 600, border: '1px solid rgba(71, 74, 255, 0.2)' }}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
icon={project.serverType === 'apache' ? <ServerIcon sx={{ fontSize: '1rem !important' }} /> : <WebIcon sx={{ fontSize: '1rem !important' }} />}
|
||||
label={project.serverType === 'apache' ? 'Apache' : 'Nginx'}
|
||||
sx={{ bgcolor: 'rgba(255, 255, 255, 0.05)', color: '#fff', fontWeight: 600 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<ProjectResourceMonitor
|
||||
serverType={project.serverType === 'apache' ? 'Apache' : 'Nginx'}
|
||||
serverStats={project.serverType === 'apache' ? status.apache : status.nginx}
|
||||
phpStats={status[`php:${project.phpVersion}`]}
|
||||
/>
|
||||
|
||||
<Paper variant="outlined" sx={{
|
||||
p: 1.5,
|
||||
bgcolor: 'rgba(0,0,0,0.2)',
|
||||
borderColor: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<HostIcon sx={{ fontSize: '1rem', color: 'rgba(255,255,255,0.3)' }} />
|
||||
<Typography variant="body2" sx={{ color: '#fff', fontWeight: 500, fontFamily: 'monospace' }}>
|
||||
{project.host}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<LaunchIcon />}
|
||||
onClick={() => {
|
||||
const port = project.serverType === 'apache' ? settings.apachePort : settings.nginxPort
|
||||
window.open(`http://localhost:${port}/${project.host}/`, '_blank')
|
||||
}}
|
||||
sx={{
|
||||
color: '#39A7FF',
|
||||
textTransform: 'none',
|
||||
fontSize: '0.75rem',
|
||||
p: '2px 8px',
|
||||
'&:hover': { bgcolor: 'rgba(57, 167, 255, 0.1)' }
|
||||
}}
|
||||
>
|
||||
{t('projects.browse')}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
})()}
|
||||
</Box>
|
||||
fetchStatus();
|
||||
}}
|
||||
onOpenSite={(p: any) => {
|
||||
const port = p.serverType === 'apache' ? settings.apachePort : settings.nginxPort
|
||||
window.open(`http://localhost:${port}/${p.host}/`, '_blank')
|
||||
}}
|
||||
onOpenFolder={(path: string) => window.api.invoke('open-path', path)}
|
||||
onManageDb={() => setActiveTab('db_manager')}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Container>
|
||||
</Box >
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Avatar,
|
||||
IconButton,
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
Grid,
|
||||
Chip,
|
||||
Divider,
|
||||
Stack,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Folder as ProjectIcon,
|
||||
FolderOpen as FolderOpenIcon,
|
||||
Stop as StopIcon,
|
||||
PlayArrow as StartIcon,
|
||||
Launch as LaunchIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
Tune as TuningIcon,
|
||||
OpenInNew as OpenIcon
|
||||
} from '@mui/icons-material'
|
||||
import ProjectResourceMonitor from './ProjectResourceMonitor'
|
||||
|
||||
interface ProjectDetailsProps {
|
||||
project: any;
|
||||
status: any;
|
||||
settings: any;
|
||||
projectDetailTab: number;
|
||||
setProjectDetailTab: (v: number) => void;
|
||||
t: any;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onToggleStatus: () => void;
|
||||
onOpenSite: () => void;
|
||||
onOpenFolder: () => void;
|
||||
onManageDb: () => void;
|
||||
}
|
||||
|
||||
export default function ProjectDetails({
|
||||
project: p,
|
||||
status,
|
||||
settings,
|
||||
projectDetailTab,
|
||||
setProjectDetailTab,
|
||||
t,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onToggleStatus,
|
||||
onOpenSite,
|
||||
onOpenFolder,
|
||||
onManageDb
|
||||
}: ProjectDetailsProps) {
|
||||
if (!p) {
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: 0.3 }}>
|
||||
<ProjectIcon sx={{ fontSize: 80, mb: 2 }} />
|
||||
<Typography variant="h6">{t('projects.no_project_selected')}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const isRunning = (
|
||||
(p.serverType === 'apache' ? status.apache?.status : status.nginx?.status) === 'running' &&
|
||||
status[`php:${p.phpVersion}`]?.status === 'running'
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', bgcolor: 'rgba(0,0,0,0.1)', height: '100%', overflow: 'hidden' }}>
|
||||
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main', width: 48, height: 48 }}>
|
||||
<ProjectIcon />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>{p.name}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontFamily: 'monospace' }}>
|
||||
{p.path}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={onOpenFolder} sx={{ color: 'rgba(255,255,255,0.2)', p: 0.5 }}>
|
||||
<FolderOpenIcon sx={{ fontSize: '0.9rem' }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1.5 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={isRunning ? <StopIcon /> : <StartIcon />}
|
||||
color={isRunning ? 'error' : 'primary'}
|
||||
onClick={onToggleStatus}
|
||||
>
|
||||
{isRunning ? t('projects.stop_site') : t('projects.start_site')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<LaunchIcon />}
|
||||
onClick={onOpenSite}
|
||||
>
|
||||
{t('projects.open_site')}
|
||||
</Button>
|
||||
<Tooltip title={t('common.edit')}>
|
||||
<IconButton onClick={onEdit} sx={{ border: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<IconButton onClick={onDelete} sx={{ border: '1px solid rgba(255,255,255,0.1)', '&:hover': { color: 'error.main', borderColor: 'error.main' } }}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}>
|
||||
<Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary">
|
||||
<Tab label={t('projects.overview')} />
|
||||
<Tab label={t('projects.database')} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ p: 4, flexGrow: 1, overflowY: 'auto' }}>
|
||||
{projectDetailTab === 0 && (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.site_domain')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.host}.local</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.web_server')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.serverType.toUpperCase()}</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.php_version')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 0.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>{p.phpVersion}</Typography>
|
||||
<Chip label="CHANGE" size="small" variant="outlined" component="button" onClick={onEdit} sx={{ cursor: 'pointer', height: 20, fontSize: '0.65rem' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TuningIcon fontSize="small" color="primary" /> {t('dashboard.status')}
|
||||
</Typography>
|
||||
<ProjectResourceMonitor
|
||||
serverType={p.serverType === 'apache' ? 'Apache' : 'Nginx'}
|
||||
serverStats={p.serverType === 'apache' ? status.apache : status.nginx}
|
||||
phpStats={status[`php:${p.phpVersion}`]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{projectDetailTab === 1 && (
|
||||
<Box sx={{ maxWidth: 600 }}>
|
||||
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700 }}>{t('projects.mariadb_version')}</Typography>
|
||||
<Typography sx={{ mt: 0.5, fontWeight: 600 }}>MariaDB {p.mariadbVersion}</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<OpenIcon />}
|
||||
onClick={onManageDb}
|
||||
>
|
||||
{t('mariadb_manager.title')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3, opacity: 0.1 }} />
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Host</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>localhost</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Port</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{settings.mariaDbPort}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Username</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>root</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Password</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>(no password)</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Box } from '@mui/material'
|
||||
import ProjectSidebar from './ProjectSidebar'
|
||||
import ProjectDetails from './ProjectDetails'
|
||||
|
||||
interface ProjectManagerProps {
|
||||
projects: any[];
|
||||
status: any;
|
||||
settings: any;
|
||||
projectSearch: string;
|
||||
setProjectSearch: (v: string) => void;
|
||||
selectedProjectId: string | null;
|
||||
setSelectedProjectId: (v: string | null) => void;
|
||||
projectDetailTab: number;
|
||||
setProjectDetailTab: (v: number) => void;
|
||||
t: any;
|
||||
onAddProject: () => void;
|
||||
onEditProject: (p: any) => void;
|
||||
onRemoveProject: (id: string) => void;
|
||||
onToggleStatus: (p: any) => void;
|
||||
onOpenSite: (p: any) => void;
|
||||
onOpenFolder: (path: string) => void;
|
||||
onManageDb: () => void;
|
||||
}
|
||||
|
||||
export default function ProjectManager({
|
||||
projects,
|
||||
status,
|
||||
settings,
|
||||
projectSearch,
|
||||
setProjectSearch,
|
||||
selectedProjectId,
|
||||
setSelectedProjectId,
|
||||
projectDetailTab,
|
||||
setProjectDetailTab,
|
||||
t,
|
||||
onAddProject,
|
||||
onEditProject,
|
||||
onRemoveProject,
|
||||
onToggleStatus,
|
||||
onOpenSite,
|
||||
onOpenFolder,
|
||||
onManageDb
|
||||
}: ProjectManagerProps) {
|
||||
|
||||
// Defensive check: find selected project
|
||||
const p = projects.find(proj => proj && proj.id === selectedProjectId);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
height: 'calc(100vh - 140px)',
|
||||
m: -2,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<ProjectSidebar
|
||||
projects={projects}
|
||||
projectSearch={projectSearch}
|
||||
setProjectSearch={setProjectSearch}
|
||||
selectedProjectId={selectedProjectId}
|
||||
setSelectedProjectId={setSelectedProjectId}
|
||||
status={status}
|
||||
t={t}
|
||||
onAddProject={onAddProject}
|
||||
/>
|
||||
|
||||
<ProjectDetails
|
||||
project={p}
|
||||
status={status}
|
||||
settings={settings}
|
||||
projectDetailTab={projectDetailTab}
|
||||
setProjectDetailTab={setProjectDetailTab}
|
||||
t={t}
|
||||
onEdit={() => onEditProject(p)}
|
||||
onDelete={() => onRemoveProject(p.id)}
|
||||
onToggleStatus={() => onToggleStatus(p)}
|
||||
onOpenSite={() => onOpenSite(p)}
|
||||
onOpenFolder={() => onOpenFolder(p.path)}
|
||||
onManageDb={onManageDb}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Box, Typography, LinearProgress } from '@mui/material'
|
||||
|
||||
interface ServiceInfo {
|
||||
status: string;
|
||||
cpu?: number;
|
||||
memory?: number;
|
||||
}
|
||||
|
||||
interface ProjectResourceMonitorProps {
|
||||
serverStats?: ServiceInfo;
|
||||
phpStats?: ServiceInfo;
|
||||
serverType: string;
|
||||
}
|
||||
|
||||
const formatMemory = (bytes?: number) => {
|
||||
if (!bytes) return ''
|
||||
const mb = bytes / (1024 * 1024)
|
||||
if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB`
|
||||
return `${Math.round(mb)} MB`
|
||||
}
|
||||
|
||||
export default function ProjectResourceMonitor({ serverStats, phpStats, serverType }: ProjectResourceMonitorProps) {
|
||||
const hasServerStat = serverStats?.cpu !== undefined || serverStats?.memory !== undefined;
|
||||
const hasPhpStat = phpStats?.cpu !== undefined || phpStats?.memory !== undefined;
|
||||
|
||||
if (!hasServerStat && !hasPhpStat) return null;
|
||||
|
||||
const renderMiniStats = (stats?: ServiceInfo, label: string = 'Service') => {
|
||||
if (!stats) return null;
|
||||
const cpu = stats.cpu || 0;
|
||||
const memory = stats.memory || 0;
|
||||
const ramMB = Math.round(memory / (1024 * 1024));
|
||||
const ramPercent = Math.min((ramMB / 1024) * 100, 100);
|
||||
|
||||
return (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.65rem', fontWeight: 700 }}>{label.toUpperCase()}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: 'primary.light', fontSize: '0.65rem', fontWeight: 700 }}>{cpu}%</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'secondary.light', fontSize: '0.65rem', fontWeight: 700 }}>{formatMemory(memory)}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: '2px' }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={cpu}
|
||||
sx={{
|
||||
height: 3,
|
||||
borderRadius: 1,
|
||||
flex: 1,
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
'.MuiLinearProgress-bar': { borderRadius: 1, backgroundImage: `linear-gradient(90deg, #33d9b2 ${cpu}%, #ff5252 100%)`, bgcolor: 'transparent' }
|
||||
}}
|
||||
/>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={ramPercent}
|
||||
sx={{
|
||||
height: 3,
|
||||
borderRadius: 1,
|
||||
flex: 1,
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
'.MuiLinearProgress-bar': { borderRadius: 1, bgcolor: 'secondary.main' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1.5, display: 'flex', gap: 2 }}>
|
||||
{renderMiniStats(serverStats, serverType)}
|
||||
{renderMiniStats(phpStats, 'PHP')}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
List,
|
||||
ListItemButton
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Folder as ProjectIcon
|
||||
} from '@mui/icons-material'
|
||||
|
||||
interface ProjectSidebarProps {
|
||||
projects: any[];
|
||||
projectSearch: string;
|
||||
setProjectSearch: (v: string) => void;
|
||||
selectedProjectId: string | null;
|
||||
setSelectedProjectId: (v: string | null) => void;
|
||||
status: any;
|
||||
t: any;
|
||||
onAddProject: () => void;
|
||||
}
|
||||
|
||||
export default function ProjectSidebar({
|
||||
projects,
|
||||
projectSearch,
|
||||
setProjectSearch,
|
||||
selectedProjectId,
|
||||
setSelectedProjectId,
|
||||
status,
|
||||
t,
|
||||
onAddProject
|
||||
}: ProjectSidebarProps) {
|
||||
|
||||
const filtered = projects.filter(p => {
|
||||
if (!p) return false;
|
||||
const nameMatch = p.name?.toLowerCase().includes(projectSearch.toLowerCase()) ?? false;
|
||||
const hostMatch = p.host?.toLowerCase().includes(projectSearch.toLowerCase()) ?? false;
|
||||
return nameMatch || hostMatch;
|
||||
});
|
||||
|
||||
// Grouping
|
||||
const grouped = filtered.reduce((acc: any, p) => {
|
||||
const v = p.phpVersion ? `PHP ${p.phpVersion}` : 'Unknown PHP';
|
||||
if (!acc[v]) acc[v] = [];
|
||||
acc[v].push(p);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: 320,
|
||||
borderRight: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
bgcolor: 'rgba(255, 255, 255, 0.01)',
|
||||
height: '100%'
|
||||
}}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, alignItems: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, flexGrow: 1, fontSize: '1.1rem' }}>
|
||||
{t('projects.title')}
|
||||
</Typography>
|
||||
<Tooltip title={t('projects.add_new')}>
|
||||
<IconButton size="small" color="primary" onClick={onAddProject} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)' }}>
|
||||
<ProjectIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder={t('projects.search')}
|
||||
value={projectSearch}
|
||||
onChange={(e) => setProjectSearch(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ color: 'rgba(255, 255, 255, 0.2)', fontSize: '1.1rem' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
sx: { fontSize: '0.85rem', bgcolor: 'rgba(0,0,0,0.2)' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 1 }}>
|
||||
{filtered.length === 0 ? (
|
||||
<Box sx={{ p: 4, textAlign: 'center', color: 'rgba(255,255,255,0.2)' }}>
|
||||
<Typography variant="body2">{t('projects.empty')}</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
Object.entries(grouped).sort().map(([version, projs]: [string, any]) => (
|
||||
<Box key={version} sx={{ mb: 2 }}>
|
||||
<Typography variant="overline" sx={{ px: 1.5, color: 'primary.main', fontWeight: 800, letterSpacing: 1 }}>
|
||||
{version}
|
||||
</Typography>
|
||||
<List dense disablePadding>
|
||||
{projs.map((p: any) => {
|
||||
const isRunning = (
|
||||
(p.serverType === 'apache' ? status.apache?.status : status.nginx?.status) === 'running' &&
|
||||
status[`php:${p.phpVersion}`]?.status === 'running'
|
||||
);
|
||||
const serverStats = p.serverType === 'apache' ? status.apache : status.nginx;
|
||||
const phpStats = status[`php:${p.phpVersion}`];
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
key={p.id}
|
||||
selected={selectedProjectId === p.id}
|
||||
onClick={() => setSelectedProjectId(p.id)}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
mb: 0.5,
|
||||
border: selectedProjectId === p.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', flexDirection: 'column', width: '100%', gap: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: isRunning ? 'success.main' : 'error.main',
|
||||
boxShadow: isRunning ? '0 0 8px rgba(76, 175, 80, 0.5)' : 'none'
|
||||
}} />
|
||||
<Typography variant="body2" noWrap sx={{ fontWeight: 600, flexGrow: 1 }}>
|
||||
{p.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.7rem' }}>
|
||||
{(p.serverType || '').toUpperCase()} • {p.host}
|
||||
</Typography>
|
||||
{isRunning && (
|
||||
<Typography variant="caption" sx={{ color: 'primary.light', fontSize: '0.65rem', fontWeight: 700 }}>
|
||||
{Math.round((serverStats?.cpu || 0) + (phpStats?.cpu || 0))}% CPU
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -76,7 +76,16 @@
|
||||
"added": "Projekt erfolgreich hinzugefügt.",
|
||||
"updated": "Projekt erfolgreich aktualisiert.",
|
||||
"deleted": "Projekt wurde gelöscht.",
|
||||
"delete_confirm": "Sind Sie sicher, dass Sie dieses Projekt löschen möchten? Ihre Dateien werden nicht gelöscht, nur aus der Liste entfernt."
|
||||
"delete_confirm": "Sind Sie sicher, dass Sie dieses Projekt löschen möchten? Ihre Dateien werden nicht gelöscht, nur aus der Liste entfernt.",
|
||||
"overview": "Übersicht",
|
||||
"database": "Datenbank",
|
||||
"site_domain": "Site-Domain",
|
||||
"web_server": "Webserver",
|
||||
"site_path": "Site-Pfad",
|
||||
"open_site": "SITE ÖFFNEN",
|
||||
"stop_site": "SITE STOPPEN",
|
||||
"start_site": "SITE STARTEN",
|
||||
"no_project_selected": "Bitte wählen Sie ein Projekt aus der Liste aus oder erstellen Sie ein neues."
|
||||
},
|
||||
"settings": {
|
||||
"general": "Allgemeine Einstellungen",
|
||||
|
||||
@@ -82,7 +82,16 @@
|
||||
"added": "Project added successfully.",
|
||||
"updated": "Project updated successfully.",
|
||||
"deleted": "Project deleted.",
|
||||
"delete_confirm": "Are you sure you want to delete this project? Your files will not be deleted, only removed from the list."
|
||||
"delete_confirm": "Are you sure you want to delete this project? Your files will not be deleted, only removed from the list.",
|
||||
"overview": "Overview",
|
||||
"database": "Database",
|
||||
"site_domain": "Site Domain",
|
||||
"web_server": "Web Server",
|
||||
"site_path": "Site Path",
|
||||
"open_site": "OPEN SITE",
|
||||
"stop_site": "STOP SITE",
|
||||
"start_site": "START SITE",
|
||||
"no_project_selected": "Please select a project from the list or create a new one."
|
||||
},
|
||||
"settings": {
|
||||
"general": "General Settings",
|
||||
|
||||
@@ -83,11 +83,19 @@
|
||||
"updated": "Proje başarıyla güncellendi.",
|
||||
"deleted": "Proje silindi.",
|
||||
"delete_confirm": "Bu projeyi silmek istediğinize emin misiniz? Dosyalarınız silinmez, sadece listeden kaldırılır.",
|
||||
"browse": "Gözat"
|
||||
"browse": "Gözat",
|
||||
"overview": "Genel Bakış",
|
||||
"database": "Veritabanı",
|
||||
"site_domain": "Site Adresi",
|
||||
"web_server": "Web Sunucusu",
|
||||
"site_path": "Dosya Yolu",
|
||||
"open_site": "SİTEYİ AÇ",
|
||||
"stop_site": "SİTEYİ DURDUR",
|
||||
"start_site": "SİTEYİ BAŞLAT",
|
||||
"no_project_selected": "Lütfen listeden bir proje seçin veya yeni bir tane oluşturun."
|
||||
},
|
||||
"settings": {
|
||||
"general": "Genel Ayarlar",
|
||||
"server_settings": "Sunucu Ayarları",
|
||||
"saved": "Ayarlar kaydedildi.",
|
||||
"save_failed": "Ayarlar kaydedilemedi.",
|
||||
"language": "Dil",
|
||||
|
||||
Reference in New Issue
Block a user