This commit is contained in:
RGJorge
2026-05-08 02:19:45 +00:00
parent 9e1dadafe7
commit 1ee37368bd
23 changed files with 1321 additions and 147 deletions
+4 -1
View File
@@ -1,4 +1,5 @@
import { Database, Zap, Radio, Globe } from "lucide-react";
import { useT } from "../i18n";
const LEGEND_ITEMS = [
{ icon: Database, color: "#336791", label: "Database" },
@@ -8,9 +9,11 @@ const LEGEND_ITEMS = [
] as const;
export function EdgeLegend() {
const { t } = useT();
return (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-5 bg-slate-900/90 border border-slate-800 rounded-lg px-5 py-2.5 z-10">
<span className="text-xs text-slate-500 uppercase tracking-wider font-semibold">Conexiones</span>
<span className="text-xs text-slate-500 uppercase tracking-wider font-semibold">{t("legend.connections")}</span>
{LEGEND_ITEMS.map(({ icon: Icon, color, label }) => (
<div key={label} className="flex items-center gap-2">
<div className="w-5 h-0.5 rounded-full" style={{ backgroundColor: color }} />
+29 -5
View File
@@ -5,6 +5,7 @@ import {
Play, Square, RotateCcw,
} from "lucide-react";
import type { Service, DockerEvent } from "../../shared/types";
import { useT } from "../i18n";
export type Page = "dashboard" | "monitoring" | "settings";
@@ -53,6 +54,7 @@ interface NotificationBellProps {
}
function NotificationBell({ events }: NotificationBellProps) {
const { t } = useT();
const [open, setOpen] = useState(false);
const [lastSeen, setLastSeen] = useState(events.length);
const ref = useRef<HTMLDivElement>(null);
@@ -93,10 +95,10 @@ function NotificationBell({ events }: NotificationBellProps) {
{open && (
<div className="absolute top-full right-0 mt-1.5 bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 w-72 max-h-80 overflow-auto z-[9999]">
<div className="px-3 py-2 border-b border-slate-700/60 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Recent Events
{t("header.recentEvents")}
</div>
{recent.length === 0 ? (
<div className="px-3 py-6 text-center text-sm text-slate-500">No events yet</div>
<div className="px-3 py-6 text-center text-sm text-slate-500">{t("header.noEvents")}</div>
) : (
recent.map((ev, i) => (
<div key={`${ev.service}-${ev.time}-${i}`} className="flex items-center gap-2.5 px-3 py-2 hover:bg-slate-700/40 transition-colors">
@@ -133,14 +135,15 @@ export function HeaderBar({
onPageChange,
events,
}: HeaderBarProps) {
const { t, lang, setLang } = useT();
return (
<div className="flex items-center justify-between px-5 py-1 bg-slate-900/90 backdrop-blur-sm relative z-[9999]">
{/* Left: Navigation */}
<nav className="flex items-center gap-1">
<NavButton icon={LayoutDashboard} label="Dashboard" active={activePage === "dashboard"} onClick={() => onPageChange("dashboard")} />
<NavButton icon={Activity} label="Monitoring" active={activePage === "monitoring"} onClick={() => onPageChange("monitoring")} />
<NavButton icon={Settings} label="Settings" active={activePage === "settings"} onClick={() => onPageChange("settings")} />
<NavButton icon={LayoutDashboard} label={t("header.dashboard")} active={activePage === "dashboard"} onClick={() => onPageChange("dashboard")} />
<NavButton icon={Activity} label={t("header.monitoring")} active={activePage === "monitoring"} onClick={() => onPageChange("monitoring")} />
<NavButton icon={Settings} label={t("header.settings")} active={activePage === "settings"} onClick={() => onPageChange("settings")} />
</nav>
{/* Center: Logo */}
@@ -175,6 +178,27 @@ export function HeaderBar({
</>
)}
{/* Language toggle */}
<div className="flex items-center bg-slate-800 rounded-md border border-slate-700/50 overflow-hidden">
<button
onClick={() => setLang("en")}
className={`px-2 py-1 text-[11px] font-semibold transition-colors ${
lang === "en" ? "bg-cyan-500/20 text-cyan-400" : "text-slate-500 hover:text-slate-300"
}`}
>
EN
</button>
<div className="w-px h-4 bg-slate-700/50" />
<button
onClick={() => setLang("es")}
className={`px-2 py-1 text-[11px] font-semibold transition-colors ${
lang === "es" ? "bg-cyan-500/20 text-cyan-400" : "text-slate-500 hover:text-slate-300"
}`}
>
ES
</button>
</div>
<NotificationBell events={events} />
{/* Logout (only if auth is active) */}
+13 -11
View File
@@ -1,11 +1,13 @@
import { useState } from "react";
import { Lock, Eye, EyeOff, Terminal } from "lucide-react";
import { useT } from "../i18n";
interface LoginScreenProps {
onAuth: (token: string) => void;
}
export function LoginScreen({ onAuth }: LoginScreenProps) {
const { t } = useT();
const [token, setToken] = useState("");
const [error, setError] = useState("");
const [showToken, setShowToken] = useState(false);
@@ -31,8 +33,8 @@ export function LoginScreen({ onAuth }: LoginScreenProps) {
hackerLog([
"$ containerflow connect --auth",
"> Establishing secure connection...",
"> Validating AUTH_TOKEN...",
`> ${t("login.establishingConnection")}`,
`> ${t("login.validatingToken")}`,
], async () => {
try {
const res = await fetch("/api/health", {
@@ -40,23 +42,23 @@ export function LoginScreen({ onAuth }: LoginScreenProps) {
});
if (res.ok) {
hackerLog([
"> Token accepted",
"> Loading Docker socket...",
"> Connection established!",
`> ${t("login.tokenAccepted")}`,
`> ${t("login.loadingDocker")}`,
`> ${t("login.connectionEstablished")}`,
], () => {
localStorage.setItem("df:token", token);
setConnected(true);
setTimeout(() => onAuth(token), 800);
});
} else {
hackerLog(["> ERROR: Invalid token", "> Connection refused"], () => {
setError("Token invalido");
hackerLog([`> ${t("login.errorInvalidToken")}`, `> ${t("login.errorConnectionRefused")}`], () => {
setError(t("login.invalidToken"));
setConnecting(false);
});
}
} catch {
hackerLog(["> ERROR: Connection failed"], () => {
setError("No se pudo conectar");
hackerLog([`> ${t("login.errorConnectionFailed")}`], () => {
setError(t("login.connectionFailed"));
setConnecting(false);
});
}
@@ -110,7 +112,7 @@ export function LoginScreen({ onAuth }: LoginScreenProps) {
}`}
>
<Terminal size={14} />
{connecting ? "Connecting..." : "Connect"}
{connecting ? t("login.connecting") : t("login.connect")}
</button>
</form>
@@ -122,7 +124,7 @@ export function LoginScreen({ onAuth }: LoginScreenProps) {
key={i}
className={`${
line.includes("ERROR") ? "text-red-400" :
line.includes("accepted") || line.includes("established") ? "text-emerald-400" :
line.includes("accepted") || line.includes("established") || line.includes("aceptado") || line.includes("establecida") ? "text-emerald-400" :
line.startsWith("$") ? "text-cyan-400" : "text-slate-400"
} animate-[fadeIn_0.15s_ease-out]`}
>
+9 -7
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef } from "react";
import { RotateCw, Square, Play, Trash2, Terminal, ExternalLink, Hammer } from "lucide-react";
import type { Service } from "../../shared/types";
import { useT } from "../i18n";
interface NodeContextMenuProps {
position: { x: number; y: number };
@@ -11,6 +12,7 @@ interface NodeContextMenuProps {
}
export function NodeContextMenu({ position, service, onAction, onOpenLogs, onClose }: NodeContextMenuProps) {
const { t } = useT();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -48,23 +50,23 @@ export function NodeContextMenu({ position, service, onAction, onOpenLogs, onClo
>
{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={RotateCw} label={t("actions.restart")} color="text-yellow-400" onClick={() => { onAction("restart"); onClose(); }} />
<MenuItem icon={Square} label={t("actions.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(); }} />
<MenuItem icon={Play} label={t("actions.start")} color="text-emerald-400" onClick={() => { onAction("start"); onClose(); }} />
<MenuItem icon={Trash2} label={t("actions.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(); }} />
<MenuItem icon={Hammer} label={t("actions.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(); }} />
<MenuItem icon={Terminal} label={t("actions.openLogs")} color="text-cyan-400" onClick={() => { onOpenLogs(); onClose(); }} />
{isRunning && firstPort && (
<a
href={`http://${window.location.hostname}:${firstPort.host}`}
@@ -74,7 +76,7 @@ export function NodeContextMenu({ position, service, onAction, onOpenLogs, onClo
onClick={onClose}
>
<ExternalLink size={14} className="text-slate-400" />
<span>Open :{firstPort.host}</span>
<span>{t("actions.open")} :{firstPort.host}</span>
</a>
)}
</div>