Files
HellreigN/frontend/src/app/providers/layout/sidebar/sidebar.tsx
T
nikitaa_ts 57b43da2e3
ci-front / build (push) Successful in 2m9s
feat: add layout
2026-04-04 03:07:45 +03:00

295 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SidebarProps> = ({ 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 (
<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)" }}
aria-label="Открыть sidebar"
>
<FaBars size={18} />
</button>
);
}
return (
<>
{/* Overlay для мобильных */}
<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 ${
isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
}`}
style={{
backgroundColor: "var(--card-bg)",
borderRight: "1px solid var(--border)",
}}
>
{/* Header */}
<div
className="flex items-center justify-between px-4 py-3 border-b"
style={{ borderColor: "var(--border)" }}
>
<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>
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{ backgroundColor: "var(--bg-secondary)", color: "var(--text-secondary)" }}
>
{agents.length}
</span>
</div>
<button
onClick={onToggle}
className="p-1 rounded transition-colors md:hidden"
style={{ color: "var(--text-secondary)" }}
>
<FaTimes size={14} />
</button>
</div>
{/* Поиск */}
<div className="px-3 py-2">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Поиск агентов..."
className="w-full px-3 py-2 rounded-lg border text-sm focus:outline-none transition-all"
style={{
backgroundColor: "var(--input-bg)",
borderColor: "var(--border)",
color: "var(--text-primary)",
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = "var(--border-focus)";
e.currentTarget.style.boxShadow = `0 0 0 3px var(--border-focus)30`;
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = "var(--border)";
e.currentTarget.style.boxShadow = "none";
}}
/>
</div>
{/* Список агентов */}
<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" }} />
<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)" }}>
{error}
</div>
<button
onClick={fetchAgents}
className="text-xs hover:underline"
style={{ color: "var(--accent)" }}
>
Попробовать снова
</button>
</div>
) : filteredAgents.length === 0 ? (
<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 ? "Ничего не найдено" : "Нет агентов"}
</p>
</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"
>
<span style={{ color: "var(--text-secondary)" }}>{service.name}</span>
<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)"
}`,
}}
>
{service.status}
</span>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
{/* Footer с кнопками */}
<div
className="p-2 border-t flex gap-2"
style={{ borderColor: "var(--border)", backgroundColor: "var(--card-bg)" }}
>
<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"
style={{
backgroundColor: "var(--accent)",
color: "var(--accent-text)",
}}
>
<FaCopy size={10} />
Токен
</button>
</div>
</aside>
{/* Modal токена */}
{showTokenModal && (
<div
className="fixed inset-0 z-[60] flex items-center justify-center p-4"
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
onClick={() => setShowTokenModal(false)}
>
<div
className="w-full max-w-md rounded-xl shadow-2xl border"
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--border)" }}
onClick={(e) => e.stopPropagation()}
>
<div
className="flex items-center justify-between px-4 py-3 border-b"
style={{ borderColor: "var(--border)" }}
>
<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>
</div>
<button
onClick={() => setShowTokenModal(false)}
className="p-1 rounded transition-colors"
style={{ color: "var(--text-secondary)" }}
>
<FaTimes size={14} />
</button>
</div>
<div className="p-4 space-y-3">
<div>
<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)" }}
>
<code
className="flex-1 text-xs font-mono break-all"
style={{ color: "var(--text-primary)" }}
>
{token || "Токен не найден"}
</code>
{token && (
<button
onClick={handleCopyToken}
className="p-1.5 rounded transition-colors"
style={{ color: "var(--text-secondary)" }}
>
{copied ? (
<FaCheck size={12} style={{ color: "var(--success-text)" }} />
) : (
<FaCopy size={12} />
)}
</button>
)}
</div>
</div>
<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)" }}
>
Закрыть
</button>
</div>
</div>
</div>
)}
</>
);
};