151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
import { useState, useEffect, useMemo } from "react";
|
||
import {
|
||
Graph,
|
||
type GraphData,
|
||
type GraphNode,
|
||
type GraphLink,
|
||
} from "@/modules/graph";
|
||
import { agentApiService } from "@/modules/agent/api/agent.api.service";
|
||
import { FaSpinner } from "react-icons/fa";
|
||
|
||
import { useAgentStore } from "@/app/providers/layout/store/agent.store";
|
||
|
||
const buildGraphFromApi = (apiData: any, agents: any[]): GraphData => {
|
||
const nodes: GraphNode[] = [];
|
||
const links: GraphLink[] = [];
|
||
|
||
// Build a map of service statuses from agents
|
||
const serviceStatusMap = new Map<string, "up" | "down">();
|
||
agents.forEach((agent) => {
|
||
const services = agent.services || [];
|
||
services.forEach((svc: string) => {
|
||
// Parse "serviceName:up" or "serviceName:down"
|
||
const parts = svc.split(":");
|
||
const svcName = parts[0];
|
||
const status = parts[1] === "down" ? "down" : "up";
|
||
serviceStatusMap.set(`${agent.label}-${svcName}`, status);
|
||
});
|
||
});
|
||
|
||
if (!apiData?.nodes) return { nodes, links };
|
||
|
||
Object.entries(apiData.nodes).forEach(
|
||
([agentLabel, agentNode]: [string, any]) => {
|
||
// Агент как узел
|
||
nodes.push({
|
||
id: agentLabel,
|
||
name: agentLabel,
|
||
type: "agent",
|
||
val: 8,
|
||
description: `Агент: ${agentLabel}`,
|
||
});
|
||
|
||
// Сервисы агента
|
||
const services = agentNode?.services || {};
|
||
Object.entries(services).forEach(
|
||
([serviceName, serviceNode]: [string, any]) => {
|
||
const serviceId = `${agentLabel}-${serviceName}`;
|
||
const status = serviceStatusMap.get(serviceId) || "up";
|
||
|
||
nodes.push({
|
||
id: serviceId,
|
||
name: serviceName,
|
||
type: "service",
|
||
val: 12,
|
||
description: `Сервис: ${serviceName}`,
|
||
status,
|
||
});
|
||
|
||
// Связь агент → сервис
|
||
links.push({
|
||
source: agentLabel,
|
||
target: serviceId,
|
||
type: "hosts",
|
||
});
|
||
|
||
// Зависимости между сервисами
|
||
const dependencies = serviceNode?.dependencies || [];
|
||
dependencies.forEach((dep: any) => {
|
||
const targetServiceName = dep?.target?.name;
|
||
if (targetServiceName) {
|
||
const targetServiceId = `${agentLabel}-${targetServiceName}`;
|
||
links.push({
|
||
source: serviceId,
|
||
target: targetServiceId,
|
||
type: dep.condition || "dependency",
|
||
});
|
||
}
|
||
});
|
||
},
|
||
);
|
||
},
|
||
);
|
||
|
||
return { nodes, links };
|
||
};
|
||
|
||
export const GraphsPage = () => {
|
||
const agents = useAgentStore((s) => s.agents);
|
||
const [graphData, setGraphData] = useState<GraphData>({
|
||
nodes: [],
|
||
links: [],
|
||
});
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
const fetchGraph = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const apiData = await agentApiService.getGraph();
|
||
const data = buildGraphFromApi(apiData, agents);
|
||
setGraphData(data);
|
||
} catch (e) {
|
||
console.error("Failed to fetch graph:", e);
|
||
setError(e instanceof Error ? e.message : "Failed to load graph");
|
||
setGraphData({ nodes: [], links: [] });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchGraph();
|
||
const interval = setInterval(fetchGraph, 30000);
|
||
return () => clearInterval(interval);
|
||
}, [agents]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="h-full flex items-center justify-center">
|
||
<div className="text-center">
|
||
<FaSpinner
|
||
className="animate-spin mx-auto mb-4"
|
||
size={32}
|
||
style={{ color: "var(--accent)" }}
|
||
/>
|
||
<p style={{ color: "var(--text-secondary)" }}>Загрузка графа...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error && graphData.nodes.length === 0) {
|
||
return (
|
||
<div className="h-full flex items-center justify-center">
|
||
<div className="text-center">
|
||
<p style={{ color: "var(--error-text)", marginBottom: "12px" }}>
|
||
{error}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="h-full">
|
||
<Graph initialData={graphData} />
|
||
</div>
|
||
);
|
||
};
|