mirror of
https://github.com/RGJorge/ContainerFlow.git
synced 2026-08-03 07:21:42 +02:00
v0.0.24
This commit is contained in:
@@ -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 }} />
|
||||
|
||||
@@ -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) */}
|
||||
|
||||
@@ -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]`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user