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 [showGraphs, setShowGraphs] = useState(false);
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) => {
const next = new Set(prev);
if (next.has(name)) next.delete(name);
else next.add(name);
if (next.has(label)) next.delete(label);
else next.add(label);
return next;
});
};
@@ -50,8 +50,8 @@ export const Sidebar: React.FC<SidebarProps> = ({
const query = searchQuery.toLowerCase();
return agents.filter(
(agent) =>
agent.name.toLowerCase().includes(query) ||
agent.services.some((s) => s.name.toLowerCase().includes(query)),
agent.label.toLowerCase().includes(query) ||
agent.services.some((s) => s.toLowerCase().includes(query)),
);
}, [agents, searchQuery]);
@@ -61,25 +61,25 @@ export const Sidebar: React.FC<SidebarProps> = ({
agents.forEach((agent) => {
nodes.push({
id: agent.name,
name: agent.name,
id: agent.label,
name: agent.label,
type: "agent" as const,
val: 8,
description: `Агент: ${agent.name}`,
description: `Агент: ${agent.label}`,
});
agent.services.forEach((service) => {
const serviceId = `${agent.name}-${service.name}`;
const serviceId = `${agent.label}-${service}`;
nodes.push({
id: serviceId,
name: service.name,
name: service,
type: "service" as const,
val: 12,
description: `Сервис: ${service.name} (${service.status})`,
description: `Сервис: ${service}`,
});
links.push({
source: agent.name,
source: agent.label,
target: serviceId,
type: "hosts",
});
@@ -239,10 +239,10 @@ export const Sidebar: React.FC<SidebarProps> = ({
) : (
<div className="space-y-1">
{filteredAgents.map((agent) => {
const isExpanded = expandedAgents.has(agent.name);
const isExpanded = expandedAgents.has(agent.label);
return (
<div
key={agent.name}
key={agent.label}
className="rounded-lg border overflow-hidden transition-all group"
style={{
backgroundColor: "var(--bg-secondary)",
@@ -252,7 +252,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
{/* Agent header — кликабельный для сворачивания */}
<div
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)" }}>
{isExpanded ? (
@@ -269,13 +269,11 @@ export const Sidebar: React.FC<SidebarProps> = ({
className="text-sm font-medium flex-1 truncate"
style={{ color: "var(--text-primary)" }}
>
{agent.name}
{agent.label}
</span>
{/* Статус-индикатор агента (сколько сервисов запущено) */}
{/* Статус-индикатор агента (количество сервисов) */}
<div className="flex items-center gap-1">
{agent.services.filter(
(s) => s.status === "running",
).length > 0 && (
{agent.services.length > 0 && (
<span
className="w-2 h-2 rounded-full"
style={{ backgroundColor: "#4ade80" }}
@@ -285,12 +283,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
className="text-[10px]"
style={{ color: "var(--text-muted)" }}
>
{
agent.services.filter(
(s) => s.status === "running",
).length
}
/{agent.services.length}
{agent.services.length}
</span>
</div>
{/* Кнопка удаления — появляется при наведении */}
@@ -299,10 +292,10 @@ export const Sidebar: React.FC<SidebarProps> = ({
e.stopPropagation();
if (
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"
@@ -336,49 +329,32 @@ export const Sidebar: React.FC<SidebarProps> = ({
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}
key={service}
className="flex items-center justify-between py-1"
>
<span
className="text-xs"
style={{ color: "var(--text-secondary)" }}
>
{service.name}
{service}
</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",
backgroundColor: "#4ade80",
}}
/>
<span
className="text-[10px] font-medium"
style={{
color: isRunning
? "#4ade80"
: isError
? "#f87171"
: "#777",
color: "#4ade80",
}}
>
{isRunning
? "run"
: isError
? "err"
: "stop"}
run
</span>
</div>
</div>
@@ -12,30 +12,22 @@ interface AgentState {
const mockAgents: AgentInfo[] = [
{
name: "agent-core-01",
label: "agent-core-01",
token: "tok_a1b2c3d4e5f6g7h8",
services: [
{ name: "postgres", status: "running" },
{ name: "redis", status: "running" },
{ name: "log-collector", status: "running" },
],
services: ["postgres", "redis", "log-collector"],
connected_at: "2026-04-04 15:25:09",
},
{
name: "agent-worker-02",
label: "agent-worker-02",
token: "tok_x9y8z7w6v5u4t3s2",
services: [
{ name: "celery-worker", status: "running" },
{ name: "flower", status: "stopped" },
],
services: ["celery-worker", "flower"],
connected_at: "2026-04-04 15:25:09",
},
{
name: "agent-monitor-03",
label: "agent-monitor-03",
token: "tok_m1n2o3p4q5r6s7t8",
services: [
{ name: "prometheus", status: "running" },
{ name: "grafana", status: "running" },
{ name: "alertmanager", status: "stopped" },
],
services: ["prometheus", "grafana", "alertmanager"],
connected_at: "2026-04-04 15:25:09",
},
];
@@ -59,6 +51,6 @@ export const useAgentStore = create<AgentState>()((set, get) => ({
},
removeAgent: (name: string) => {
set({ agents: get().agents.filter((a) => a.name !== name) });
set({ agents: get().agents.filter((a) => a.label !== name) });
},
}));
@@ -45,17 +45,20 @@ class AgentApiService {
}
async searchLogs(filters?: LogFilters): Promise<LogEntry[]> {
const response = await apiClient.get<LogEntry[]>(this.logsBasePath, {
params: {
level: filters?.level,
service: filters?.service,
agent: filters?.agent,
date_from: filters?.date_from,
date_to: filters?.date_to,
limit: filters?.limit ?? 100,
offset: filters?.offset ?? 0,
const response = await apiClient.get<LogEntry[]>(
`${this.logsBasePath}/mock`,
{
params: {
level: filters?.level,
service: filters?.service,
agent: filters?.agent,
date_from: filters?.date_from,
date_to: filters?.date_to,
limit: filters?.limit ?? 100,
offset: filters?.offset ?? 0,
},
},
});
);
return response.data;
}
@@ -1,12 +1,8 @@
export interface AgentService {
name: string;
status: string;
}
export interface AgentInfo {
name: string;
services: AgentService[];
token: string;
label: string;
services: string[];
connected_at: string;
}
export interface LoginRequest {
+13 -8
View File
@@ -1,5 +1,10 @@
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";
const buildGraphFromAgents = (): GraphData => {
@@ -10,26 +15,26 @@ const buildGraphFromAgents = (): GraphData => {
agents.forEach((agent) => {
// Агент как узел
nodes.push({
id: agent.name,
name: agent.name,
id: agent.label,
name: agent.label,
type: "agent",
val: 8,
description: `Агент: ${agent.name}`,
description: `Агент: ${agent.label}`,
});
// Сервисы агента как узлы + связи
agent.services.forEach((service) => {
const serviceId = `${agent.name}-${service.name}`;
const serviceId = `${agent.label}-${service}`;
nodes.push({
id: serviceId,
name: service.name,
name: service,
type: "service",
val: 12,
description: `Сервис: ${service.name} (${service.status})`,
description: `Сервис: ${service}`,
});
links.push({
source: agent.name,
source: agent.label,
target: serviceId,
type: "hosts",
});
+101 -25
View File
@@ -21,11 +21,30 @@ const logLevelIcons: Record<string, React.ReactNode> = {
FATAL: <FiXOctagon size={14} />,
};
const logLevelColors: Record<string, { bg: string; text: string; border: string }> = {
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)" },
const logLevelColors: Record<
string,
{ bg: string; text: string; border: string }
> = {
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 = () => {
@@ -47,7 +66,9 @@ export const LogsPage: React.FC = () => {
setLogs(data);
setTotalLogs(data.length);
} catch (err) {
setError(err instanceof Error ? err.message : "Ошибка при загрузке логов");
setError(
err instanceof Error ? err.message : "Ошибка при загрузке логов",
);
} finally {
setIsLoading(false);
}
@@ -112,7 +133,10 @@ export const LogsPage: React.FC = () => {
className="w-14 h-14 rounded-xl flex items-center justify-center"
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>
<h1
@@ -160,8 +184,14 @@ export const LogsPage: React.FC = () => {
}}
>
{/* Table Header */}
<div 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)" }}>
<div
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} записей
</span>
<button
@@ -173,18 +203,27 @@ export const LogsPage: React.FC = () => {
borderColor: "var(--border)",
}}
>
<FiRefreshCw size={12} className={isLoading ? "animate-spin" : ""} />
<FiRefreshCw
size={12}
className={isLoading ? "animate-spin" : ""}
/>
Обновить
</button>
</div>
{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" />
Загрузка логов...
</div>
) : 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>
) : (
@@ -193,36 +232,58 @@ export const LogsPage: React.FC = () => {
<table className="w-full">
<thead>
<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 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 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 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 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>
</tr>
</thead>
<tbody>
{logs.map((log, index) => {
const colors = logLevelColors[log.level] || logLevelColors.INFO;
const colors =
logLevelColors[log.level] || logLevelColors.INFO;
return (
<tr
key={index}
className="border-t"
style={{
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)}
</td>
<td className="px-4 py-3">
@@ -238,13 +299,22 @@ export const LogsPage: React.FC = () => {
{log.level}
</span>
</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}
</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}
</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}
</td>
</tr>
@@ -255,7 +325,10 @@ export const LogsPage: React.FC = () => {
</div>
{/* 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
onClick={handlePrevPage}
disabled={offset === 0}
@@ -269,7 +342,10 @@ export const LogsPage: React.FC = () => {
<FiChevronLeft size={16} />
Назад
</button>
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>
<span
className="text-sm"
style={{ color: "var(--text-secondary)" }}
>
Показано {logs.length} записей (смещение: {offset})
</span>
<button
+4 -4
View File
@@ -25,12 +25,12 @@ export const useWebSocketService = ({
const selectedServices: string[] = [];
const selectedHosts: string[] = [];
// TODO: реализовать механизм выбора сервисов
// Пока выбираем все
agents.forEach((agent) => {
agent.services.forEach((service) => {
if (service.isSelected) {
selectedServices.push(service.name);
selectedHosts.push(agent.token);
}
selectedServices.push(service);
selectedHosts.push(agent.token);
});
});