Compare commits
No commits in common. "3fc7ee0ac3165ca912b0e9d9a9d01f77fcb4414e" and "67f776efe3273fc76e0a9ddffc8833a9372b0030" have entirely different histories.
3fc7ee0ac3
...
67f776efe3
|
|
@ -23,8 +23,7 @@
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/material": "^6.4.7",
|
"@mui/material": "^6.4.7",
|
||||||
"@mui/icons-material": "^6.4.8",
|
"@mui/icons-material": "^6.4.8"
|
||||||
"reactflow": "^11.11.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
else if (range <= 86400) step = 120;
|
else if (range <= 86400) step = 120;
|
||||||
else step = 300;
|
else step = 300;
|
||||||
|
|
||||||
const response = await axios.get('http://192.168.2.39:3000/metrics', {
|
const response = await axios.get('https://192.168.2.43:3000/metrics', {
|
||||||
params: { metric: metricName, start, end, step },
|
params: { metric: metricName, start, end, step },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
<h2 style={{ padding: "16px", fontWeight: "bold", }}>Меню</h2>
|
<h2 style={{ padding: "16px", fontWeight: "bold" }}>Меню</h2>
|
||||||
<MenuItem item={data} onSelectItem={handleSelectItem} />
|
<MenuItem item={data} onSelectItem={handleSelectItem} />
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ListItem, ListItemIcon, ListItemText, Collapse, List } from "@mui/material";
|
import { Drawer, List, ListItem, ListItemIcon, ListItemText, Collapse } from "@mui/material";
|
||||||
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
||||||
|
|
||||||
// Функция для сбора всех потомков
|
// Функция для сбора всех потомков
|
||||||
|
|
@ -14,7 +14,7 @@ const getAllChildren = (node) => {
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен параметр level для отслеживания уровня вложенности
|
const MenuItem = ({ item, onSelectItem }) => {
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||||
|
|
||||||
|
|
@ -30,67 +30,20 @@ const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен п
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem component="div" onClick={handleToggle}>
|
||||||
component="div"
|
|
||||||
onClick={hasChildren ? handleToggle : handleOpenTab}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer", // Курсор pointer везде
|
|
||||||
pl: 2 + level * 2, // Сдвиг в зависимости от уровня вложенности
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "#f5f5f5", // Подсветка при наведении на весь элемент
|
|
||||||
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
{hasChildren ? (
|
<div onClick={handleOpenTab} style={{ cursor: "pointer" }}>
|
||||||
<div
|
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
|
||||||
onClick={handleOpenTab}
|
</div>
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
borderRadius: "4px", // Скругление углов
|
|
||||||
padding: "4px", // Отступы для увеличения области hover
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
|
|
||||||
// transform: 2,
|
|
||||||
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isOpen ? <FolderOpen /> : <Folder />}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
onClick={handleOpenTab}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
borderRadius: "4px", // Скругление углов
|
|
||||||
padding: "4px", // Отступы для увеличения области hover
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Здесь можно добавить другую иконку или оставить пустым */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText primary={item.title} />
|
||||||
primary={item.title}
|
|
||||||
sx={{ cursor: "pointer" }} // Курсор pointer для текста
|
|
||||||
/>
|
|
||||||
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{hasChildren && (
|
{hasChildren && (
|
||||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||||
<List component="div" disablePadding>
|
<List component="div" disablePadding>
|
||||||
{item.items.map((child, index) => (
|
{item.items.map((child, index) => (
|
||||||
<MenuItem
|
<MenuItem key={index} item={child} onSelectItem={onSelectItem} />
|
||||||
key={index}
|
|
||||||
item={child}
|
|
||||||
onSelectItem={onSelectItem}
|
|
||||||
level={level + 1} // Увеличиваем уровень вложенности
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
|
||||||
import ReactFlow, { Controls, Background, useNodesState, useEdgesState } from 'reactflow';
|
|
||||||
import 'reactflow/dist/style.css';
|
|
||||||
import { statusManager1, getStatusColor } from './dataUtils';
|
|
||||||
|
|
||||||
const FlowChart = ({ data }) => {
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
||||||
const [collapsedNodes, setCollapsedNodes] = useState(new Set()); // Состояние для свернутых узлов
|
|
||||||
const [nodePositions, setNodePositions] = useState({}); // Состояние для сохранения позиций узлов
|
|
||||||
|
|
||||||
// Обновление статусов
|
|
||||||
useEffect(() => {
|
|
||||||
const updateStatuses = (data) => {
|
|
||||||
statusManager1.updateStatuses(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateStatuses(data); // Обновляем статусы только при изменении данных
|
|
||||||
}, [data]); // Зависимость от data
|
|
||||||
|
|
||||||
// Функция для построения радиального графа
|
|
||||||
const parseData = useCallback(
|
|
||||||
(data) => {
|
|
||||||
const nodes = [];
|
|
||||||
const edges = [];
|
|
||||||
|
|
||||||
const centerX = 500; // Центр графа по X
|
|
||||||
const centerY = 400; // Центр графа по Y
|
|
||||||
const levelRadius = 150; // Расстояние между уровнями
|
|
||||||
|
|
||||||
// Основная рекурсивная функция
|
|
||||||
const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI) => {
|
|
||||||
const nodeId = item.id;
|
|
||||||
|
|
||||||
// Угол узла на круге
|
|
||||||
const angle = (angleStart + angleEnd) / 2; // Угол для текущего узла
|
|
||||||
const nodeX = centerX + Math.cos(angle) * level * levelRadius;
|
|
||||||
const nodeY = centerY + Math.sin(angle) * level * levelRadius;
|
|
||||||
|
|
||||||
// Используем сохраненную позицию, если она есть
|
|
||||||
const savedPosition = nodePositions[nodeId];
|
|
||||||
const position = savedPosition || { x: nodeX, y: nodeY };
|
|
||||||
|
|
||||||
const node = {
|
|
||||||
id: nodeId,
|
|
||||||
position,
|
|
||||||
style: {
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
borderRadius: '50%', // Делаем круги
|
|
||||||
backgroundColor: getStatusColor(item.status),
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'white',
|
|
||||||
border: '2px solid #fff',
|
|
||||||
},
|
|
||||||
data: { label: item.title },
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes.push(node);
|
|
||||||
|
|
||||||
// Создаем ребро к родителю
|
|
||||||
if (parentId) {
|
|
||||||
edges.push({ id: `${parentId}-${nodeId}`, source: parentId, target: nodeId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Дочерние узлы (если узел не свернут)
|
|
||||||
if (item.items && item.items.length > 0 && !collapsedNodes.has(nodeId)) {
|
|
||||||
const angleStep = (angleEnd - angleStart) / item.items.length; // Шаг угла для дочерних узлов
|
|
||||||
item.items.forEach((child, index) => {
|
|
||||||
// Равномерно распределяем дочерние узлы вокруг родителя
|
|
||||||
traverse(
|
|
||||||
child,
|
|
||||||
nodeId,
|
|
||||||
level + 1,
|
|
||||||
angleStart + index * angleStep, // Начальный угол для дочернего узла
|
|
||||||
angleStart + (index + 1) * angleStep // Конечный угол для дочернего узла
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Начинаем с центрального узла
|
|
||||||
const centerNode = {
|
|
||||||
id: data.id,
|
|
||||||
position: { x: centerX, y: centerY },
|
|
||||||
style: {
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: getStatusColor(data.status),
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'white',
|
|
||||||
border: '2px solid #fff',
|
|
||||||
},
|
|
||||||
data: { label: data.title },
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes.push(centerNode);
|
|
||||||
|
|
||||||
// Обрабатываем дочерние узлы центрального узла
|
|
||||||
if (data.items && data.items.length > 0 && !collapsedNodes.has(data.id)) {
|
|
||||||
const angleStep = (2 * Math.PI) / data.items.length; // Равномерно распределяем вокруг центра
|
|
||||||
data.items.forEach((child, index) => {
|
|
||||||
traverse(
|
|
||||||
child,
|
|
||||||
data.id,
|
|
||||||
1, // Уровень 1 (первый круг вокруг центра)
|
|
||||||
index * angleStep, // Начальный угол
|
|
||||||
(index + 1) * angleStep // Конечный угол
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { nodes, edges };
|
|
||||||
},
|
|
||||||
[collapsedNodes, nodePositions] // Зависимость от collapsedNodes и nodePositions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Обработчик клика по узлу
|
|
||||||
const onNodeClick = useCallback(
|
|
||||||
(event, node) => {
|
|
||||||
const nodeId = node.id;
|
|
||||||
const newCollapsedNodes = new Set(collapsedNodes);
|
|
||||||
|
|
||||||
if (newCollapsedNodes.has(nodeId)) {
|
|
||||||
newCollapsedNodes.delete(nodeId); // Разворачиваем узел
|
|
||||||
} else {
|
|
||||||
newCollapsedNodes.add(nodeId); // Сворачиваем узел
|
|
||||||
}
|
|
||||||
|
|
||||||
setCollapsedNodes(newCollapsedNodes); // Обновляем состояние
|
|
||||||
},
|
|
||||||
[collapsedNodes]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Обработчик завершения перетаскивания узла
|
|
||||||
const onNodeDragStop = useCallback(
|
|
||||||
(event, node) => {
|
|
||||||
// Сохраняем новую позицию узла
|
|
||||||
setNodePositions((prevPositions) => ({
|
|
||||||
...prevPositions,
|
|
||||||
[node.id]: node.position,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Обновляем узлы и ребра при изменении данных
|
|
||||||
useEffect(() => {
|
|
||||||
const { nodes: initialNodes, edges: initialEdges } = parseData(data);
|
|
||||||
setNodes(initialNodes);
|
|
||||||
setEdges(initialEdges);
|
|
||||||
}, [data, parseData, setNodes, setEdges]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ height: '100vh', width: '100%' }}>
|
|
||||||
<ReactFlow
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
onNodeClick={onNodeClick} // Добавляем обработчик клика
|
|
||||||
onNodeDragStop={onNodeDragStop} // Добавляем обработчик завершения перетаскивания
|
|
||||||
fitView
|
|
||||||
>
|
|
||||||
<Background />
|
|
||||||
<Controls />
|
|
||||||
</ReactFlow>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(FlowChart); // Оптимизация рендера
|
|
||||||
|
|
@ -1,55 +1,110 @@
|
||||||
import React, { useRef, useEffect, useMemo, useState } from "react";
|
import React, { useRef, useEffect, useMemo } from "react";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import { calculateNodePositions } from "./TreeChartComponents/NodePosition";
|
import "../../Style/TreeChart.css";
|
||||||
import { getStatusColor } from "./dataUtils";
|
import { getStatusColor } from "./dataUtils";
|
||||||
|
|
||||||
const TreeChart = ({ data }) => {
|
const TreeChart = ({ data, onNodeClick }) => {
|
||||||
const chartRef = useRef();
|
const chartRef = useRef();
|
||||||
const nodePositions = useRef(new Map());
|
const nodePositions = useRef(new Map());
|
||||||
const [treeData, setTreeData] = useState(data);
|
|
||||||
|
|
||||||
// Пересчитываем позиции узлов при изменении данных
|
|
||||||
const { root, nodes, links } = useMemo(() => {
|
const { root, nodes, links } = useMemo(() => {
|
||||||
return calculateNodePositions(treeData, nodePositions.current);
|
if (!data || !data.items) return { root: null, nodes: [], links: [] };
|
||||||
}, [treeData]);
|
|
||||||
|
const root = d3.hierarchy(data, (d) => d.items);
|
||||||
|
const maxDepth = d3.max(root.descendants(), (d) => d.depth);
|
||||||
|
|
||||||
|
// Фильтруем узлы, исключая последний уровень
|
||||||
|
const nodes = root.descendants().filter((d) => d.depth < maxDepth);
|
||||||
|
|
||||||
|
// Фильтруем связи
|
||||||
|
const links = nodes.filter((d) => d.parent).map((d) => ({
|
||||||
|
source: d.parent,
|
||||||
|
target: d,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Размещаем узлы иерархически
|
||||||
|
const center = { x: 0, y: 0 }; // Центральная точка
|
||||||
|
const baseRadius = 150; // Базовый радиус для 1-го уровня
|
||||||
|
const branchOffset = 80; // Смещение узлов вдоль ветки
|
||||||
|
const angleOffset = Math.PI / 4; // Угол смещения для дочерних ветвей
|
||||||
|
const spreadFactor = 1.5; // Коэффициент растяжения для последних узлов
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const prev = nodePositions.current.get(node.data.id);
|
||||||
|
if (prev) {
|
||||||
|
node.x = prev.x;
|
||||||
|
node.y = prev.y;
|
||||||
|
} else {
|
||||||
|
if (node.depth === 0) {
|
||||||
|
// Центральный узел
|
||||||
|
node.x = center.x;
|
||||||
|
node.y = center.y;
|
||||||
|
} else if (node.depth === 1) {
|
||||||
|
// Первый уровень - равномерно по окружности
|
||||||
|
const parent = node.parent;
|
||||||
|
const index = parent.children.indexOf(node);
|
||||||
|
const totalSiblings = parent.children.length;
|
||||||
|
|
||||||
|
const radius = baseRadius * node.depth;
|
||||||
|
const sectorAngle = (Math.PI * 2) / totalSiblings;
|
||||||
|
const angle = index * sectorAngle;
|
||||||
|
|
||||||
|
node.x = parent.x + radius * Math.cos(angle);
|
||||||
|
node.y = parent.y + radius * Math.sin(angle);
|
||||||
|
node.angle = angle; // Запоминаем угол для веток
|
||||||
|
} else {
|
||||||
|
// Второй уровень и дальше - ветка растет в направлении родителя
|
||||||
|
const parent = node.parent;
|
||||||
|
const siblings = parent.children || [];
|
||||||
|
const index = siblings.indexOf(node);
|
||||||
|
const totalSiblings = siblings.length;
|
||||||
|
|
||||||
|
const direction = parent.angle || 0;
|
||||||
|
const offsetAngle = ((index - (totalSiblings - 1) / 2) * angleOffset) / totalSiblings;
|
||||||
|
|
||||||
|
let distance = branchOffset;
|
||||||
|
if (!node.children || node.children.length === 0) {
|
||||||
|
// Если это последний узел, увеличиваем расстояние
|
||||||
|
distance *= spreadFactor + node.depth * 0.2; // Чем глубже, тем больше разброс
|
||||||
|
}
|
||||||
|
|
||||||
|
node.x = parent.x + distance * Math.cos(direction + offsetAngle);
|
||||||
|
node.y = parent.y + distance * Math.sin(direction + offsetAngle);
|
||||||
|
node.angle = direction + offsetAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodePositions.current.set(node.data.id, { x: node.x, y: node.y });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return { root, nodes, links };
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartRef.current) return;
|
if (!chartRef.current) return;
|
||||||
|
|
||||||
// Удаляем старый граф перед отрисовкой нового
|
|
||||||
d3.select(chartRef.current).selectAll("*").remove();
|
|
||||||
|
|
||||||
// Определяем границы графа
|
|
||||||
const xMin = d3.min(nodes, (d) => d.x) || -500;
|
|
||||||
const xMax = d3.max(nodes, (d) => d.x) || 500;
|
|
||||||
const yMin = d3.min(nodes, (d) => d.y) || -500;
|
|
||||||
const yMax = d3.max(nodes, (d) => d.y) || 500;
|
|
||||||
|
|
||||||
const width = xMax - xMin + 200;
|
|
||||||
const height = yMax - yMin + 200;
|
|
||||||
|
|
||||||
const svg = d3.select(chartRef.current)
|
const svg = d3.select(chartRef.current)
|
||||||
.attr("width", "100%")
|
.attr("width", 2000)
|
||||||
.attr("height", "100%")
|
.attr("height", 2000)
|
||||||
.attr("viewBox", `${xMin - 100} ${yMin - 100} ${width} ${height}`)
|
.attr("viewBox", [-500, -500, 1500, 1500])
|
||||||
.attr("style", "max-width: 100%; height: auto;")
|
.attr("style", "max-width: 100%; height: auto;");
|
||||||
.call(d3.zoom()
|
|
||||||
.scaleExtent([0.5, 5])
|
|
||||||
.on("zoom", (event) => {
|
|
||||||
svg.select("g").attr("transform", event.transform);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const g = svg.append("g");
|
svg.append("g").attr("class", "links");
|
||||||
|
svg.append("g").attr("class", "nodes");
|
||||||
|
svg.append("g").attr("class", "labels");
|
||||||
|
|
||||||
g.append("g").attr("class", "links");
|
// Очищаем предыдущие элементы
|
||||||
g.append("g").attr("class", "nodes");
|
svg.selectAll(".links line").remove();
|
||||||
g.append("g").attr("class", "labels");
|
svg.selectAll(".nodes circle").remove();
|
||||||
|
svg.selectAll(".labels text").remove();
|
||||||
|
|
||||||
// Рисуем связи
|
// Рисуем связи
|
||||||
g.select(".links")
|
const linkGroup = svg.select(".links");
|
||||||
|
const link = linkGroup
|
||||||
.selectAll("line")
|
.selectAll("line")
|
||||||
.data(links)
|
.data(links, (d) => `${d.source.data.id}-${d.target.data.id}`)
|
||||||
.join("line")
|
.join("line")
|
||||||
.attr("stroke", "#999")
|
.attr("stroke", "#999")
|
||||||
.attr("stroke-opacity", 0.6)
|
.attr("stroke-opacity", 0.6)
|
||||||
|
|
@ -59,33 +114,40 @@ const TreeChart = ({ data }) => {
|
||||||
.attr("y2", (d) => d.target.y);
|
.attr("y2", (d) => d.target.y);
|
||||||
|
|
||||||
// Рисуем узлы
|
// Рисуем узлы
|
||||||
g.select(".nodes")
|
const nodeGroup = svg.select(".nodes");
|
||||||
|
const node = nodeGroup
|
||||||
.selectAll("circle")
|
.selectAll("circle")
|
||||||
.data(nodes)
|
.data(nodes, (d) => d.data.id)
|
||||||
.join("circle")
|
.join("circle")
|
||||||
.attr("fill", (d) => getStatusColor(d.data.status))
|
.attr("fill", (d) => getStatusColor(d.data.status))
|
||||||
.attr("stroke", "#fff")
|
.attr("stroke", "#fff")
|
||||||
.attr("r", 7)
|
.attr("r", 7)
|
||||||
.attr("cx", (d) => d.x)
|
.attr("cx", (d) => d.x)
|
||||||
.attr("cy", (d) => d.y)
|
.attr("cy", (d) => d.y)
|
||||||
.on("click", (event, d) => toggleNode(d))
|
|
||||||
.call(drag());
|
.call(drag());
|
||||||
|
|
||||||
|
node.on("click", (event, d) => {
|
||||||
|
if (onNodeClick) {
|
||||||
|
onNodeClick(d.data.id, d.data.title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Рисуем текстовые метки
|
// Рисуем текстовые метки
|
||||||
g.select(".labels")
|
const labelGroup = svg.select(".labels");
|
||||||
|
const text = labelGroup
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.data(nodes)
|
.data(nodes, (d) => d.data.id)
|
||||||
.join("text")
|
.join("text")
|
||||||
.text((d) => (nodes.length > 50 ? "" : d.data.title))
|
.text((d) => (nodes.length > 50 ? "" : d.data.title)) // Скрываем текст, если узлов много
|
||||||
.attr("dx", 12)
|
.attr("dx", 12)
|
||||||
.attr("dy", 4)
|
.attr("dy", 4)
|
||||||
.style("user-select", "none")
|
.style("user-select", "none") // Запрет выделения текста
|
||||||
.style("pointer-events", "none")
|
.style("pointer-events", "none") // Запрет взаимодействия с текстом
|
||||||
.style("fill", "var(--TreeChart-text-color)")
|
.style("fill", "var(--TreeChart-text-color)") // Используем переменную для цвета текста
|
||||||
.attr("x", (d) => d.x + 12)
|
.attr("x", (d) => d.x + 12)
|
||||||
.attr("y", (d) => d.y + 4);
|
.attr("y", (d) => d.y + 4);
|
||||||
|
|
||||||
}, [root, links, nodes]);
|
}, [root, links, nodes, onNodeClick]);
|
||||||
|
|
||||||
const drag = () => {
|
const drag = () => {
|
||||||
function dragstarted(event, d) {
|
function dragstarted(event, d) {
|
||||||
|
|
@ -97,11 +159,13 @@ const TreeChart = ({ data }) => {
|
||||||
d.y = event.y;
|
d.y = event.y;
|
||||||
d3.select(this).attr("cx", d.x).attr("cy", d.y);
|
d3.select(this).attr("cx", d.x).attr("cy", d.y);
|
||||||
|
|
||||||
|
// Обновляем текстовую метку
|
||||||
d3.select(this.parentNode)
|
d3.select(this.parentNode)
|
||||||
.select("text")
|
.select("text")
|
||||||
.attr("x", d.x + 12)
|
.attr("x", d.x + 12)
|
||||||
.attr("y", d.y + 4);
|
.attr("y", d.y + 4);
|
||||||
|
|
||||||
|
// Обновляем связи
|
||||||
d3.select(chartRef.current)
|
d3.select(chartRef.current)
|
||||||
.selectAll(".links line")
|
.selectAll(".links line")
|
||||||
.filter((link) => link.source === d || link.target === d)
|
.filter((link) => link.source === d || link.target === d)
|
||||||
|
|
@ -119,21 +183,7 @@ const TreeChart = ({ data }) => {
|
||||||
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
|
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleNode = (d) => {
|
|
||||||
d.data.collapsed = !d.data.collapsed;
|
|
||||||
|
|
||||||
if (d.data.collapsed) {
|
|
||||||
d._children = d.data.children;
|
|
||||||
d.data.children = [];
|
|
||||||
} else {
|
|
||||||
d.data.children = d._children;
|
|
||||||
d._children = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTreeData({ ...treeData });
|
|
||||||
};
|
|
||||||
|
|
||||||
return <svg ref={chartRef} />;
|
return <svg ref={chartRef} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TreeChart;
|
export default TreeChart;
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const Label = ({ node }) => {
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
x={node.x + 12}
|
|
||||||
y={node.y + 4}
|
|
||||||
dx={12}
|
|
||||||
dy={4}
|
|
||||||
style={{
|
|
||||||
userSelect: "none",
|
|
||||||
pointerEvents: "none",
|
|
||||||
fill: "var(--TreeChart-text-color)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.data.title}
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Label;
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const Link = ({ link }) => {
|
|
||||||
return (
|
|
||||||
<line
|
|
||||||
x1={link.source.x}
|
|
||||||
y1={link.source.y}
|
|
||||||
x2={link.target.x}
|
|
||||||
y2={link.target.y}
|
|
||||||
stroke="#999"
|
|
||||||
strokeOpacity={0.6}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Link;
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import { getStatusColor } from "../dataUtils";
|
|
||||||
|
|
||||||
const Node = ({ node, onNodeClick, drag }) => {
|
|
||||||
return (
|
|
||||||
<circle
|
|
||||||
cx={node.x}
|
|
||||||
cy={node.y}
|
|
||||||
r={7}
|
|
||||||
fill={getStatusColor(node.data.status)}
|
|
||||||
stroke="#fff"
|
|
||||||
onMouseDown={(event) => drag(event, node)}
|
|
||||||
onClick={() => onNodeClick(node.data.id, node.data.title)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Node;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
export const calculateNodePositions = (data, nodePositions) => {
|
|
||||||
if (!data || !data.items) return { root: null, nodes: [], links: [] };
|
|
||||||
|
|
||||||
const root = d3.hierarchy(data, (d) => d.items);
|
|
||||||
const treeLayout = d3.tree().size([4000 * Math.PI, 300]); // Угловое распределение (радиан, радиус)
|
|
||||||
|
|
||||||
treeLayout(root); // Заполняем координаты
|
|
||||||
|
|
||||||
const nodes = root.descendants();
|
|
||||||
const links = nodes
|
|
||||||
.filter((d) => d.parent)
|
|
||||||
.map((d) => ({
|
|
||||||
source: d.parent,
|
|
||||||
target: d,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Преобразуем полярные координаты в декартовые
|
|
||||||
nodes.forEach((node) => {
|
|
||||||
const angle = node.x; // x теперь угол (радианы)
|
|
||||||
const radius = node.y; // y теперь радиус
|
|
||||||
nodePositions.set(node.data.id, {
|
|
||||||
x: radius * Math.cos(angle),
|
|
||||||
y: radius * Math.sin(angle),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { root, nodes, links };
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -222,13 +222,9 @@ const TreeTable = ({ data }) => {
|
||||||
<tr className="tree-table-row">{renderData(filteredData)}</tr>
|
<tr className="tree-table-row">{renderData(filteredData)}</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button
|
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
|
||||||
onClick={() => setIsLogVisible(!isLogVisible)}
|
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
||||||
className="toggle-log-button"
|
</button>
|
||||||
style={{ marginTop: "10px" }}
|
|
||||||
>
|
|
||||||
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
|
||||||
</button>
|
|
||||||
{isLogVisible && (
|
{isLogVisible && (
|
||||||
<div className="status-log">
|
<div className="status-log">
|
||||||
<h3>Лог статусов</h3>
|
<h3>Лог статусов</h3>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
||||||
import TreeTable from "../UI/TreeTable";
|
import TreeTable from "../UI/TreeTable";
|
||||||
import TreeChart from "../TreeChart/TreeChart";
|
import TreeChart from "../TreeChart/TreeChart";
|
||||||
import FlowChart from "../TreeChart/FlowChart";
|
|
||||||
|
|
||||||
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
||||||
if (activeTab === "Главная") {
|
if (activeTab === "Главная") {
|
||||||
|
|
@ -23,7 +22,7 @@ const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleO
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (activeTab === "Визуализация") {
|
} else if (activeTab === "Визуализация") {
|
||||||
return <FlowChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
|
return <TreeChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
|
||||||
} else {
|
} else {
|
||||||
const tabData = tabContent[activeTab];
|
const tabData = tabContent[activeTab];
|
||||||
return tabData ? tabData.content : <p>Нет данных</p>;
|
return tabData ? tabData.content : <p>Нет данных</p>;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
--modal-background: #2d2d2d;
|
--modal-background: #2d2d2d;
|
||||||
--modal--btn-background: #333333;
|
--modal--btn-background: #333333;
|
||||||
--modal-text: #FFFFFF;
|
--modal-text: #FFFFFF;
|
||||||
--table-border: #c70a0a;
|
--table-border: #444444;
|
||||||
--table-header-background: #2d2d2d;
|
--table-header-background: #2d2d2d;
|
||||||
--table-cell-background: #333333;
|
--table-cell-background: #333333;
|
||||||
--table-text-color: #E0E0E0;
|
--table-text-color: #E0E0E0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue