feat
ci-front / build (push) Successful in 2m26s

This commit is contained in:
2026-04-04 19:49:37 +03:00
parent 69ff617c30
commit c6a9907822
7 changed files with 171 additions and 123 deletions
@@ -33,14 +33,14 @@ export const Sidebar: React.FC<SidebarProps> = ({
const [showTokenModal, setShowTokenModal] = useState(false); const [showTokenModal, setShowTokenModal] = useState(false);
const [showGraphs, setShowGraphs] = useState(false); const [showGraphs, setShowGraphs] = useState(false);
const [expandedAgents, setExpandedAgents] = useState<Set<string>>( const [expandedAgents, setExpandedAgents] = useState<Set<string>>(
new Set(agents.map((a) => a.name)), new Set(agents.map((a) => a.label)),
); );
const toggleAgent = (name: string) => { const toggleAgent = (label: string) => {
setExpandedAgents((prev) => { setExpandedAgents((prev) => {
const next = new Set(prev); const next = new Set(prev);
if (next.has(name)) next.delete(name); if (next.has(label)) next.delete(label);
else next.add(name); else next.add(label);
return next; return next;
}); });
}; };
@@ -50,8 +50,8 @@ export const Sidebar: React.FC<SidebarProps> = ({
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return agents.filter( return agents.filter(
(agent) => (agent) =>
agent.name.toLowerCase().includes(query) || agent.label.toLowerCase().includes(query) ||
agent.services.some((s) => s.name.toLowerCase().includes(query)), agent.services.some((s) => s.toLowerCase().includes(query)),
); );
}, [agents, searchQuery]); }, [agents, searchQuery]);
@@ -61,25 +61,25 @@ export const Sidebar: React.FC<SidebarProps> = ({
agents.forEach((agent) => { agents.forEach((agent) => {
nodes.push({ nodes.push({
id: agent.name, id: agent.label,
name: agent.name, name: agent.label,
type: "agent" as const, type: "agent" as const,
val: 8, val: 8,
description: `Агент: ${agent.name}`, description: `Агент: ${agent.label}`,
}); });
agent.services.forEach((service) => { agent.services.forEach((service) => {
const serviceId = `${agent.name}-${service.name}`; const serviceId = `${agent.label}-${service}`;
nodes.push({ nodes.push({
id: serviceId, id: serviceId,
name: service.name, name: service,
type: "service" as const, type: "service" as const,
val: 12, val: 12,
description: `Сервис: ${service.name} (${service.status})`, description: `Сервис: ${service}`,
}); });
links.push({ links.push({
source: agent.name, source: agent.label,
target: serviceId, target: serviceId,
type: "hosts", type: "hosts",
}); });
@@ -239,10 +239,10 @@ export const Sidebar: React.FC<SidebarProps> = ({
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{filteredAgents.map((agent) => { {filteredAgents.map((agent) => {
const isExpanded = expandedAgents.has(agent.name); const isExpanded = expandedAgents.has(agent.label);
return ( return (
<div <div
key={agent.name} key={agent.label}
className="rounded-lg border overflow-hidden transition-all group" className="rounded-lg border overflow-hidden transition-all group"
style={{ style={{
backgroundColor: "var(--bg-secondary)", backgroundColor: "var(--bg-secondary)",
@@ -252,7 +252,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
{/* Agent header — кликабельный для сворачивания */} {/* Agent header — кликабельный для сворачивания */}
<div <div
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity" className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => toggleAgent(agent.name)} onClick={() => toggleAgent(agent.label)}
> >
<span style={{ color: "var(--text-muted)" }}> <span style={{ color: "var(--text-muted)" }}>
{isExpanded ? ( {isExpanded ? (
@@ -269,13 +269,11 @@ export const Sidebar: React.FC<SidebarProps> = ({
className="text-sm font-medium flex-1 truncate" className="text-sm font-medium flex-1 truncate"
style={{ color: "var(--text-primary)" }} style={{ color: "var(--text-primary)" }}
> >
{agent.name} {agent.label}
</span> </span>
{/* Статус-индикатор агента (сколько сервисов запущено) */} {/* Статус-индикатор агента (количество сервисов) */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{agent.services.filter( {agent.services.length > 0 && (
(s) => s.status === "running",
).length > 0 && (
<span <span
className="w-2 h-2 rounded-full" className="w-2 h-2 rounded-full"
style={{ backgroundColor: "#4ade80" }} style={{ backgroundColor: "#4ade80" }}
@@ -285,12 +283,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
className="text-[10px]" className="text-[10px]"
style={{ color: "var(--text-muted)" }} style={{ color: "var(--text-muted)" }}
> >
{ {agent.services.length}
agent.services.filter(
(s) => s.status === "running",
).length
}
/{agent.services.length}
</span> </span>
</div> </div>
{/* Кнопка удаления — появляется при наведении */} {/* Кнопка удаления — появляется при наведении */}
@@ -299,10 +292,10 @@ export const Sidebar: React.FC<SidebarProps> = ({
e.stopPropagation(); e.stopPropagation();
if ( if (
window.confirm( window.confirm(
`Удалить агента "${agent.name}"?`, `Удалить агента "${agent.label}"?`,
) )
) { ) {
removeAgent(agent.name); removeAgent(agent.label);
} }
}} }}
className="opacity-0 group-hover:opacity-100 p-1 rounded transition-all flex-shrink-0" className="opacity-0 group-hover:opacity-100 p-1 rounded transition-all flex-shrink-0"
@@ -336,49 +329,32 @@ export const Sidebar: React.FC<SidebarProps> = ({
style={{ borderColor: "var(--border)" }} style={{ borderColor: "var(--border)" }}
> >
{agent.services.map((service) => { {agent.services.map((service) => {
const isRunning = service.status === "running";
const isError = service.status === "error";
const isStopped =
service.status === "stopped" || !isRunning;
return ( return (
<div <div
key={service.name} key={service}
className="flex items-center justify-between py-1" className="flex items-center justify-between py-1"
> >
<span <span
className="text-xs" className="text-xs"
style={{ color: "var(--text-secondary)" }} style={{ color: "var(--text-secondary)" }}
> >
{service.name} {service}
</span> </span>
{/* Status indicator */} {/* Status indicator */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span <span
className="w-1.5 h-1.5 rounded-full flex-shrink-0" className="w-1.5 h-1.5 rounded-full flex-shrink-0"
style={{ style={{
backgroundColor: isRunning backgroundColor: "#4ade80",
? "#4ade80"
: isError
? "#f87171"
: "#555",
}} }}
/> />
<span <span
className="text-[10px] font-medium" className="text-[10px] font-medium"
style={{ style={{
color: isRunning color: "#4ade80",
? "#4ade80"
: isError
? "#f87171"
: "#777",
}} }}
> >
{isRunning run
? "run"
: isError
? "err"
: "stop"}
</span> </span>
</div> </div>
</div> </div>
@@ -12,30 +12,22 @@ interface AgentState {
const mockAgents: AgentInfo[] = [ const mockAgents: AgentInfo[] = [
{ {
name: "agent-core-01", label: "agent-core-01",
token: "tok_a1b2c3d4e5f6g7h8", token: "tok_a1b2c3d4e5f6g7h8",
services: [ services: ["postgres", "redis", "log-collector"],
{ name: "postgres", status: "running" }, connected_at: "2026-04-04 15:25:09",
{ name: "redis", status: "running" },
{ name: "log-collector", status: "running" },
],
}, },
{ {
name: "agent-worker-02", label: "agent-worker-02",
token: "tok_x9y8z7w6v5u4t3s2", token: "tok_x9y8z7w6v5u4t3s2",
services: [ services: ["celery-worker", "flower"],
{ name: "celery-worker", status: "running" }, connected_at: "2026-04-04 15:25:09",
{ name: "flower", status: "stopped" },
],
}, },
{ {
name: "agent-monitor-03", label: "agent-monitor-03",
token: "tok_m1n2o3p4q5r6s7t8", token: "tok_m1n2o3p4q5r6s7t8",
services: [ services: ["prometheus", "grafana", "alertmanager"],
{ name: "prometheus", status: "running" }, connected_at: "2026-04-04 15:25:09",
{ name: "grafana", status: "running" },
{ name: "alertmanager", status: "stopped" },
],
}, },
]; ];
@@ -59,6 +51,6 @@ export const useAgentStore = create<AgentState>()((set, get) => ({
}, },
removeAgent: (name: string) => { removeAgent: (name: string) => {
set({ agents: get().agents.filter((a) => a.name !== name) }); set({ agents: get().agents.filter((a) => a.label !== name) });
}, },
})); }));
@@ -45,7 +45,9 @@ class AgentApiService {
} }
async searchLogs(filters?: LogFilters): Promise<LogEntry[]> { async searchLogs(filters?: LogFilters): Promise<LogEntry[]> {
const response = await apiClient.get<LogEntry[]>(this.logsBasePath, { const response = await apiClient.get<LogEntry[]>(
`${this.logsBasePath}/mock`,
{
params: { params: {
level: filters?.level, level: filters?.level,
service: filters?.service, service: filters?.service,
@@ -55,7 +57,8 @@ class AgentApiService {
limit: filters?.limit ?? 100, limit: filters?.limit ?? 100,
offset: filters?.offset ?? 0, offset: filters?.offset ?? 0,
}, },
}); },
);
return response.data; return response.data;
} }
@@ -1,12 +1,8 @@
export interface AgentService {
name: string;
status: string;
}
export interface AgentInfo { export interface AgentInfo {
name: string;
services: AgentService[];
token: string; token: string;
label: string;
services: string[];
connected_at: string;
} }
export interface LoginRequest { export interface LoginRequest {
+13 -8
View File
@@ -1,5 +1,10 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Graph, type GraphData, type GraphNode, type GraphLink } from "@/modules/graph"; import {
Graph,
type GraphData,
type GraphNode,
type GraphLink,
} from "@/modules/graph";
import { useAgentStore } from "@/app/providers/layout/store/agent.store"; import { useAgentStore } from "@/app/providers/layout/store/agent.store";
const buildGraphFromAgents = (): GraphData => { const buildGraphFromAgents = (): GraphData => {
@@ -10,26 +15,26 @@ const buildGraphFromAgents = (): GraphData => {
agents.forEach((agent) => { agents.forEach((agent) => {
// Агент как узел // Агент как узел
nodes.push({ nodes.push({
id: agent.name, id: agent.label,
name: agent.name, name: agent.label,
type: "agent", type: "agent",
val: 8, val: 8,
description: `Агент: ${agent.name}`, description: `Агент: ${agent.label}`,
}); });
// Сервисы агента как узлы + связи // Сервисы агента как узлы + связи
agent.services.forEach((service) => { agent.services.forEach((service) => {
const serviceId = `${agent.name}-${service.name}`; const serviceId = `${agent.label}-${service}`;
nodes.push({ nodes.push({
id: serviceId, id: serviceId,
name: service.name, name: service,
type: "service", type: "service",
val: 12, val: 12,
description: `Сервис: ${service.name} (${service.status})`, description: `Сервис: ${service}`,
}); });
links.push({ links.push({
source: agent.name, source: agent.label,
target: serviceId, target: serviceId,
type: "hosts", type: "hosts",
}); });
+101 -25
View File
@@ -21,11 +21,30 @@ const logLevelIcons: Record<string, React.ReactNode> = {
FATAL: <FiXOctagon size={14} />, FATAL: <FiXOctagon size={14} />,
}; };
const logLevelColors: Record<string, { bg: string; text: string; border: string }> = { const logLevelColors: Record<
INFO: { bg: "var(--info-bg)", text: "var(--info-text)", border: "var(--info-border)" }, string,
WARNING: { bg: "var(--warning-bg)", text: "var(--warning-text)", border: "var(--warning-border)" }, { bg: string; text: string; border: string }
ERROR: { bg: "var(--error-bg)", text: "var(--error-text)", border: "var(--error-border)" }, > = {
FATAL: { bg: "var(--fatal-bg)", text: "var(--fatal-text)", border: "var(--fatal-border)" }, INFO: {
bg: "var(--info-bg)",
text: "var(--info-text)",
border: "var(--info-border)",
},
WARNING: {
bg: "var(--warning-bg)",
text: "var(--warning-text)",
border: "var(--warning-border)",
},
ERROR: {
bg: "var(--error-bg)",
text: "var(--error-text)",
border: "var(--error-border)",
},
FATAL: {
bg: "var(--fatal-bg)",
text: "var(--fatal-text)",
border: "var(--fatal-border)",
},
}; };
export const LogsPage: React.FC = () => { export const LogsPage: React.FC = () => {
@@ -47,7 +66,9 @@ export const LogsPage: React.FC = () => {
setLogs(data); setLogs(data);
setTotalLogs(data.length); setTotalLogs(data.length);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Ошибка при загрузке логов"); setError(
err instanceof Error ? err.message : "Ошибка при загрузке логов",
);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -112,7 +133,10 @@ export const LogsPage: React.FC = () => {
className="w-14 h-14 rounded-xl flex items-center justify-center" className="w-14 h-14 rounded-xl flex items-center justify-center"
style={{ backgroundColor: "var(--bg-secondary)" }} style={{ backgroundColor: "var(--bg-secondary)" }}
> >
<FiFileText className="w-7 h-7" style={{ color: "var(--accent)" }} /> <FiFileText
className="w-7 h-7"
style={{ color: "var(--accent)" }}
/>
</div> </div>
<div> <div>
<h1 <h1
@@ -160,8 +184,14 @@ export const LogsPage: React.FC = () => {
}} }}
> >
{/* Table Header */} {/* Table Header */}
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: "var(--border)" }}> <div
<span className="text-sm font-medium" style={{ color: "var(--text-primary)" }}> className="flex items-center justify-between px-4 py-3 border-b"
style={{ borderColor: "var(--border)" }}
>
<span
className="text-sm font-medium"
style={{ color: "var(--text-primary)" }}
>
Найдено: {totalLogs} записей Найдено: {totalLogs} записей
</span> </span>
<button <button
@@ -173,18 +203,27 @@ export const LogsPage: React.FC = () => {
borderColor: "var(--border)", borderColor: "var(--border)",
}} }}
> >
<FiRefreshCw size={12} className={isLoading ? "animate-spin" : ""} /> <FiRefreshCw
size={12}
className={isLoading ? "animate-spin" : ""}
/>
Обновить Обновить
</button> </button>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-12" style={{ color: "var(--text-secondary)" }}> <div
className="flex items-center justify-center py-12"
style={{ color: "var(--text-secondary)" }}
>
<FiRefreshCw size={24} className="animate-spin mr-3" /> <FiRefreshCw size={24} className="animate-spin mr-3" />
Загрузка логов... Загрузка логов...
</div> </div>
) : logs.length === 0 ? ( ) : logs.length === 0 ? (
<div className="text-center py-12" style={{ color: "var(--text-muted)" }}> <div
className="text-center py-12"
style={{ color: "var(--text-muted)" }}
>
Логи не найдены Логи не найдены
</div> </div>
) : ( ) : (
@@ -193,36 +232,58 @@ export const LogsPage: React.FC = () => {
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr style={{ backgroundColor: "var(--bg-secondary)" }}> <tr style={{ backgroundColor: "var(--bg-secondary)" }}>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}> <th
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: "var(--text-secondary)" }}
>
Время Время
</th> </th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}> <th
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: "var(--text-secondary)" }}
>
Уровень Уровень
</th> </th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}> <th
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: "var(--text-secondary)" }}
>
Сервис Сервис
</th> </th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}> <th
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: "var(--text-secondary)" }}
>
Агент Агент
</th> </th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}> <th
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: "var(--text-secondary)" }}
>
Сообщение Сообщение
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{logs.map((log, index) => { {logs.map((log, index) => {
const colors = logLevelColors[log.level] || logLevelColors.INFO; const colors =
logLevelColors[log.level] || logLevelColors.INFO;
return ( return (
<tr <tr
key={index} key={index}
className="border-t" className="border-t"
style={{ style={{
borderColor: "var(--border)", borderColor: "var(--border)",
backgroundColor: index % 2 === 0 ? "var(--card-bg)" : "var(--bg-secondary)", backgroundColor:
index % 2 === 0
? "var(--card-bg)"
: "var(--bg-secondary)",
}} }}
> >
<td className="px-4 py-3 text-sm font-mono whitespace-nowrap" style={{ color: "var(--text-secondary)" }}> <td
className="px-4 py-3 text-sm font-mono whitespace-nowrap"
style={{ color: "var(--text-secondary)" }}
>
{formatTimestamp(log.timestamp)} {formatTimestamp(log.timestamp)}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
@@ -238,13 +299,22 @@ export const LogsPage: React.FC = () => {
{log.level} {log.level}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-sm" style={{ color: "var(--text-primary)" }}> <td
className="px-4 py-3 text-sm"
style={{ color: "var(--text-primary)" }}
>
{log.service} {log.service}
</td> </td>
<td className="px-4 py-3 text-sm font-mono" style={{ color: "var(--text-primary)" }}> <td
className="px-4 py-3 text-sm font-mono"
style={{ color: "var(--text-primary)" }}
>
{log.agent} {log.agent}
</td> </td>
<td className="px-4 py-3 text-sm" style={{ color: "var(--text-primary)" }}> <td
className="px-4 py-3 text-sm"
style={{ color: "var(--text-primary)" }}
>
{log.message} {log.message}
</td> </td>
</tr> </tr>
@@ -255,7 +325,10 @@ export const LogsPage: React.FC = () => {
</div> </div>
{/* Pagination */} {/* Pagination */}
<div className="flex items-center justify-between px-4 py-3 border-t" style={{ borderColor: "var(--border)" }}> <div
className="flex items-center justify-between px-4 py-3 border-t"
style={{ borderColor: "var(--border)" }}
>
<button <button
onClick={handlePrevPage} onClick={handlePrevPage}
disabled={offset === 0} disabled={offset === 0}
@@ -269,7 +342,10 @@ export const LogsPage: React.FC = () => {
<FiChevronLeft size={16} /> <FiChevronLeft size={16} />
Назад Назад
</button> </button>
<span className="text-sm" style={{ color: "var(--text-secondary)" }}> <span
className="text-sm"
style={{ color: "var(--text-secondary)" }}
>
Показано {logs.length} записей (смещение: {offset}) Показано {logs.length} записей (смещение: {offset})
</span> </span>
<button <button
+3 -3
View File
@@ -25,12 +25,12 @@ export const useWebSocketService = ({
const selectedServices: string[] = []; const selectedServices: string[] = [];
const selectedHosts: string[] = []; const selectedHosts: string[] = [];
// TODO: реализовать механизм выбора сервисов
// Пока выбираем все
agents.forEach((agent) => { agents.forEach((agent) => {
agent.services.forEach((service) => { agent.services.forEach((service) => {
if (service.isSelected) { selectedServices.push(service);
selectedServices.push(service.name);
selectedHosts.push(agent.token); selectedHosts.push(agent.token);
}
}); });
}); });