From 36335ee5abc73be23139b7bc1d452a8007afcf89 Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Wed, 25 Feb 2026 23:30:24 -0500 Subject: [PATCH] feat: improved EditorLayout component for managing Docker Compose stacks, editing files, and monitoring containers. --- backend/src/services/DockerController.ts | 43 ++++++++--- frontend/src/components/EditorLayout.tsx | 98 ++++++++++++++++-------- 2 files changed, 98 insertions(+), 43 deletions(-) diff --git a/backend/src/services/DockerController.ts b/backend/src/services/DockerController.ts index 28b6fdd6..74efb711 100644 --- a/backend/src/services/DockerController.ts +++ b/backend/src/services/DockerController.ts @@ -53,6 +53,7 @@ class DockerController { Name?: string; State?: string; Status?: string; + Publishers?: { URL?: string, TargetPort?: number, PublishedPort?: number }[]; } let containers: ComposeContainer[] = []; @@ -81,12 +82,21 @@ class DockerController { // Map to frontend's expected interface // Note: docker compose ps returns Name (singular), but frontend expects Names (array) // Dockerode returns Names with leading slash, so we add it for compatibility - return containers.map((c) => ({ - Id: c.ID || '', - Names: ['/' + (c.Name || '')], // Add leading slash to match Dockerode format - State: c.State || 'unknown', - Status: c.Status || '' - })); + return containers.map((c) => { + let Ports: { PrivatePort: number, PublicPort: number }[] = []; + if (c.Publishers && Array.isArray(c.Publishers)) { + Ports = c.Publishers + .filter(p => typeof p.PublishedPort === 'number' && p.PublishedPort > 0) + .map(p => ({ PrivatePort: (p.TargetPort || 0) as number, PublicPort: p.PublishedPort as number })); + } + return { + Id: c.ID || '', + Names: ['/' + (c.Name || '')], // Add leading slash to match Dockerode format + State: c.State || 'unknown', + Status: c.Status || '', + Ports + }; + }); } // SMART FALLBACK: Trigger when docker compose ps returns empty @@ -163,12 +173,21 @@ class DockerController { }); // 5. Map to the frontend interface - return fallbackContainers.map(c => ({ - Id: c.Id, - Names: c.Names, - State: c.State, - Status: c.Status - })); + return fallbackContainers.map(c => { + let Ports: { PrivatePort: number, PublicPort: number }[] = []; + if (c.Ports && Array.isArray(c.Ports)) { + Ports = c.Ports + .filter((p: any) => typeof p.PublicPort === 'number' && p.PublicPort > 0) + .map((p: any) => ({ PrivatePort: (p.PrivatePort || 0) as number, PublicPort: p.PublicPort as number })); + } + return { + Id: c.Id, + Names: c.Names, + State: c.State, + Status: c.Status, + Ports + }; + }); } catch (fallbackError) { console.error(`Smart Fallback failed for ${stackName}:`, fallbackError); return []; diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 330d3af0..2289c551 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -12,7 +12,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, import { Tabs, TabsList, TabsTrigger } from './ui/tabs'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { Badge } from './ui/badge'; -import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Home, LogOut, Brush } from 'lucide-react'; +import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Home, LogOut, Brush, ExternalLink } from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { apiFetch } from '@/lib/api'; import { toast } from 'sonner'; @@ -26,6 +26,7 @@ interface ContainerInfo { Id: string; Names: string[]; State: string; + Ports?: { PrivatePort: number, PublicPort: number }[]; } interface StackStatus { @@ -675,38 +676,73 @@ export default function EditorLayout() {
No containers running for this stack.
) : (
- {safeContainers.map(container => ( -
-
-
- - {container?.State || 'unknown'} - - - CPU: {containerStats[container?.Id]?.cpu || 'N/A'} | RAM: {containerStats[container?.Id]?.ram || 'N/A'} - + {safeContainers.map(container => { + let mainPort: number | undefined; + if (container.Ports && container.Ports.length > 0) { + const WEB_UI_PORTS = [32400, 8989, 7878, 9696, 5055, 8080, 80, 443, 3000, 9000]; + const IGNORE_PORTS = [1900, 53, 22]; + + // 1. Match typical Web UI Private ports + let match = container.Ports.find(p => WEB_UI_PORTS.includes(p.PrivatePort)); + // 2. Match typical Web UI Public ports + if (!match) match = container.Ports.find(p => WEB_UI_PORTS.includes(p.PublicPort)); + // 3. Fallback to any port not in ignore list + if (!match) match = container.Ports.find(p => !IGNORE_PORTS.includes(p.PrivatePort) && !IGNORE_PORTS.includes(p.PublicPort)); + + mainPort = (match || container.Ports[0]).PublicPort; + } + + return ( +
+
+
+ + {container?.State || 'unknown'} + + + CPU: {containerStats[container?.Id]?.cpu || 'N/A'} | RAM: {containerStats[container?.Id]?.ram || 'N/A'} + +
+
+
+ {mainPort && ( + + + + + + Open App ({mainPort}) + + + )} + + + + + + Open Bash Terminal + +
-
- - - - - - Open Bash Terminal - - -
-
- ))} + ); + })}
)}