diff --git a/frontend/src/app/providers/layout/layout.tsx b/frontend/src/app/providers/layout/layout.tsx new file mode 100644 index 0000000..4210ac4 --- /dev/null +++ b/frontend/src/app/providers/layout/layout.tsx @@ -0,0 +1,31 @@ +import { useState, useEffect, type ReactNode } from "react"; +import { Sidebar } from "@/app/providers/layout/sidebar/sidebar"; +import { Navigation } from "@/app/providers/layout/navigation/navigation"; +import { useAgentStore } from "@/app/providers/layout/store/agent.store"; + +export const Layout = ({ children }: { children: ReactNode }) => { + const [isOpen, setOpen] = useState(true); + const { fetchAgents } = useAgentStore(); + + useEffect(() => { + fetchAgents(); + }, [fetchAgents]); + + useEffect(() => { + const interval = setInterval(() => { + fetchAgents(); + }, 30000); + + return () => clearInterval(interval); + }, [fetchAgents]); + + return ( +
+ setOpen(!isOpen)} /> +
+ +
{children}
+
+
+ ); +}; diff --git a/frontend/src/app/providers/layout/navigation/navigation.tsx b/frontend/src/app/providers/layout/navigation/navigation.tsx new file mode 100644 index 0000000..3c7a3be --- /dev/null +++ b/frontend/src/app/providers/layout/navigation/navigation.tsx @@ -0,0 +1,116 @@ +import { useNavigate, useLocation } from "react-router-dom"; +import { FaHome, FaServer, FaPalette, FaUser } from "react-icons/fa"; +import { useAuthStore } from "@/modules/auth/store/useAuthStore"; + +export const Navigation = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { user, logout } = useAuthStore(); + + const navItems = [ + { path: "/", label: "Главная", icon: FaHome }, + { path: "/add-agents", label: "Агенты", icon: FaServer }, + { path: "/themes", label: "Темы", icon: FaPalette }, + ]; + + const isActive = (path: string) => location.pathname === path; + + return ( +
+
+ {/* Логотип */} +
navigate("/")} + > + + + HellreigN + +
+ + {/* Навигация */} +
+ {navItems.map((item) => { + const Icon = item.icon; + const active = isActive(item.path); + return ( + + ); + })} +
+ + {/* Профиль пользователя */} +
+ {user && ( +
+
+ +
+ + {user.name} + +
+ )} + +
+
+
+ ); +}; diff --git a/frontend/src/app/providers/layout/sidebar/sidebar.tsx b/frontend/src/app/providers/layout/sidebar/sidebar.tsx new file mode 100644 index 0000000..5f20adf --- /dev/null +++ b/frontend/src/app/providers/layout/sidebar/sidebar.tsx @@ -0,0 +1,294 @@ +import React, { useMemo, useState } from "react"; +import { FaBars, FaMicrochip, FaTimes, FaSpinner, FaCopy, FaCheck } from "react-icons/fa"; +import { useAgentStore } from "@/app/providers/layout/store/agent.store"; +import { useAuthStore } from "@/modules/auth/store/useAuthStore"; + +interface SidebarProps { + isOpen?: boolean; + onToggle?: () => void; +} + +export const Sidebar: React.FC = ({ isOpen = true, onToggle }) => { + const { agents, isLoading, error, fetchAgents } = useAgentStore(); + const { token } = useAuthStore(); + const [searchQuery, setSearchQuery] = useState(""); + const [copied, setCopied] = useState(false); + const [showTokenModal, setShowTokenModal] = useState(false); + + const filteredAgents = useMemo(() => { + if (!searchQuery) return agents; + const query = searchQuery.toLowerCase(); + return agents.filter( + (agent) => + agent.name.toLowerCase().includes(query) || + agent.services.some((s) => s.name.toLowerCase().includes(query)) + ); + }, [agents, searchQuery]); + + const handleCopyToken = () => { + if (token) { + navigator.clipboard.writeText(token); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + if (!isOpen) { + return ( + + ); + } + + return ( + <> + {/* Overlay для мобильных */} +
+ + + + {/* Modal токена */} + {showTokenModal && ( +
setShowTokenModal(false)} + > +
e.stopPropagation()} + > +
+
+ +

+ Ваш токен доступа +

+
+ +
+ +
+
+ +
+ + {token || "Токен не найден"} + + {token && ( + + )} +
+
+ + +
+
+
+ )} + + ); +}; diff --git a/frontend/src/app/providers/layout/store/agent.store.ts b/frontend/src/app/providers/layout/store/agent.store.ts new file mode 100644 index 0000000..33f4352 --- /dev/null +++ b/frontend/src/app/providers/layout/store/agent.store.ts @@ -0,0 +1,34 @@ +import { create } from "zustand"; +import { agentApiService } from "@/modules/agent/api/agent.api.service"; +import type { AgentInfo } from "@/modules/agent/types/agent.types"; + +interface AgentState { + agents: AgentInfo[]; + isLoading: boolean; + error: string | null; + fetchAgents: () => Promise; + removeAgent: (name: string) => void; +} + +export const useAgentStore = create()((set, get) => ({ + agents: [], + isLoading: false, + error: null, + + fetchAgents: async () => { + set({ isLoading: true, error: null }); + try { + const agents = await agentApiService.getAgents(); + set({ agents, isLoading: false }); + } catch (error) { + set({ + error: error instanceof Error ? error.message : "Failed to fetch agents", + isLoading: false, + }); + } + }, + + removeAgent: (name: string) => { + set({ agents: get().agents.filter((a) => a.name !== name) }); + }, +})); diff --git a/frontend/src/app/providers/routing/routing.tsx b/frontend/src/app/providers/routing/routing.tsx index af4b471..7d8d4f8 100644 --- a/frontend/src/app/providers/routing/routing.tsx +++ b/frontend/src/app/providers/routing/routing.tsx @@ -17,15 +17,18 @@ export const Routing = () => { } > + {/* Публичные маршруты */} + } /> + } /> + + {/* Защищённые маршруты с Layout */} }> } /> - } /> - } /> } /> } /> - - } /> + + } /> ); diff --git a/frontend/src/modules/agent/types/agent.types.ts b/frontend/src/modules/agent/types/agent.types.ts index ce0aa51..7d0e342 100644 --- a/frontend/src/modules/agent/types/agent.types.ts +++ b/frontend/src/modules/agent/types/agent.types.ts @@ -1,6 +1,11 @@ +export interface AgentService { + name: string; + status: string; +} + export interface AgentInfo { - label: string; - services: string[]; + name: string; + services: AgentService[]; token: string; } diff --git a/frontend/src/shared/layouts/DefaultLayout.tsx b/frontend/src/shared/layouts/DefaultLayout.tsx index 3a3e439..9d21dbe 100644 --- a/frontend/src/shared/layouts/DefaultLayout.tsx +++ b/frontend/src/shared/layouts/DefaultLayout.tsx @@ -1,90 +1,17 @@ import { useAuthStore } from "@/modules/auth/store/useAuthStore"; -import { ThemeToggle } from "@/modules/theme-bw/ui/ThemeToggle"; -import React from "react"; -import { Outlet, useNavigate } from "react-router-dom"; +import { Navigate, Outlet } from "react-router-dom"; +import { Layout } from "@/app/providers/layout/layout"; -interface DefaultLayoutProps { - children?: React.ReactNode; -} +export const DefaultLayout = () => { + const { token } = useAuthStore(); -export const DefaultLayout: React.FC = ({ children }) => { - const { user, logout } = useAuthStore(); - const navigate = useNavigate(); - - const handleLogout = () => { - logout(); - navigate("/auth"); - }; + if (!token) { + return ; + } return ( -
- {/* Header */} -
-
-
- {/* Logo */} -
navigate("/")} - > - HellreigN -
- - {/* Right side */} -
- - {user && ( -
- - {user.firstName} {user.lastName} - - -
- )} -
-
-
-
- - {/* Main content */} -
{children || }
- - {/* Footer */} -
-
-

- © 2026 HellreigN. Все права защищены. -

-
-
-
+ + + ); };