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",
|
"service": "php:5.6.40",
|
||||||
"action": "start",
|
"action": "start",
|
||||||
|
|||||||
+50
-249
@@ -39,10 +39,9 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
|
Grid,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Stack,
|
Stack,
|
||||||
Grid,
|
|
||||||
Avatar,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@@ -72,16 +71,11 @@ import {
|
|||||||
Close as CloseIcon,
|
Close as CloseIcon,
|
||||||
ContentCopy as ContentCopyIcon,
|
ContentCopy as ContentCopyIcon,
|
||||||
OpenInNew as OpenIcon,
|
OpenInNew as OpenIcon,
|
||||||
Edit as EditIcon,
|
|
||||||
Folder as FolderIcon,
|
|
||||||
Language as HostIcon,
|
|
||||||
Launch as LaunchIcon,
|
|
||||||
Handyman as ToolsIcon,
|
|
||||||
Restore as ResetIcon,
|
Restore as ResetIcon,
|
||||||
Search as SearchIcon,
|
|
||||||
Tune as TuningIcon,
|
Tune as TuningIcon,
|
||||||
History as HistoryIcon,
|
History as HistoryIcon,
|
||||||
MenuBook as GuideIcon
|
MenuBook as GuideIcon,
|
||||||
|
Handyman as ToolsIcon
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -95,6 +89,7 @@ import PhpExtensionManager from './components/PhpExtensionManager'
|
|||||||
import ServerTuning from './components/ServerTuning'
|
import ServerTuning from './components/ServerTuning'
|
||||||
import ServiceEventMonitor from './components/ServiceEventMonitor'
|
import ServiceEventMonitor from './components/ServiceEventMonitor'
|
||||||
import UserGuide from './components/UserGuide'
|
import UserGuide from './components/UserGuide'
|
||||||
|
import ProjectManager from './components/ProjectManager/ProjectManager'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -192,6 +187,8 @@ function App(): JSX.Element {
|
|||||||
const [selectedProjectHost, setSelectedProjectHost] = useState('')
|
const [selectedProjectHost, setSelectedProjectHost] = useState('')
|
||||||
const [isPhpExtensionsOpen, setIsPhpExtensionsOpen] = useState(false)
|
const [isPhpExtensionsOpen, setIsPhpExtensionsOpen] = useState(false)
|
||||||
const [selectedPhpVersion, setSelectedPhpVersion] = useState('')
|
const [selectedPhpVersion, setSelectedPhpVersion] = useState('')
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||||
|
const [projectDetailTab, setProjectDetailTab] = useState(0)
|
||||||
|
|
||||||
const parseNginxError = (message: string) => {
|
const parseNginxError = (message: string) => {
|
||||||
const match = message.match(/in (.*):(\d+)/)
|
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 () => {
|
const handleAddProject = async () => {
|
||||||
if (!window.api) return
|
if (!window.api) return
|
||||||
|
let result;
|
||||||
if (newProject.id) {
|
if (newProject.id) {
|
||||||
await window.api.updateProject(newProject)
|
result = await window.api.updateProject(newProject)
|
||||||
} else {
|
} else {
|
||||||
await window.api.addProject(newProject)
|
result = await window.api.addProject(newProject)
|
||||||
|
if (result && result.id) setSelectedProjectId(result.id)
|
||||||
}
|
}
|
||||||
setIsProjectDialogOpen(false)
|
setIsProjectDialogOpen(false)
|
||||||
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost' })
|
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost' })
|
||||||
@@ -694,6 +636,7 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
await window.api.removeProject(id)
|
await window.api.removeProject(id)
|
||||||
|
if (selectedProjectId === id) setSelectedProjectId(null)
|
||||||
setNotification({ open: true, message: t('projects.deleted'), severity: 'success' })
|
setNotification({ open: true, message: t('projects.deleted'), severity: 'success' })
|
||||||
fetchProjects()
|
fetchProjects()
|
||||||
}
|
}
|
||||||
@@ -2008,192 +1951,50 @@ function App(): JSX.Element {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{activeTab === 'projects' && (
|
{activeTab === 'projects' && (
|
||||||
<Box>
|
<ProjectManager
|
||||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 2, alignItems: { xs: 'stretch', sm: 'center' }, mb: 3 }}>
|
projects={projects}
|
||||||
<Typography variant="h5" sx={{ fontWeight: 'medium', flexGrow: 1 }}>
|
status={status}
|
||||||
{t('projects.title')}
|
settings={settings}
|
||||||
</Typography>
|
projectSearch={projectSearch}
|
||||||
<TextField
|
setProjectSearch={setProjectSearch}
|
||||||
size="small"
|
selectedProjectId={selectedProjectId}
|
||||||
placeholder={t('projects.search')}
|
setSelectedProjectId={setSelectedProjectId}
|
||||||
value={projectSearch}
|
projectDetailTab={projectDetailTab}
|
||||||
onChange={(e) => setProjectSearch(e.target.value)}
|
setProjectDetailTab={setProjectDetailTab}
|
||||||
sx={{
|
t={t}
|
||||||
minWidth: { sm: 300 },
|
onAddProject={() => {
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.03)',
|
setNewProject({ name: '', path: '', phpVersion: '', mariaDbVersion: '', host: 'localhost', serverType: 'nginx' })
|
||||||
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())
|
|
||||||
);
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
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={{
|
onEditProject={(p) => {
|
||||||
color: '#39A7FF',
|
setNewProject(p)
|
||||||
textTransform: 'none',
|
setIsProjectDialogOpen(true)
|
||||||
fontSize: '0.75rem',
|
|
||||||
p: '2px 8px',
|
|
||||||
'&:hover': { bgcolor: 'rgba(57, 167, 255, 0.1)' }
|
|
||||||
}}
|
}}
|
||||||
>
|
onRemoveProject={handleRemoveProject}
|
||||||
{t('projects.browse')}
|
onToggleStatus={async (p: any) => {
|
||||||
</Button>
|
const isRunning = (
|
||||||
</Paper>
|
(p.serverType === 'apache' ? status.apache?.status : status.nginx?.status) === 'running' &&
|
||||||
</Stack>
|
status[`php:${p.phpVersion}`]?.status === 'running'
|
||||||
</Paper>
|
);
|
||||||
</Grid>
|
if (isRunning) {
|
||||||
))}
|
await window.api.stopService(p.serverType === 'apache' ? 'apache' : 'nginx');
|
||||||
</Grid>
|
await window.api.stopService(`php:${p.phpVersion}`);
|
||||||
)
|
} else {
|
||||||
})()}
|
await window.api.startService(p.serverType === 'apache' ? 'apache' : 'nginx');
|
||||||
</Box>
|
await window.api.startService(`php:${p.phpVersion}`);
|
||||||
|
}
|
||||||
|
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>
|
</Container>
|
||||||
</Box >
|
</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.",
|
"added": "Projekt erfolgreich hinzugefügt.",
|
||||||
"updated": "Projekt erfolgreich aktualisiert.",
|
"updated": "Projekt erfolgreich aktualisiert.",
|
||||||
"deleted": "Projekt wurde gelöscht.",
|
"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": {
|
"settings": {
|
||||||
"general": "Allgemeine Einstellungen",
|
"general": "Allgemeine Einstellungen",
|
||||||
|
|||||||
@@ -82,7 +82,16 @@
|
|||||||
"added": "Project added successfully.",
|
"added": "Project added successfully.",
|
||||||
"updated": "Project updated successfully.",
|
"updated": "Project updated successfully.",
|
||||||
"deleted": "Project deleted.",
|
"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": {
|
"settings": {
|
||||||
"general": "General Settings",
|
"general": "General Settings",
|
||||||
|
|||||||
@@ -83,11 +83,19 @@
|
|||||||
"updated": "Proje başarıyla güncellendi.",
|
"updated": "Proje başarıyla güncellendi.",
|
||||||
"deleted": "Proje silindi.",
|
"deleted": "Proje silindi.",
|
||||||
"delete_confirm": "Bu projeyi silmek istediğinize emin misiniz? Dosyalarınız silinmez, sadece listeden kaldırılır.",
|
"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": {
|
"settings": {
|
||||||
"general": "Genel Ayarlar",
|
"general": "Genel Ayarlar",
|
||||||
"server_settings": "Sunucu Ayarları",
|
|
||||||
"saved": "Ayarlar kaydedildi.",
|
"saved": "Ayarlar kaydedildi.",
|
||||||
"save_failed": "Ayarlar kaydedilemedi.",
|
"save_failed": "Ayarlar kaydedilemedi.",
|
||||||
"language": "Dil",
|
"language": "Dil",
|
||||||
|
|||||||
Reference in New Issue
Block a user