redezign: list agents & services; feat: button remove agents
ci-front / build (push) Successful in 2m5s
ci-front / build (push) Successful in 2m5s
This commit is contained in:
@@ -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<SidebarProps> = ({ isOpen = true, onToggle }) => {
|
||||
const { agents, isLoading, error, fetchAgents } = useAgentStore();
|
||||
export const Sidebar: React.FC<SidebarProps> = ({
|
||||
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<Set<string>>(
|
||||
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<SidebarProps> = ({ 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<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="fixed top-4 left-4 z-50 p-2.5 rounded-lg shadow-lg transition-colors md:hidden"
|
||||
style={{ backgroundColor: "var(--accent)", color: "var(--accent-text)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "var(--accent-text)",
|
||||
}}
|
||||
aria-label="Открыть sidebar"
|
||||
>
|
||||
<FaBars size={18} />
|
||||
@@ -49,7 +79,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
return (
|
||||
<>
|
||||
{/* Overlay для мобильных */}
|
||||
<div className="fixed inset-0 bg-black/50 z-40 md:hidden" onClick={onToggle} />
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 md:hidden"
|
||||
onClick={onToggle}
|
||||
/>
|
||||
|
||||
<aside
|
||||
className={`fixed md:relative w-72 h-screen z-50 transition-transform duration-300 ease-in-out flex flex-col ${
|
||||
@@ -67,12 +100,18 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaMicrochip style={{ color: "var(--accent)", fontSize: "18px" }} />
|
||||
<h2 className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>
|
||||
<h2
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
Агенты
|
||||
</h2>
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 rounded"
|
||||
style={{ backgroundColor: "var(--bg-secondary)", color: "var(--text-secondary)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
color: "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
{agents.length}
|
||||
</span>
|
||||
@@ -114,14 +153,20 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
<div className="flex-1 overflow-y-auto px-2 py-2">
|
||||
{isLoading && agents.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<FaSpinner className="animate-spin mb-3" style={{ color: "var(--accent)", fontSize: "20px" }} />
|
||||
<FaSpinner
|
||||
className="animate-spin mb-3"
|
||||
style={{ color: "var(--accent)", fontSize: "20px" }}
|
||||
/>
|
||||
<p className="text-xs" style={{ color: "var(--text-secondary)" }}>
|
||||
Загрузка агентов...
|
||||
</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-xs mb-2" style={{ color: "var(--error-text)" }}>
|
||||
<div
|
||||
className="text-xs mb-2"
|
||||
style={{ color: "var(--error-text)" }}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
<button
|
||||
@@ -133,7 +178,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
</button>
|
||||
</div>
|
||||
) : filteredAgents.length === 0 ? (
|
||||
<div className="text-center py-8" style={{ color: "var(--text-muted)" }}>
|
||||
<div
|
||||
className="text-center py-8"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
<FaMicrochip className="mx-auto mb-2 opacity-50" size={16} />
|
||||
<p className="text-xs">
|
||||
{searchQuery ? "Ничего не найдено" : "Нет агентов"}
|
||||
@@ -141,58 +189,153 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{filteredAgents.map((agent) => (
|
||||
<div
|
||||
key={agent.name}
|
||||
className="rounded-lg border p-3 transition-all hover:shadow-md"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium" style={{ color: "var(--text-primary)" }}>
|
||||
{agent.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{agent.services.map((service) => (
|
||||
<div
|
||||
key={service.name}
|
||||
className="flex items-center justify-between text-xs"
|
||||
{filteredAgents.map((agent) => {
|
||||
const isExpanded = expandedAgents.has(agent.name);
|
||||
return (
|
||||
<div
|
||||
key={agent.name}
|
||||
className="rounded-lg border overflow-hidden transition-all group"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
{/* Agent header — кликабельный для сворачивания */}
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => toggleAgent(agent.name)}
|
||||
>
|
||||
<span style={{ color: "var(--text-muted)" }}>
|
||||
{isExpanded ? (
|
||||
<FaChevronDown size={10} />
|
||||
) : (
|
||||
<FaChevronRight size={10} />
|
||||
)}
|
||||
</span>
|
||||
<FaMicrochip
|
||||
size={12}
|
||||
style={{ color: "var(--accent)" }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm font-medium flex-1 truncate"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
<span style={{ color: "var(--text-secondary)" }}>{service.name}</span>
|
||||
{agent.name}
|
||||
</span>
|
||||
{/* Статус-индикатор агента (сколько сервисов запущено) */}
|
||||
<div className="flex items-center gap-1">
|
||||
{agent.services.filter((s) => s.status === "running")
|
||||
.length > 0 && (
|
||||
<span
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: "#4ade80" }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-medium"
|
||||
style={{
|
||||
backgroundColor:
|
||||
service.status === "running"
|
||||
? "var(--success-bg)"
|
||||
: service.status === "error"
|
||||
? "var(--error-bg)"
|
||||
: "var(--bg-secondary)",
|
||||
color:
|
||||
service.status === "running"
|
||||
? "var(--success-text)"
|
||||
: service.status === "error"
|
||||
? "var(--error-text)"
|
||||
: "var(--text-muted)",
|
||||
border: `1px solid ${
|
||||
service.status === "running"
|
||||
? "var(--success-border)"
|
||||
: service.status === "error"
|
||||
? "var(--error-border)"
|
||||
: "var(--border)"
|
||||
}`,
|
||||
}}
|
||||
className="text-[10px]"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
{service.status}
|
||||
{
|
||||
agent.services.filter((s) => s.status === "running")
|
||||
.length
|
||||
}
|
||||
/{agent.services.length}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{/* Кнопка удаления — появляется при наведении */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (
|
||||
window.confirm(`Удалить агента "${agent.name}"?`)
|
||||
) {
|
||||
removeAgent(agent.name);
|
||||
}
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded transition-all flex-shrink-0"
|
||||
style={{
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = "#f87171";
|
||||
e.currentTarget.style.backgroundColor =
|
||||
"rgba(248, 113, 113, 0.15)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = "var(--text-muted)";
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
title="Удалить агента"
|
||||
>
|
||||
<FaTrash size={10} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Services list — сворачивается */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
className="px-3 pb-2"
|
||||
style={{ paddingLeft: "24px" }}
|
||||
>
|
||||
<div
|
||||
className="border-l-2 pl-3 space-y-1"
|
||||
style={{ borderColor: "var(--border)" }}
|
||||
>
|
||||
{agent.services.map((service) => {
|
||||
const isRunning = service.status === "running";
|
||||
const isError = service.status === "error";
|
||||
const isStopped =
|
||||
service.status === "stopped" || !isRunning;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={service.name}
|
||||
className="flex items-center justify-between py-1"
|
||||
>
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
>
|
||||
{service.name}
|
||||
</span>
|
||||
{/* Status indicator */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: isRunning
|
||||
? "#4ade80"
|
||||
: isError
|
||||
? "#f87171"
|
||||
: "#555",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="text-[10px] font-medium"
|
||||
style={{
|
||||
color: isRunning
|
||||
? "#4ade80"
|
||||
: isError
|
||||
? "#f87171"
|
||||
: "#777",
|
||||
}}
|
||||
>
|
||||
{isRunning
|
||||
? "run"
|
||||
: isError
|
||||
? "err"
|
||||
: "stop"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -200,8 +343,29 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
{/* Footer с кнопками */}
|
||||
<div
|
||||
className="p-2 border-t flex gap-2"
|
||||
style={{ borderColor: "var(--border)", backgroundColor: "var(--card-bg)" }}
|
||||
style={{
|
||||
borderColor: "var(--border)",
|
||||
backgroundColor: "var(--card-bg)",
|
||||
}}
|
||||
>
|
||||
{/* Кнопка Графы */}
|
||||
<button
|
||||
className="flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs rounded transition-colors"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
color: "var(--text-secondary)",
|
||||
border: "1px solid var(--border)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "var(--border)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "var(--bg-secondary)";
|
||||
}}
|
||||
>
|
||||
<FaProjectDiagram size={10} />
|
||||
Графы
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowTokenModal(true)}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors"
|
||||
@@ -225,7 +389,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-md rounded-xl shadow-2xl border"
|
||||
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--border)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--card-bg)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
@@ -234,7 +401,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaCopy style={{ color: "var(--accent)" }} size={14} />
|
||||
<h2 className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>
|
||||
<h2
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
Ваш токен доступа
|
||||
</h2>
|
||||
</div>
|
||||
@@ -249,12 +419,18 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-2" style={{ color: "var(--text-secondary)" }}>
|
||||
<label
|
||||
className="block text-xs font-medium mb-2"
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
>
|
||||
Токен
|
||||
</label>
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-lg p-3 border"
|
||||
style={{ backgroundColor: "var(--bg-secondary)", borderColor: "var(--border)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<code
|
||||
className="flex-1 text-xs font-mono break-all"
|
||||
@@ -269,7 +445,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
>
|
||||
{copied ? (
|
||||
<FaCheck size={12} style={{ color: "var(--success-text)" }} />
|
||||
<FaCheck
|
||||
size={12}
|
||||
style={{ color: "var(--success-text)" }}
|
||||
/>
|
||||
) : (
|
||||
<FaCopy size={12} />
|
||||
)}
|
||||
@@ -281,7 +460,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen = true, onToggle }) =>
|
||||
<button
|
||||
onClick={() => setShowTokenModal(false)}
|
||||
className="w-full py-2 rounded-lg text-xs font-medium transition-colors"
|
||||
style={{ backgroundColor: "var(--accent)", color: "var(--accent-text)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "var(--accent-text)",
|
||||
}}
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
|
||||
@@ -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<AgentState>()((set, get) => ({
|
||||
agents: [],
|
||||
agents: mockAgents,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
@@ -22,7 +51,8 @@ export const useAgentStore = create<AgentState>()((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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user