feat: graph-page
ci-front / build (push) Successful in 1m58s

This commit is contained in:
nikita
2026-04-04 12:14:17 +03:00
parent 26ca7c0d51
commit aac3fa3758
14 changed files with 757 additions and 537 deletions
+125
View File
@@ -0,0 +1,125 @@
import React, { useRef, useEffect, useState } from "react";
import type {
GraphData,
GraphNode,
GraphLink,
ContextMenuState,
} from "./types";
import { useGraphStore } from "./store/useGraphStore";
import {
ForceGraph,
GraphControls,
GraphContextMenu,
GraphStatusBar,
} from "./components";
interface GraphProps {
initialData?: GraphData;
onExport?: () => void;
onDataChange?: (data: GraphData) => void;
}
export const Graph: React.FC<GraphProps> = ({
initialData,
onExport,
onDataChange,
}) => {
const fgRef = useRef<any>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const data = useGraphStore((s) => s.data);
const isLinkMode = useGraphStore((s) => s.isLinkMode);
const selectedNode = useGraphStore((s) => s.selectedNode);
const setData = useGraphStore((s) => s.setData);
// Инициализация данных
useEffect(() => {
if (initialData) setData(initialData);
}, [initialData, setData]);
// Отслеживаем размеры контейнера
useEffect(() => {
const container = fgRef.current?.parentElement;
if (!container) return;
const updateDimensions = () => {
setDimensions({
width: container.clientWidth,
height: container.clientHeight || window.innerHeight - 160,
});
};
updateDimensions();
const observer = new ResizeObserver(updateDimensions);
observer.observe(container);
return () => observer.disconnect();
}, []);
// Закрыть контекстное меню по клику вне
useEffect(() => {
const handleClickOutside = () => setContextMenu(null);
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, []);
const handleNodeRightClick = (node: GraphNode, event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
setContextMenu({ x: event.clientX, y: event.clientY, node, link: null });
};
const handleLinkRightClick = (link: GraphLink, event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
setContextMenu({ x: event.clientX, y: event.clientY, node: null, link });
};
if (!data || data.nodes.length === 0) {
return (
<div className="bg-gray-900 rounded-xl shadow-lg p-6">
<div className="flex items-center justify-center h-96">
<div className="text-center">
<p className="text-gray-400 mb-4">Нет данных для отображения</p>
</div>
</div>
</div>
);
}
return (
<div className="bg-gray-900 rounded-xl shadow-lg p-6">
<div
className="border border-gray-800 rounded-lg overflow-hidden relative"
style={{
height: "calc(100vh - 200px)",
position: "relative",
width: "100%",
}}
>
<ForceGraph
data={data}
onNodeRightClick={handleNodeRightClick}
onLinkRightClick={handleLinkRightClick}
/>
<GraphContextMenu
menu={contextMenu}
data={data}
onClose={() => setContextMenu(null)}
/>
<GraphStatusBar isLinkMode={isLinkMode} selectedNode={selectedNode} />
</div>
<GraphControls
fgRef={fgRef}
onExport={onExport}
onDataChange={onDataChange}
/>
</div>
);
};
export default Graph;