Compare commits
4 Commits
67f776efe3
...
3fc7ee0ac3
| Author | SHA1 | Date |
|---|---|---|
|
|
3fc7ee0ac3 | |
|
|
13101ac57c | |
|
|
175b4f993d | |
|
|
5ec58ab476 |
|
|
@ -23,7 +23,8 @@
|
|||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/material": "^6.4.7",
|
||||
"@mui/icons-material": "^6.4.8"
|
||||
"@mui/icons-material": "^6.4.8",
|
||||
"reactflow": "^11.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ const PrometheusChart = ({ metricName }) => {
|
|||
else if (range <= 86400) step = 120;
|
||||
else step = 300;
|
||||
|
||||
const response = await axios.get('https://192.168.2.43:3000/metrics', {
|
||||
const response = await axios.get('http://192.168.2.39:3000/metrics', {
|
||||
params: { metric: metricName, start, end, step },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
|||
}}
|
||||
>
|
||||
<List>
|
||||
<h2 style={{ padding: "16px", fontWeight: "bold" }}>Меню</h2>
|
||||
<h2 style={{ padding: "16px", fontWeight: "bold", }}>Меню</h2>
|
||||
<MenuItem item={data} onSelectItem={handleSelectItem} />
|
||||
</List>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Drawer, List, ListItem, ListItemIcon, ListItemText, Collapse } from "@mui/material";
|
||||
import { ListItem, ListItemIcon, ListItemText, Collapse, List } from "@mui/material";
|
||||
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
||||
|
||||
// Функция для сбора всех потомков
|
||||
|
|
@ -14,7 +14,7 @@ const getAllChildren = (node) => {
|
|||
return children;
|
||||
};
|
||||
|
||||
const MenuItem = ({ item, onSelectItem }) => {
|
||||
const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен параметр level для отслеживания уровня вложенности
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||
|
||||
|
|
@ -30,20 +30,67 @@ const MenuItem = ({ item, onSelectItem }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ListItem component="div" onClick={handleToggle}>
|
||||
<ListItem
|
||||
component="div"
|
||||
onClick={hasChildren ? handleToggle : handleOpenTab}
|
||||
sx={{
|
||||
cursor: "pointer", // Курсор pointer везде
|
||||
pl: 2 + level * 2, // Сдвиг в зависимости от уровня вложенности
|
||||
"&:hover": {
|
||||
backgroundColor: "#f5f5f5", // Подсветка при наведении на весь элемент
|
||||
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<div onClick={handleOpenTab} style={{ cursor: "pointer" }}>
|
||||
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
|
||||
</div>
|
||||
{hasChildren ? (
|
||||
<div
|
||||
onClick={handleOpenTab}
|
||||
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>
|
||||
<ListItemText primary={item.title} />
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
sx={{ cursor: "pointer" }} // Курсор pointer для текста
|
||||
/>
|
||||
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
||||
</ListItem>
|
||||
{hasChildren && (
|
||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
{item.items.map((child, index) => (
|
||||
<MenuItem key={index} item={child} onSelectItem={onSelectItem} />
|
||||
<MenuItem
|
||||
key={index}
|
||||
item={child}
|
||||
onSelectItem={onSelectItem}
|
||||
level={level + 1} // Увеличиваем уровень вложенности
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
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,110 +1,55 @@
|
|||
import React, { useRef, useEffect, useMemo } from "react";
|
||||
import React, { useRef, useEffect, useMemo, useState } from "react";
|
||||
import * as d3 from "d3";
|
||||
import "../../Style/TreeChart.css";
|
||||
import { calculateNodePositions } from "./TreeChartComponents/NodePosition";
|
||||
import { getStatusColor } from "./dataUtils";
|
||||
|
||||
const TreeChart = ({ data, onNodeClick }) => {
|
||||
const TreeChart = ({ data }) => {
|
||||
const chartRef = useRef();
|
||||
const nodePositions = useRef(new Map());
|
||||
const [treeData, setTreeData] = useState(data);
|
||||
|
||||
// Пересчитываем позиции узлов при изменении данных
|
||||
const { root, nodes, links } = useMemo(() => {
|
||||
if (!data || !data.items) return { root: null, nodes: [], links: [] };
|
||||
|
||||
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]);
|
||||
return calculateNodePositions(treeData, nodePositions.current);
|
||||
}, [treeData]);
|
||||
|
||||
useEffect(() => {
|
||||
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)
|
||||
.attr("width", 2000)
|
||||
.attr("height", 2000)
|
||||
.attr("viewBox", [-500, -500, 1500, 1500])
|
||||
.attr("style", "max-width: 100%; height: auto;");
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `${xMin - 100} ${yMin - 100} ${width} ${height}`)
|
||||
.attr("style", "max-width: 100%; height: auto;")
|
||||
.call(d3.zoom()
|
||||
.scaleExtent([0.5, 5])
|
||||
.on("zoom", (event) => {
|
||||
svg.select("g").attr("transform", event.transform);
|
||||
})
|
||||
);
|
||||
|
||||
svg.append("g").attr("class", "links");
|
||||
svg.append("g").attr("class", "nodes");
|
||||
svg.append("g").attr("class", "labels");
|
||||
const g = svg.append("g");
|
||||
|
||||
// Очищаем предыдущие элементы
|
||||
svg.selectAll(".links line").remove();
|
||||
svg.selectAll(".nodes circle").remove();
|
||||
svg.selectAll(".labels text").remove();
|
||||
g.append("g").attr("class", "links");
|
||||
g.append("g").attr("class", "nodes");
|
||||
g.append("g").attr("class", "labels");
|
||||
|
||||
// Рисуем связи
|
||||
const linkGroup = svg.select(".links");
|
||||
const link = linkGroup
|
||||
g.select(".links")
|
||||
.selectAll("line")
|
||||
.data(links, (d) => `${d.source.data.id}-${d.target.data.id}`)
|
||||
.data(links)
|
||||
.join("line")
|
||||
.attr("stroke", "#999")
|
||||
.attr("stroke-opacity", 0.6)
|
||||
|
|
@ -114,40 +59,33 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
.attr("y2", (d) => d.target.y);
|
||||
|
||||
// Рисуем узлы
|
||||
const nodeGroup = svg.select(".nodes");
|
||||
const node = nodeGroup
|
||||
g.select(".nodes")
|
||||
.selectAll("circle")
|
||||
.data(nodes, (d) => d.data.id)
|
||||
.data(nodes)
|
||||
.join("circle")
|
||||
.attr("fill", (d) => getStatusColor(d.data.status))
|
||||
.attr("stroke", "#fff")
|
||||
.attr("r", 7)
|
||||
.attr("cx", (d) => d.x)
|
||||
.attr("cy", (d) => d.y)
|
||||
.on("click", (event, d) => toggleNode(d))
|
||||
.call(drag());
|
||||
|
||||
node.on("click", (event, d) => {
|
||||
if (onNodeClick) {
|
||||
onNodeClick(d.data.id, d.data.title);
|
||||
}
|
||||
});
|
||||
|
||||
// Рисуем текстовые метки
|
||||
const labelGroup = svg.select(".labels");
|
||||
const text = labelGroup
|
||||
g.select(".labels")
|
||||
.selectAll("text")
|
||||
.data(nodes, (d) => d.data.id)
|
||||
.data(nodes)
|
||||
.join("text")
|
||||
.text((d) => (nodes.length > 50 ? "" : d.data.title)) // Скрываем текст, если узлов много
|
||||
.text((d) => (nodes.length > 50 ? "" : d.data.title))
|
||||
.attr("dx", 12)
|
||||
.attr("dy", 4)
|
||||
.style("user-select", "none") // Запрет выделения текста
|
||||
.style("pointer-events", "none") // Запрет взаимодействия с текстом
|
||||
.style("fill", "var(--TreeChart-text-color)") // Используем переменную для цвета текста
|
||||
.style("user-select", "none")
|
||||
.style("pointer-events", "none")
|
||||
.style("fill", "var(--TreeChart-text-color)")
|
||||
.attr("x", (d) => d.x + 12)
|
||||
.attr("y", (d) => d.y + 4);
|
||||
|
||||
}, [root, links, nodes, onNodeClick]);
|
||||
}, [root, links, nodes]);
|
||||
|
||||
const drag = () => {
|
||||
function dragstarted(event, d) {
|
||||
|
|
@ -159,13 +97,11 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
d.y = event.y;
|
||||
d3.select(this).attr("cx", d.x).attr("cy", d.y);
|
||||
|
||||
// Обновляем текстовую метку
|
||||
d3.select(this.parentNode)
|
||||
.select("text")
|
||||
.attr("x", d.x + 12)
|
||||
.attr("y", d.y + 4);
|
||||
|
||||
// Обновляем связи
|
||||
d3.select(chartRef.current)
|
||||
.selectAll(".links line")
|
||||
.filter((link) => link.source === d || link.target === d)
|
||||
|
|
@ -183,7 +119,21 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
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} />;
|
||||
};
|
||||
|
||||
export default TreeChart;
|
||||
export default TreeChart;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
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,9 +222,13 @@ const TreeTable = ({ data }) => {
|
|||
<tr className="tree-table-row">{renderData(filteredData)}</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
|
||||
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsLogVisible(!isLogVisible)}
|
||||
className="toggle-log-button"
|
||||
style={{ marginTop: "10px" }}
|
||||
>
|
||||
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
||||
</button>
|
||||
{isLogVisible && (
|
||||
<div className="status-log">
|
||||
<h3>Лог статусов</h3>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
||||
import TreeTable from "../UI/TreeTable";
|
||||
import TreeChart from "../TreeChart/TreeChart";
|
||||
import FlowChart from "../TreeChart/FlowChart";
|
||||
|
||||
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
||||
if (activeTab === "Главная") {
|
||||
|
|
@ -22,7 +23,7 @@ const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleO
|
|||
</div>
|
||||
);
|
||||
} else if (activeTab === "Визуализация") {
|
||||
return <TreeChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
|
||||
return <FlowChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
|
||||
} else {
|
||||
const tabData = tabContent[activeTab];
|
||||
return tabData ? tabData.content : <p>Нет данных</p>;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
--modal-background: #2d2d2d;
|
||||
--modal--btn-background: #333333;
|
||||
--modal-text: #FFFFFF;
|
||||
--table-border: #444444;
|
||||
--table-border: #c70a0a;
|
||||
--table-header-background: #2d2d2d;
|
||||
--table-cell-background: #333333;
|
||||
--table-text-color: #E0E0E0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue