diff --git a/src/Charts/NegativeStatusChart.jsx b/src/Charts/NegativeStatusChart.jsx index d142ded..0ee7531 100644 --- a/src/Charts/NegativeStatusChart.jsx +++ b/src/Charts/NegativeStatusChart.jsx @@ -1,30 +1,27 @@ import React from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; -const NegativeStatusChart = ({ data }) => { - // Подсчет количества негативных статусов - const processedData = data.map(entry => ({ - time: entry.time, - negativeCount: entry.statuses.filter(status => ['yellow', 'orange', 'red'].includes(status)).length - })); +const SystemStatusChart = ({ data }) => { + // Обрезаем массив, оставляя только последние 20 точек + const trimmedData = data.slice(-20); return ( - + - + ); }; -export default NegativeStatusChart; +export default SystemStatusChart; \ No newline at end of file diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx index 415d68a..d6f7b0b 100644 --- a/src/Components/Layout/Dashboard.jsx +++ b/src/Components/Layout/Dashboard.jsx @@ -1,75 +1,79 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import SidebarMenu from "./SidebarMenu"; import TreeChart from "../TreeChart/TreeChart"; import "../../Style/Dashboard.css"; import SystemStatusChart from "../../Charts/SystemStatusChart"; import Tabs from "../UI/Tabs"; -import menuData from "../TreeChart/menuData.json"; // Импортируем JSON-данные +import menuData from "../TreeChart/menuData.json"; import TreeTable from "../UI/TreeTable"; -import { updateStatuses } from "../TreeChart/dataUtils"; // Функция обновления статусов -import generateTabContent from "../TreeChart/tabContent"; // Импортируем функцию generateTabContent +import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; +import generateTabContent from "../TreeChart/tabContent"; const Dashboard = () => { const [tabs, setTabs] = useState([]); const [activeTab, setActiveTab] = useState("Главная"); - const [tabContent, setTabContent] = useState({}); // Состояние для контента вкладок - const [treeData, setTreeData] = useState(menuData); // Загружаем меню в state - const [sidebarWidth, setSidebarWidth] = useState(250); // Начальная ширина сайдбара - const [isResizing, setIsResizing] = useState(false); // Состояние перетаскивания - const [statusHistory, setStatusHistory] = useState([]); // История статусов для графика - const sidebarRef = useRef(null); // Референс на сайдбар + const [tabContent, setTabContent] = useState({}); + const [treeData1, setTreeData1] = useState(menuData); + const [treeData2, setTreeData2] = useState(menuData); + const [sidebarWidth, setSidebarWidth] = useState(250); + const [isResizing, setIsResizing] = useState(false); + const [statusHistories, setStatusHistories] = useState({ + history1: [], + history2: [], + }); + const sidebarRef = useRef(null); - // Генерация контента для вкладок на основе menuData useEffect(() => { const generatedTabContent = generateTabContent(menuData); setTabContent(generatedTabContent); }, []); - // Обновление treeData каждые 30 секунд useEffect(() => { const interval = setInterval(() => { - setTreeData((prevData) => { - const updatedData = JSON.parse(JSON.stringify(prevData)); // Клонируем данные - const averageStatusValue = updateStatuses(updatedData); // Обновляем статусы и получаем среднее значение + const updatedData1 = JSON.parse(JSON.stringify(treeData1)); + const averageStatusValue1 = statusManager1.updateStatuses(updatedData1); + const statusPercentage1 = Math.max(0, Math.min(100, averageStatusValue1 * 100)); - // Преобразуем среднее значение в проценты (0% - 100%) - const statusPercentage = (1 - (averageStatusValue / 3)) * 100; + const updatedData2 = JSON.parse(JSON.stringify(treeData2)); + const averageStatusValue2 = statusManager2.updateStatuses(updatedData2); + const statusPercentage2 = Math.max(0, Math.min(100, averageStatusValue2 * 100)); - // Добавляем новое состояние в историю - setStatusHistory((prevHistory) => [ - ...prevHistory, - { time: new Date().toLocaleTimeString(), status: statusPercentage } - ]); + setStatusHistories((prevHistories) => ({ + history1: [ + ...prevHistories.history1.slice(-49), + { time: new Date().toLocaleTimeString(), status: statusPercentage1 }, + ], + history2: [ + ...prevHistories.history2.slice(-49), + { time: new Date().toLocaleTimeString(), status: statusPercentage2 }, + ], + })); - return updatedData; - }); + setTreeData1(updatedData1); + setTreeData2(updatedData2); }, 30000); return () => clearInterval(interval); - }, []); + }, [treeData1, treeData2]); - // Обработчик начала перетаскивания - const startResizing = (e) => { + const startResizing = useCallback((e) => { e.preventDefault(); setIsResizing(true); - }; + }, []); - // Обработчик движения мыши - const resize = (e) => { + const resize = useCallback((e) => { if (isResizing) { - const newWidth = e.clientX; // Новая ширина сайдбара - if (newWidth > 100 && newWidth < 400) { // Ограничиваем минимальную и максимальную ширину + const newWidth = e.clientX; + if (newWidth > 100 && newWidth < 400) { setSidebarWidth(newWidth); } } - }; + }, [isResizing]); - // Обработчик окончания перетаскивания - const stopResizing = () => { + const stopResizing = useCallback(() => { setIsResizing(false); - }; + }, []); - // Добавляем обработчики событий useEffect(() => { const handleMouseMove = (e) => resize(e); const handleMouseUp = () => stopResizing(); @@ -83,7 +87,7 @@ const Dashboard = () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; - }, [isResizing]); + }, [isResizing, resize, stopResizing]); const handleOpenTab = (id, title) => { if (!tabs.some((tab) => tab.id === id)) { @@ -105,14 +109,16 @@ const Dashboard = () => { return (

Общий мониторинг состояния системы

- - {/* График состояния системы */} - - {/* Используем актуальные данные */} + + + + + +
); } else if (activeTab === "Визуализация") { - return handleOpenTab(id, title)} />; + return handleOpenTab(id, title)} />; } else { const tabData = tabContent[activeTab]; return tabData ? tabData.content :

Нет данных

; @@ -124,10 +130,9 @@ const Dashboard = () => {
- - {/* Элемент для перетаскивания */} +
{ @@ -11,8 +12,16 @@ const TreeChart = ({ data, onNodeClick }) => { if (!data || !data.items) return { root: null, nodes: [], links: [] }; const root = d3.hierarchy(data, (d) => d.items); - const nodes = root.descendants(); - const links = root.links(); + 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, + })); // Применяем сохраненные позиции к узлам nodes.forEach((node) => { @@ -20,10 +29,9 @@ const TreeChart = ({ data, onNodeClick }) => { if (prev) { node.x = prev.x; node.y = prev.y; - node.fx = prev.fx ?? null; // Если фиксированные координаты были, сохраняем + node.fx = prev.fx ?? null; node.fy = prev.fy ?? null; } else { - // Если узел новый, задаем ему позицию рядом с родителем const parent = node.parent; node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000; node.y = parent ? parent.y + Math.random() * 50 - 25 : Math.random() * 1000; @@ -39,7 +47,7 @@ const TreeChart = ({ data, onNodeClick }) => { const svg = d3.select(chartRef.current) .attr("width", 2000) - .attr("height", 1000) + .attr("height", 2000) .attr("viewBox", [-500, -500, 1000, 1000]) .attr("style", "max-width: 100%; height: auto;"); @@ -53,25 +61,27 @@ const TreeChart = ({ data, onNodeClick }) => { .force("charge", d3.forceManyBody().strength(-200)) .force("center", d3.forceCenter(0, 0)) .force("collision", d3.forceCollide().radius(20)) - .force("x", d3.forceX(0).strength(0.05)) // Ограничиваем разлет по X - .force("y", d3.forceY(0).strength(0.05)) // Ограничиваем разлет по Y - .force("radial", d3.forceRadial(200, 0, 0).strength(0.02)) // Держим узлы ближе к центру - .alphaDecay(0.02) // Замедляем затухание + .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(() => { - simulationRef.current.stop(); // Останавливаем симуляцию - nodes.forEach((node) => { - node.fx = node.x; // Фиксируем текущие позиции узлов - node.fy = node.y; - }); + if (simulationRef.current) { + simulationRef.current.stop(); // Останавливаем симуляцию + nodes.forEach((node) => { + node.fx = node.x; // Фиксируем текущие позиции узлов + node.fy = node.y; + }); + } }, 15000); // 15 секунд }, []); useEffect(() => { - if (!root || !chartRef.current) return; + if (!root || !chartRef.current || !simulationRef.current) return; // Проверяем, что симуляция инициализирована const svg = d3.select(chartRef.current); const linkGroup = svg.select(".links"); @@ -107,9 +117,11 @@ const TreeChart = ({ data, onNodeClick }) => { .selectAll("text") .data(nodes, (d) => d.data.id) .join("text") - .text((d) => d.data.title) + .text((d) => (nodes.length > 50 ? "" : d.data.title)) // Скрываем текст, если узлов много .attr("dx", 12) - .attr("dy", 4); + .attr("dy", 4) + .style("user-select", "none") // Запрет выделения текста + .style("pointer-events", "none"); // Запрет взаимодействия с текстом // Обновляем симуляцию simulationRef.current.nodes(nodes); @@ -136,7 +148,7 @@ const TreeChart = ({ data, onNodeClick }) => { const drag = () => { function dragstarted(event, d) { - if (!event.active) simulationRef.current.alphaTarget(0.3).restart(); + if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } @@ -147,7 +159,7 @@ const TreeChart = ({ data, onNodeClick }) => { } function dragended(event, d) { - if (!event.active) simulationRef.current.alphaTarget(0); + if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0); nodePositions.current.set(d.data.id, { x: d.x, y: d.y, fx: d.fx, fy: d.fy }); } @@ -157,4 +169,4 @@ const TreeChart = ({ data, onNodeClick }) => { return ; }; -export default TreeChart; \ No newline at end of file +export default TreeChart; diff --git a/src/Components/TreeChart/dataUtils.jsx b/src/Components/TreeChart/dataUtils.jsx index 7ee90e0..1a1c70d 100644 --- a/src/Components/TreeChart/dataUtils.jsx +++ b/src/Components/TreeChart/dataUtils.jsx @@ -1,60 +1,85 @@ -// Функция для генерации случайных статусов -const getRandomStatus = () => { - const statuses = [ - ...Array(90).fill("green"), // 90% chance - ...Array(6).fill("yellow"), // 6% chance - ...Array(3).fill("orange"), // 3% chance - ...Array(1).fill("red"), // 1% chance - ]; - return statuses[Math.floor(Math.random() * statuses.length)]; -}; +const StatusManager = () => { + const getRandomStatus = () => { + const statuses = [ + ...Array(90).fill("green"), // 90% шанс + ...Array(6).fill("yellow"), // 6% шанс + ...Array(3).fill("orange"), // 3% шанс + ...Array(1).fill("red"), // 1% шанс + ]; + return statuses[Math.floor(Math.random() * statuses.length)]; + }; -// Функция для получения числового значения статуса -const getStatusValue = (status) => { - switch (status) { - case "green": - return 0; - case "yellow": + const getStatusWeight = (status) => { + switch (status) { + case "green": return 1; // 100% здоровья + case "yellow": return 0.75; + case "orange": return 0.5; + case "red": return 0.25; // 25% здоровья + default: return 1; // По умолчанию "green" + } + }; + + const updateStatuses = (data) => { + if (!data.items || data.items.length === 0) { + // Если это элемент нижнего уровня, генерируем случайный статус + data.status = getRandomStatus(); + return getStatusWeight(data.status); + } + + // Рекурсивно обновляем статусы для всех дочерних элементов + let childStatusWeights = data.items.map((child) => updateStatuses(child)); + + // Проверяем, есть ли дочерние элементы (избегаем деления на 0) + if (childStatusWeights.length === 0) { + data.status = "green"; return 1; - case "orange": - return 2; - case "red": - return 3; - default: - return 0; // По умолчанию green - } + } + + // Вычисляем среднее арифметическое значение весов статусов + const averageStatusWeight = + childStatusWeights.reduce((sum, weight) => sum + weight, 0) / childStatusWeights.length; + + // Определяем статус текущего элемента + data.status = getStatusFromWeight(averageStatusWeight); + + return Math.max(0, averageStatusWeight); // Гарантия, что не будет отрицательных значений + }; + + const getStatusFromWeight = (weight) => { + if (weight >= 0.875) return "green"; + if (weight >= 0.625) return "yellow"; + if (weight >= 0.375) return "orange"; + return "red"; + }; + + const getStatusColor = (status) => { + switch (status) { + case "green": return "#4CAF50"; // Зеленый + case "yellow": return "#cebd21"; // Желтый + case "orange": return "#FF9800"; // Оранжевый + case "red": return "#F44336"; // Красный + default: return "#4CAF50"; // По умолчанию зеленый + } + }; + + return { + getRandomStatus, + updateStatuses, + getStatusColor, + }; }; -// Функция для получения статуса по числовому значению -const getStatusFromValue = (value) => { - if (value >= 3) return "red"; - if (value >= 2) return "orange"; - if (value >= 1) return "yellow"; - return "green"; +// Создаем два независимых менеджера статусов +export const statusManager1 = StatusManager(); +export const statusManager2 = StatusManager(); + +// Функция для расчета процентов здоровья системы +export const calculateStatusPercentage = (averageStatusValue) => { + return Math.max(0, Math.min(100, averageStatusValue * 100)); }; -// Функция для обновления статусов в дереве -const updateStatuses = (data) => { - if (!data.items || data.items.length === 0) { - // Если это элемент нижнего уровня, генерируем случайный статус - data.status = getRandomStatus(); - return getStatusValue(data.status); - } - - // Рекурсивно обновляем статусы для всех дочерних элементов - let childStatusValues = data.items.map((child) => updateStatuses(child)); - - // Вычисляем среднее арифметическое значение статусов - const averageStatusValue = childStatusValues.reduce((sum, value) => sum + value, 0) / childStatusValues.length; - - // Определяем статус текущего элемента на основе среднего значения - data.status = getStatusFromValue(averageStatusValue); - - return getStatusValue(data.status); -}; - -// Функция для получения цвета по статусу -const getStatusColor = (status) => { +// Экспортируем getStatusColor отдельно +export const getStatusColor = (status) => { switch (status) { case "green": return "#4CAF50"; // Зеленый @@ -68,5 +93,3 @@ const getStatusColor = (status) => { return "#4CAF50"; // По умолчанию зеленый } }; - -export { getRandomStatus, updateStatuses, getStatusColor }; \ No newline at end of file diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx index 9c55909..58e6e49 100644 --- a/src/Components/TreeChart/tabContent.jsx +++ b/src/Components/TreeChart/tabContent.jsx @@ -26,64 +26,38 @@ const getAllChildIds = (node) => { const tabContent = (data) => { const tabContent = {}; - + // Функция для рекурсивного обхода и сбора данных const generateContent = (nodes) => { nodes.forEach((node) => { - - // Если у узла есть вложенные элементы, рекурсивно обрабатываем их if (node.items && node.items.length > 0) { + // Создаем контент для родителя + const childrenContent = generateContent(node.items); - generateContent(node.items); - } - - // Если у узла есть id, добавляем его в tabContent - if (node.id) { - - - let content = ( + const content = (

{node.title}

Контент для {node.title}.

+ {childrenContent}
); - // Если у узла есть потомки, добавляем графики для всех потомков - if (node.items && node.items.length > 0) { - const childIds = getAllChildIds(node); // Получаем все id потомков - const charts = childIds.map((id) => { - const metricName = getMetricName(id); - return ( -
-

{node.title} - {id}

- Загрузка графика...
}> - - -
- ); - }); - - content = ( -
-

{node.title}

-

Контент для {node.title}.

- {charts} -
- ); - } else { - // Если у узла нет потомков, добавляем график для него - - const metricName = getMetricName(node.id); - content = ( -
-

{node.title}

-

Контент для {node.title}.

- Загрузка графика...
}> - - -
- ); - } + // Сохраняем контент для текущего id + tabContent[node.id] = { + title: node.title, + content: content, + }; + } else { + // Если у узла нет вложенных элементов, это самый нижний уровень + const metricName = getMetricName(node.id); + const content = ( +
+

{node.title}

{/* Используем title узла */} + Загрузка графика...
}> + + + + ); // Сохраняем контент для текущего id tabContent[node.id] = { @@ -92,6 +66,15 @@ const tabContent = (data) => { }; } }); + + // Возвращаем контент для всех потомков + return ( +
+ {nodes.map((node) => ( +
{tabContent[node.id].content}
+ ))} +
+ ); }; // Начинаем обработку с корневого уровня @@ -104,4 +87,4 @@ const tabContent = (data) => { return tabContent; }; -export default tabContent; // Экспортируем только функцию \ No newline at end of file +export default tabContent; \ No newline at end of file diff --git a/src/Components/UI/TreeTable.jsx b/src/Components/UI/TreeTable.jsx index 235ac1d..51287aa 100644 --- a/src/Components/UI/TreeTable.jsx +++ b/src/Components/UI/TreeTable.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import "../../Style/TreeTable.css"; -import { getStatusColor } from "../TreeChart/dataUtils"; +import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; const TreeTable = ({ data }) => { const tableRef = useRef(null); @@ -61,7 +61,8 @@ const TreeTable = ({ data }) => { return (
-
+
+
{item.title}
@@ -82,7 +83,8 @@ const TreeTable = ({ data }) => { {item.items.map((child) => (
-
+
+
{child.title}
@@ -93,7 +95,8 @@ const TreeTable = ({ data }) => { return (
-
+
+
{item.title}
@@ -115,7 +118,8 @@ const TreeTable = ({ data }) => { title={data.title} >
-
+
+
{data.title}
@@ -132,7 +136,7 @@ const TreeTable = ({ data }) => {

Лог статусов

    {log.map((entry, index) => ( -
  • +
  • [{entry.time}] {entry.status}: {entry.title}
  • ))} @@ -143,4 +147,4 @@ const TreeTable = ({ data }) => { ); }; -export default TreeTable; +export default TreeTable; \ No newline at end of file diff --git a/src/Style/TreeChart.css b/src/Style/TreeChart.css new file mode 100644 index 0000000..5a86d0a --- /dev/null +++ b/src/Style/TreeChart.css @@ -0,0 +1,9 @@ +svg { + user-select: none; + /* Запрет выделения текста */ +} + +text { + pointer-events: none; + /* Запрет взаимодействия с текстом */ +} \ No newline at end of file