This commit is contained in:
RGJorge
2026-05-02 23:00:53 +00:00
parent 5225c1aeda
commit 019a48c89e
4 changed files with 208 additions and 8 deletions
+94
View File
@@ -0,0 +1,94 @@
import { useEffect, useRef } from "react";
import { RotateCw, Square, Play, Trash2, Terminal, ExternalLink, Hammer } from "lucide-react";
import type { Service } from "../../shared/types";
interface NodeContextMenuProps {
position: { x: number; y: number };
service: Service;
onAction: (action: "start" | "stop" | "restart" | "remove" | "rebuild") => void;
onOpenLogs: () => void;
onClose: () => void;
}
export function NodeContextMenu({ position, service, onAction, onOpenLogs, onClose }: NodeContextMenuProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as HTMLElement)) {
onClose();
}
};
const scrollHandler = () => onClose();
const keyHandler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
document.addEventListener("mousedown", handler);
document.addEventListener("scroll", scrollHandler, true);
document.addEventListener("keydown", keyHandler);
return () => {
document.removeEventListener("mousedown", handler);
document.removeEventListener("scroll", scrollHandler, true);
document.removeEventListener("keydown", keyHandler);
};
}, [onClose]);
// Adjust position so menu doesn't overflow viewport
const menuWidth = 180;
const menuHeight = 200;
const x = position.x + menuWidth > window.innerWidth ? position.x - menuWidth : position.x;
const y = position.y + menuHeight > window.innerHeight - 50 ? position.y - menuHeight : position.y;
const isRunning = service.state === "running";
const firstPort = service.ports.length > 0 ? service.ports[0] : null;
return (
<div
ref={ref}
className="fixed z-[10000] bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/50 py-1.5 min-w-[180px]"
style={{ left: x, top: y }}
>
{isRunning ? (
<>
<MenuItem icon={RotateCw} label="Restart" color="text-yellow-400" onClick={() => { onAction("restart"); onClose(); }} />
<MenuItem icon={Square} label="Stop" color="text-red-400" onClick={() => { onAction("stop"); onClose(); }} />
</>
) : (
<>
<MenuItem icon={Play} label="Start" color="text-emerald-400" onClick={() => { onAction("start"); onClose(); }} />
<MenuItem icon={Trash2} label="Remove" color="text-red-400" onClick={() => { onAction("remove"); onClose(); }} />
</>
)}
{service.compose_file && (
<>
<div className="border-t border-slate-700/50 my-1" />
<MenuItem icon={Hammer} label="Rebuild" color="text-cyan-400" onClick={() => { onAction("rebuild"); onClose(); }} />
</>
)}
<div className="border-t border-slate-700/50 my-1" />
<MenuItem icon={Terminal} label="Open Logs" color="text-cyan-400" onClick={() => { onOpenLogs(); onClose(); }} />
{isRunning && firstPort && (
<a
href={`http://${window.location.hostname}:${firstPort.host}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2.5 w-full px-3.5 py-2 text-sm text-slate-300 hover:bg-slate-700/60 transition-colors cursor-pointer"
onClick={onClose}
>
<ExternalLink size={14} className="text-slate-400" />
<span>Open :{firstPort.host}</span>
</a>
)}
</div>
);
}
function MenuItem({ icon: Icon, label, color, onClick }: { icon: typeof Play; label: string; color: string; onClick: () => void }) {
return (
<button
onClick={onClick}
className="flex items-center gap-2.5 w-full px-3.5 py-2 text-sm text-slate-300 hover:bg-slate-700/60 transition-colors"
>
<Icon size={14} className={color} />
<span>{label}</span>
</button>
);
}