Привел граф в рвбочее состояние, улучшил интерфейс

pull/19/head
DmitriyA 2025-03-12 17:29:36 -04:00
parent f525d216e3
commit 306ac00efe
8 changed files with 189 additions and 160 deletions

View File

@ -1,13 +1,16 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/system_monitor_icon.svg" /> <link rel="icon" type="image/svg+xml" href="/system_monitor_icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Модуль доверия</title> <title>Модуль устойчивого функционирования</title>
</head> </head>
<body>
<body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>
</html> </html>

View File

@ -109,7 +109,7 @@ const Dashboard = () => {
return ( return (
<div> <div>
<h2>Общий мониторинг состояния системы</h2> <h2>Общий мониторинг состояния системы</h2>
<label>Процент доверия системы</label> <label>Надежность системы</label>
<SystemStatusChart data={statusHistories.history1} /> <SystemStatusChart data={statusHistories.history1} />
<label>Функциональность системы</label> <label>Функциональность системы</label>
<SystemStatusChart data={statusHistories.history2} /> <SystemStatusChart data={statusHistories.history2} />

View File

@ -7,25 +7,23 @@ const MenuItem = ({ item, onSelectItem, sidebarWidth }) => {
const hasChildren = Array.isArray(item.items) && item.items.length > 0; const hasChildren = Array.isArray(item.items) && item.items.length > 0;
const statusColor = getStatusColor(item.status); const statusColor = getStatusColor(item.status);
// Обработчик одинарного клика (разворачивание/сворачивание или открытие элемента)
const handleSingleClick = () => { const handleSingleClick = () => {
if (hasChildren) { if (hasChildren) {
setIsOpen(!isOpen); // Разворачиваем/сворачиваем дочерние элементы setIsOpen(!isOpen);
} else { } else {
onSelectItem(item); // Если нет потомков, открываем элемент как вкладку onSelectItem(item);
} }
}; };
// Обработчик клика для открытия родителя
const handleOpenParent = (e) => { const handleOpenParent = (e) => {
e.stopPropagation(); // Останавливаем всплытие события, чтобы не сработал handleSingleClick e.stopPropagation();
onSelectItem(item); // Открываем родителя onSelectItem(item);
}; };
return ( return (
<div className="menu-item" style={{ width: sidebarWidth - 20 }}> {/* Динамическая ширина */} <div className={`menu-item ${hasChildren ? "has-children" : ""}`} style={{ width: sidebarWidth - 20 }}>
<div <div
onClick={handleSingleClick} // Одинарный клик для разворачивания/сворачивания или открытия onClick={handleSingleClick}
className="menu-item-header" className="menu-item-header"
> >
{/* Круглый индикатор статуса */} {/* Круглый индикатор статуса */}
@ -33,10 +31,13 @@ const MenuItem = ({ item, onSelectItem, sidebarWidth }) => {
className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`} className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`}
style={{ backgroundColor: statusColor }} style={{ backgroundColor: statusColor }}
/> />
{/* Текст элемента меню */}
<span>{item.title}</span> <span>{item.title}</span>
{/* Иконка для открытия родителя */} {/* Иконки */}
{hasChildren && ( {hasChildren && (
<div style={{ display: "flex", alignItems: "center" }}>
{/* Иконка для открытия родителя */}
<span <span
onClick={handleOpenParent} onClick={handleOpenParent}
className="open-parent-icon" className="open-parent-icon"
@ -44,10 +45,12 @@ const MenuItem = ({ item, onSelectItem, sidebarWidth }) => {
> >
📂 📂
</span> </span>
)}
{/* Иконка для разворачивания/сворачивания */} {/* Иконка для разворачивания/сворачивания */}
{hasChildren && <span>{isOpen ? "▲" : "▼"}</span>} <span className="toggle-icon">
{isOpen ? "▲" : "▼"}
</span>
</div>
)}
</div> </div>
{isOpen && hasChildren && ( {isOpen && hasChildren && (
<div className="submenu"> <div className="submenu">

View File

@ -5,7 +5,6 @@ import { getStatusColor } from "./dataUtils";
const TreeChart = ({ data, onNodeClick }) => { const TreeChart = ({ data, onNodeClick }) => {
const chartRef = useRef(); const chartRef = useRef();
const simulationRef = useRef(null);
const nodePositions = useRef(new Map()); const nodePositions = useRef(new Map());
const { root, nodes, links } = useMemo(() => { const { root, nodes, links } = useMemo(() => {
@ -23,22 +22,63 @@ const TreeChart = ({ data, onNodeClick }) => {
target: d, 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) => { nodes.forEach((node) => {
const prev = nodePositions.current.get(node.data.id); const prev = nodePositions.current.get(node.data.id);
if (prev) { if (prev) {
node.x = prev.x; node.x = prev.x;
node.y = prev.y; node.y = prev.y;
node.fx = prev.fx ?? null;
node.fy = prev.fy ?? null;
} else { } else {
if (node.depth === 0) {
// Центральный узел
node.x = center.x;
node.y = center.y;
} else if (node.depth === 1) {
// Первый уровень - равномерно по окружности
const parent = node.parent; const parent = node.parent;
node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000; const index = parent.children.indexOf(node);
node.y = parent ? parent.y + Math.random() * 50 - 25 : Math.random() * 1000; 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; // Чем глубже, тем больше разброс
} }
nodePositions.current.set(node.data.id, { x: node.x, y: node.y, fx: node.fx, fy: node.fy });
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 }; return { root, nodes, links };
}, [data]); }, [data]);
@ -55,48 +95,26 @@ const TreeChart = ({ data, onNodeClick }) => {
svg.append("g").attr("class", "nodes"); svg.append("g").attr("class", "nodes");
svg.append("g").attr("class", "labels"); svg.append("g").attr("class", "labels");
// Инициализация симуляции // Очищаем предыдущие элементы
simulationRef.current = d3.forceSimulation() svg.selectAll(".links line").remove();
.force("link", d3.forceLink().id((d) => d.data.id).distance(80).strength(1)) svg.selectAll(".nodes circle").remove();
.force("charge", d3.forceManyBody().strength(-200)) svg.selectAll(".labels text").remove();
.force("center", d3.forceCenter(0, 0))
.force("collision", d3.forceCollide().radius(20))
.force("x", d3.forceX(0).strength(0.05))
.force("y", d3.forceY(0).strength(0.05))
.force("radial", d3.forceRadial(200, 0, 0).strength(0.02))
.alphaDecay(0.02)
.alphaTarget(0.1);
// Запускаем симуляцию на 15 секунд, затем отключаем // Рисуем связи
setTimeout(() => {
if (simulationRef.current) {
simulationRef.current.stop(); // Останавливаем симуляцию
nodes.forEach((node) => {
node.fx = node.x; // Фиксируем текущие позиции узлов
node.fy = node.y;
});
}
}, 15000); // 15 секунд
}, []);
useEffect(() => {
if (!root || !chartRef.current || !simulationRef.current) return; // Проверяем, что симуляция инициализирована
const svg = d3.select(chartRef.current);
const linkGroup = svg.select(".links"); const linkGroup = svg.select(".links");
const nodeGroup = svg.select(".nodes");
const labelGroup = svg.select(".labels");
// Обновляем связи
const link = linkGroup const link = linkGroup
.selectAll("line") .selectAll("line")
.data(links, (d) => `${d.source.data.id}-${d.target.data.id}`) .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)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
// Обновляем узлы // Рисуем узлы
const nodeGroup = svg.select(".nodes");
const node = nodeGroup const node = nodeGroup
.selectAll("circle") .selectAll("circle")
.data(nodes, (d) => d.data.id) .data(nodes, (d) => d.data.id)
@ -104,6 +122,8 @@ const TreeChart = ({ data, onNodeClick }) => {
.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("cy", (d) => d.y)
.call(drag()); .call(drag());
node.on("click", (event, d) => { node.on("click", (event, d) => {
@ -112,7 +132,8 @@ const TreeChart = ({ data, onNodeClick }) => {
} }
}); });
// Обновляем текстовые метки // Рисуем текстовые метки
const labelGroup = svg.select(".labels");
const text = labelGroup const text = labelGroup
.selectAll("text") .selectAll("text")
.data(nodes, (d) => d.data.id) .data(nodes, (d) => d.data.id)
@ -121,46 +142,42 @@ const TreeChart = ({ data, onNodeClick }) => {
.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)") // Используем переменную для цвета текста
// Обновляем симуляцию
simulationRef.current.nodes(nodes);
simulationRef.current.force("link").links(links);
simulationRef.current.alphaTarget(0.1).restart();
simulationRef.current.on("tick", () => {
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
node
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y);
text
.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, onNodeClick]); }, [root, links, nodes, onNodeClick]);
const drag = () => { const drag = () => {
function dragstarted(event, d) { function dragstarted(event, d) {
if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0.3).restart(); d3.select(this).raise().attr("stroke", "#000");
d.fx = d.x;
d.fy = d.y;
} }
function dragged(event, d) { function dragged(event, d) {
d.fx = event.x; d.x = event.x;
d.fy = event.y; 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)
.attr("x1", (link) => link.source.x)
.attr("y1", (link) => link.source.y)
.attr("x2", (link) => link.target.x)
.attr("y2", (link) => link.target.y);
} }
function dragended(event, d) { function dragended(event, d) {
if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0); d3.select(this).attr("stroke", "#fff");
nodePositions.current.set(d.data.id, { x: d.x, y: d.y, fx: d.fx, fy: d.fy }); nodePositions.current.set(d.data.id, { x: d.x, y: d.y });
} }
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);

View File

@ -36,14 +36,6 @@
"id": "4", "id": "4",
"title": "OS Linux (module$4) АО", "title": "OS Linux (module$4) АО",
"items": [ "items": [
{
"id": "188",
"title": "Наименование"
},
{
"id": "189",
"title": "Время работы"
},
{ {
"id": "190", "id": "190",
"title": "Загрузка процессора за 1 минуту" "title": "Загрузка процессора за 1 минуту"
@ -153,21 +145,13 @@
] ]
}, },
{ {
"id": "261", "id": "280",
"title": "Сетевой адаптер №1 (port$261) Eth_1", "title": "Сетевой адаптер №1 (port$261) Eth_1",
"items": [ "items": [
{
"id": "206",
"title": "Наименование порта Eth_1"
},
{ {
"id": "207", "id": "207",
"title": "Скорость порта Eth_1" "title": "Скорость порта Eth_1"
}, },
{
"id": "208",
"title": "Физический адрес порта Eth_1"
},
{ {
"id": "209", "id": "209",
"title": "Административное состояние порта Eth_1" "title": "Административное состояние порта Eth_1"
@ -227,21 +211,13 @@
] ]
}, },
{ {
"id": "262", "id": "281",
"title": "Сетевой адаптер №2 (port$262) Eth_2", "title": "Сетевой адаптер №2 (port$262) Eth_2",
"items": [ "items": [
{
"id": "223",
"title": "Наименование порта Eth_2"
},
{ {
"id": "224", "id": "224",
"title": "Скорость порта Eth_2" "title": "Скорость порта Eth_2"
}, },
{
"id": "225",
"title": "Физический адрес порта Eth_2"
},
{ {
"id": "226", "id": "226",
"title": "Административное состояние порта Eth_2" "title": "Административное состояние порта Eth_2"
@ -301,21 +277,13 @@
] ]
}, },
{ {
"id": "263", "id": "282",
"title": "Сетевой адаптер №3 (port$263) Eth_3", "title": "Сетевой адаптер №3 (port$263) Eth_3",
"items": [ "items": [
{
"id": "240",
"title": "Наименование порта Eth_3"
},
{ {
"id": "241", "id": "241",
"title": "Скорость порта Eth_3" "title": "Скорость порта Eth_3"
}, },
{
"id": "242",
"title": "Физический адрес порта Eth_3"
},
{ {
"id": "243", "id": "243",
"title": "Административное состояние порта Eth_3" "title": "Административное состояние порта Eth_3"
@ -375,21 +343,13 @@
] ]
}, },
{ {
"id": "264", "id": "283",
"title": "Сетевой адаптер №4 (port$264) Eth_4", "title": "Сетевой адаптер №4 (port$264) Eth_4",
"items": [ "items": [
{
"id": "257",
"title": "Наименование порта Eth_4"
},
{ {
"id": "258", "id": "258",
"title": "Скорость порта Eth_4" "title": "Скорость порта Eth_4"
}, },
{
"id": "259",
"title": "Физический адрес порта Eth_4"
},
{ {
"id": "260", "id": "260",
"title": "Административное состояние порта Eth_4" "title": "Административное состояние порта Eth_4"

View File

@ -3,7 +3,6 @@
height: 100vh; height: 100vh;
background-color: var(--sidebar-color); background-color: var(--sidebar-color);
color: var(--sidebar-text-color); color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
@ -20,6 +19,8 @@
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px;
/* Отступ справа для скроллбара */
} }
/* Заголовок меню */ /* Заголовок меню */
@ -28,7 +29,6 @@
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
color: var(--sidebar-text-color); color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
padding: 10px; padding: 10px;
} }
@ -36,7 +36,6 @@
.menu-item { .menu-item {
margin-bottom: 10px; margin-bottom: 10px;
color: var(--sidebar-text-color); color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
width: 100%; width: 100%;
} }
@ -57,13 +56,42 @@
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
} }
/* Стили для заголовка элемента меню */
.menu-item-header { .menu-item-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
/* Распределяем пространство между элементами */
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
width: 100%;
/* Занимаем всю доступную ширину */
box-sizing: border-box;
/* Учитываем padding в ширине */
}
/* Стили для текста элемента меню */
.menu-item-header span {
flex: 1;
/* Текст занимает все доступное пространство */
margin-right: 14px;
/* Отступ справа для текста */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие, если текст не помещается */
}
/* Стили для иконок */
.menu-item-header .open-parent-icon,
.menu-item-header .toggle-icon {
flex-shrink: 0;
/* Запрещаем сжатие иконок */
margin-left: 1px;
/* Отступ между иконками */
cursor: pointer;
} }
.menu-item-header:hover { .menu-item-header:hover {
@ -82,9 +110,18 @@
/* Подменю */ /* Подменю */
.submenu { .submenu {
margin-left: 20px; margin-left: 20px;
/* Отступ слева для вложенных элементов */
margin-top: 10px; margin-top: 10px;
} }
/* Стили для элементов нижнего уровня вложенности */
/* Дополнительные отступы для элементов без иконок */
.menu-item:not(.has-children) .menu-item-header {
padding-right: 25px;
/* Добавляем отступ справа для элементов без иконок */
}
/* Футер сайдбара */ /* Футер сайдбара */
.sidebar-footer { .sidebar-footer {
padding: 10px; padding: 10px;
@ -98,7 +135,6 @@
.help, .help,
.settings { .settings {
color: var(--sidebar-text-color); color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
margin: 5px 0; margin: 5px 0;
overflow-x: hidden; overflow-x: hidden;
text-align: left; text-align: left;

View File

@ -15,5 +15,7 @@
--table-cell-background: #333333; --table-cell-background: #333333;
--table-text-color: #E0E0E0; --table-text-color: #E0E0E0;
/* Светлый текст в таблице */ /* Светлый текст в таблице */
--TreeChart-text-color: #ffffff;
--scrollbar-track-color: #333;
} }
} }

View File

@ -75,24 +75,32 @@ button:focus-visible {
/* Глобальный стиль для WebKit-браузеров (Chrome, Edge, Safari) */ /* Глобальный стиль для WebKit-браузеров (Chrome, Edge, Safari) */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; /* Толщина вертикального скролла */ width: 10px;
height: 10px; /* Толщина горизонтального скролла */ /* Толщина вертикального скролла */
height: 10px;
/* Толщина горизонтального скролла */
} }
/* Фон скроллбара */ /* Фон скроллбара */
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #f1f1f1; /* Цвет фона */ background: var(--scrollbar-track-color, #f1f1f1);
border-radius: 10px; /* Скругление углов */ /* Цвет фона */
border-radius: 10px;
/* Скругление углов */
} }
/* Ползунок */ /* Ползунок */
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #3d74c7; /* Основной цвет */ background: #3d74c7;
border-radius: 10px; /* Скругляем края */ /* Основной цвет */
border: 1px solid #1c36c9; /* Белая обводка */ border-radius: 10px;
/* Скругляем края */
border: 1px solid #1c36c9;
/* Белая обводка */
} }
/* Эффект при наведении */ /* Эффект при наведении */
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #2b5aa5; /* Чуть темнее при наведении */ background: #2b5aa5;
/* Чуть темнее при наведении */
} }