diff --git a/frontend/src/app/providers/layout/sidebar/sidebar.tsx b/frontend/src/app/providers/layout/sidebar/sidebar.tsx index 5f20adf..bc22ebe 100644 --- a/frontend/src/app/providers/layout/sidebar/sidebar.tsx +++ b/frontend/src/app/providers/layout/sidebar/sidebar.tsx @@ -1,5 +1,16 @@ import React, { useMemo, useState } from "react"; -import { FaBars, FaMicrochip, FaTimes, FaSpinner, FaCopy, FaCheck } from "react-icons/fa"; +import { + FaBars, + FaMicrochip, + FaTimes, + FaSpinner, + FaCopy, + FaCheck, + FaChevronRight, + FaChevronDown, + FaProjectDiagram, + FaTrash, +} from "react-icons/fa"; import { useAgentStore } from "@/app/providers/layout/store/agent.store"; import { useAuthStore } from "@/modules/auth/store/useAuthStore"; @@ -8,12 +19,28 @@ interface SidebarProps { onToggle?: () => void; } -export const Sidebar: React.FC = ({ isOpen = true, onToggle }) => { - const { agents, isLoading, error, fetchAgents } = useAgentStore(); +export const Sidebar: React.FC = ({ + isOpen = true, + onToggle, +}) => { + const { agents, isLoading, error, fetchAgents, removeAgent } = + useAgentStore(); const { token } = useAuthStore(); const [searchQuery, setSearchQuery] = useState(""); const [copied, setCopied] = useState(false); const [showTokenModal, setShowTokenModal] = useState(false); + const [expandedAgents, setExpandedAgents] = useState>( + new Set(agents.map((a) => a.name)), + ); + + const toggleAgent = (name: string) => { + setExpandedAgents((prev) => { + const next = new Set(prev); + if (next.has(name)) next.delete(name); + else next.add(name); + return next; + }); + }; const filteredAgents = useMemo(() => { if (!searchQuery) return agents; @@ -21,7 +48,7 @@ export const Sidebar: React.FC = ({ isOpen = true, onToggle }) => return agents.filter( (agent) => agent.name.toLowerCase().includes(query) || - agent.services.some((s) => s.name.toLowerCase().includes(query)) + agent.services.some((s) => s.name.toLowerCase().includes(query)), ); }, [agents, searchQuery]); @@ -38,7 +65,10 @@ export const Sidebar: React.FC = ({ isOpen = true, onToggle }) => ) : filteredAgents.length === 0 ? ( -
+

{searchQuery ? "Ничего не найдено" : "Нет агентов"} @@ -141,58 +189,153 @@ export const Sidebar: React.FC = ({ isOpen = true, onToggle }) =>

) : (
- {filteredAgents.map((agent) => ( -
-
- - {agent.name} - -
-
- {agent.services.map((service) => ( -
{ + const isExpanded = expandedAgents.has(agent.name); + return ( +
+ {/* Agent header — кликабельный для сворачивания */} +
toggleAgent(agent.name)} + > + + {isExpanded ? ( + + ) : ( + + )} + + + - {service.name} + {agent.name} + + {/* Статус-индикатор агента (сколько сервисов запущено) */} +
+ {agent.services.filter((s) => s.status === "running") + .length > 0 && ( + + )} - {service.status} + { + agent.services.filter((s) => s.status === "running") + .length + } + /{agent.services.length}
- ))} + {/* Кнопка удаления — появляется при наведении */} + +
+ + {/* Services list — сворачивается */} + {isExpanded && ( +
+
+ {agent.services.map((service) => { + const isRunning = service.status === "running"; + const isError = service.status === "error"; + const isStopped = + service.status === "stopped" || !isRunning; + + return ( +
+ + {service.name} + + {/* Status indicator */} +
+ + + {isRunning + ? "run" + : isError + ? "err" + : "stop"} + +
+
+ ); + })} +
+
+ )}
-
- ))} + ); + })}
)}
@@ -200,8 +343,29 @@ export const Sidebar: React.FC = ({ isOpen = true, onToggle }) => {/* Footer с кнопками */}
+ {/* Кнопка Графы */} + diff --git a/frontend/src/app/providers/layout/store/agent.store.ts b/frontend/src/app/providers/layout/store/agent.store.ts index 33f4352..fb00e96 100644 --- a/frontend/src/app/providers/layout/store/agent.store.ts +++ b/frontend/src/app/providers/layout/store/agent.store.ts @@ -10,8 +10,37 @@ interface AgentState { removeAgent: (name: string) => void; } +const mockAgents: AgentInfo[] = [ + { + name: "agent-core-01", + token: "tok_a1b2c3d4e5f6g7h8", + services: [ + { name: "postgres", status: "running" }, + { name: "redis", status: "running" }, + { name: "log-collector", status: "running" }, + ], + }, + { + name: "agent-worker-02", + token: "tok_x9y8z7w6v5u4t3s2", + services: [ + { name: "celery-worker", status: "running" }, + { name: "flower", status: "stopped" }, + ], + }, + { + name: "agent-monitor-03", + token: "tok_m1n2o3p4q5r6s7t8", + services: [ + { name: "prometheus", status: "running" }, + { name: "grafana", status: "running" }, + { name: "alertmanager", status: "stopped" }, + ], + }, +]; + export const useAgentStore = create()((set, get) => ({ - agents: [], + agents: mockAgents, isLoading: false, error: null, @@ -22,7 +51,8 @@ export const useAgentStore = create()((set, get) => ({ set({ agents, isLoading: false }); } catch (error) { set({ - error: error instanceof Error ? error.message : "Failed to fetch agents", + error: + error instanceof Error ? error.message : "Failed to fetch agents", isLoading: false, }); } diff --git a/frontend/src/shared/api/websocket.service.ts b/frontend/src/shared/api/websocket.service.ts index 08419b9..6d72ea8 100644 --- a/frontend/src/shared/api/websocket.service.ts +++ b/frontend/src/shared/api/websocket.service.ts @@ -1,5 +1,5 @@ // shared/api/websocket.service.ts -import { useAgentStore } from "@/components/layout/sidebar/store/agent.store"; +import { useAgentStore } from "@/app/providers/layout/store/agent.store"; import { useWebSocket, type LogMessage } from "@/shared/hooks/useWebSocket"; import { useEffect, useRef, useCallback, useMemo } from "react";