mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-03 06:07:58 +00:00
feat: improved EditorLayout component for managing Docker Compose stacks, editing files, and monitoring containers.
This commit is contained in:
@@ -53,6 +53,7 @@ class DockerController {
|
|||||||
Name?: string;
|
Name?: string;
|
||||||
State?: string;
|
State?: string;
|
||||||
Status?: string;
|
Status?: string;
|
||||||
|
Publishers?: { URL?: string, TargetPort?: number, PublishedPort?: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let containers: ComposeContainer[] = [];
|
let containers: ComposeContainer[] = [];
|
||||||
@@ -81,12 +82,21 @@ class DockerController {
|
|||||||
// Map to frontend's expected interface
|
// Map to frontend's expected interface
|
||||||
// Note: docker compose ps returns Name (singular), but frontend expects Names (array)
|
// Note: docker compose ps returns Name (singular), but frontend expects Names (array)
|
||||||
// Dockerode returns Names with leading slash, so we add it for compatibility
|
// Dockerode returns Names with leading slash, so we add it for compatibility
|
||||||
return containers.map((c) => ({
|
return containers.map((c) => {
|
||||||
Id: c.ID || '',
|
let Ports: { PrivatePort: number, PublicPort: number }[] = [];
|
||||||
Names: ['/' + (c.Name || '')], // Add leading slash to match Dockerode format
|
if (c.Publishers && Array.isArray(c.Publishers)) {
|
||||||
State: c.State || 'unknown',
|
Ports = c.Publishers
|
||||||
Status: c.Status || ''
|
.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
|
// SMART FALLBACK: Trigger when docker compose ps returns empty
|
||||||
@@ -163,12 +173,21 @@ class DockerController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 5. Map to the frontend interface
|
// 5. Map to the frontend interface
|
||||||
return fallbackContainers.map(c => ({
|
return fallbackContainers.map(c => {
|
||||||
Id: c.Id,
|
let Ports: { PrivatePort: number, PublicPort: number }[] = [];
|
||||||
Names: c.Names,
|
if (c.Ports && Array.isArray(c.Ports)) {
|
||||||
State: c.State,
|
Ports = c.Ports
|
||||||
Status: c.Status
|
.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) {
|
} catch (fallbackError) {
|
||||||
console.error(`Smart Fallback failed for ${stackName}:`, fallbackError);
|
console.error(`Smart Fallback failed for ${stackName}:`, fallbackError);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
|||||||
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
import { Badge } from './ui/badge';
|
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 { useAuth } from '@/context/AuthContext';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -26,6 +26,7 @@ interface ContainerInfo {
|
|||||||
Id: string;
|
Id: string;
|
||||||
Names: string[];
|
Names: string[];
|
||||||
State: string;
|
State: string;
|
||||||
|
Ports?: { PrivatePort: number, PublicPort: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StackStatus {
|
interface StackStatus {
|
||||||
@@ -675,38 +676,73 @@ export default function EditorLayout() {
|
|||||||
<div className="text-muted-foreground text-sm">No containers running for this stack.</div>
|
<div className="text-muted-foreground text-sm">No containers running for this stack.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{safeContainers.map(container => (
|
{safeContainers.map(container => {
|
||||||
<div key={container?.Id || Math.random()} className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
let mainPort: number | undefined;
|
||||||
<div className="flex flex-col gap-1">
|
if (container.Ports && container.Ports.length > 0) {
|
||||||
<div className="flex items-center gap-2">
|
const WEB_UI_PORTS = [32400, 8989, 7878, 9696, 5055, 8080, 80, 443, 3000, 9000];
|
||||||
<Badge variant={container?.State === 'running' ? 'default' : 'destructive'} className="text-xs">
|
const IGNORE_PORTS = [1900, 53, 22];
|
||||||
{container?.State || 'unknown'}
|
|
||||||
</Badge>
|
// 1. Match typical Web UI Private ports
|
||||||
<span className="text-xs text-muted-foreground">
|
let match = container.Ports.find(p => WEB_UI_PORTS.includes(p.PrivatePort));
|
||||||
CPU: {containerStats[container?.Id]?.cpu || 'N/A'} | RAM: {containerStats[container?.Id]?.ram || 'N/A'}
|
// 2. Match typical Web UI Public ports
|
||||||
</span>
|
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 (
|
||||||
|
<div key={container?.Id || Math.random()} className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={container?.State === 'running' ? 'default' : 'destructive'} className="text-xs">
|
||||||
|
{container?.State || 'unknown'}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
CPU: {containerStats[container?.Id]?.cpu || 'N/A'} | RAM: {containerStats[container?.Id]?.ram || 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{mainPort && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-lg h-8 px-2 mr-1"
|
||||||
|
onClick={() => window.open(`http://${window.location.hostname}:${mainPort}`, '_blank')}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3 h-3 mr-1" />
|
||||||
|
{mainPort}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Open App ({mainPort})</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="rounded-lg h-8 w-8"
|
||||||
|
onClick={() => openBashModal(container?.Id, container?.Names?.[0]?.replace('/', '') || 'container')}
|
||||||
|
disabled={container?.State !== 'running'}
|
||||||
|
>
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Open Bash Terminal</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
);
|
||||||
<TooltipProvider>
|
})}
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="rounded-lg h-8 w-8"
|
|
||||||
onClick={() => openBashModal(container?.Id, container?.Names?.[0]?.replace('/', '') || 'container')}
|
|
||||||
disabled={container?.State !== 'running'}
|
|
||||||
>
|
|
||||||
<Terminal className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Open Bash Terminal</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user