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
@@ -0,0 +1,149 @@
import React from "react";
import {
FiDownload,
FiZoomIn,
FiZoomOut,
FiMove,
FiPlus,
FiLink,
} from "react-icons/fi";
import { useGraphStore } from "../store/useGraphStore";
import type { GraphData } from "../types";
interface GraphControlsProps {
fgRef: React.RefObject<any>;
onExport?: () => void;
onDataChange?: (data: GraphData) => void;
}
export const GraphControls: React.FC<GraphControlsProps> = ({
fgRef,
onExport,
onDataChange,
}) => {
const isLinkMode = useGraphStore((s) => s.isLinkMode);
const selectedNode = useGraphStore((s) => s.selectedNode);
const data = useGraphStore((s) => s.data);
const toggleLinkMode = useGraphStore((s) => s.toggleLinkMode);
const addNode = useGraphStore((s) => s.addNode);
const exportData = useGraphStore((s) => s.exportData);
const handleAddNode = () => {
const newNodeName = prompt(
"Введите имя узла:",
`Node ${data.nodes.length + 1}`,
);
if (newNodeName) {
const isService = window.confirm(
"Выберите тип: OK - Сервис, Отмена - Агент",
);
addNode({
id: `node-${Date.now()}`,
name: newNodeName,
type: isService ? "service" : "agent",
val: isService ? 12 : 8,
description: "Новый узел",
});
}
};
const handleZoomIn = () => {
if (fgRef.current) {
const currentZoom = fgRef.current.zoom();
fgRef.current.zoom(currentZoom * 1.2);
}
};
const handleZoomOut = () => {
if (fgRef.current) {
const currentZoom = fgRef.current.zoom();
fgRef.current.zoom(currentZoom / 1.2);
}
};
const handleFit = () => {
if (fgRef.current) {
fgRef.current.zoomToFit(400);
}
};
return (
<div className="flex items-center justify-between mt-4 text-sm text-gray-400">
<div className="flex gap-6">
<div className="flex items-center gap-2">
<span>
Сервисы: {data.nodes.filter((n) => n.type === "service").length}
</span>
</div>
<div className="flex items-center gap-2">
<span>
Агенты: {data.nodes.filter((n) => n.type === "agent").length}
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gray-500 rounded-sm"></div>
<span>Связи: {data.links.length}</span>
</div>
</div>
<div className="flex gap-2">
{/* Режим создания связи */}
<button
onClick={toggleLinkMode}
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${
isLinkMode
? "bg-green-600 hover:bg-green-700 text-white"
: "bg-gray-800 hover:bg-gray-700 text-gray-300"
}`}
>
<FiLink />
<span className="text-sm">
{isLinkMode ? "Создание связи..." : "Добавить связь"}
</span>
</button>
{/* Добавить узел */}
<button
onClick={handleAddNode}
className="flex items-center gap-2 px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-gray-300"
>
<FiPlus />
<span className="text-sm">Узел</span>
</button>
{/* Зум + */}
<button
onClick={handleZoomIn}
className="p-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-gray-300"
>
<FiZoomIn />
</button>
{/* Зум - */}
<button
onClick={handleZoomOut}
className="p-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-gray-300"
>
<FiZoomOut />
</button>
{/* Fit */}
<button
onClick={handleFit}
className="p-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-gray-300"
>
<FiMove />
</button>
{/* Экспорт */}
<button
onClick={onExport || exportData}
className="flex items-center gap-2 px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-gray-300"
>
<FiDownload />
<span className="text-sm">Экспорт</span>
</button>
</div>
</div>
);
};