Добавил второй индикатор, исправил отображение потомков в уровнях вложенности, уменьшил граф
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
parent
88b63959be
commit
a53942b264
|
|
@ -1,30 +1,27 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||||
|
|
||||||
const NegativeStatusChart = ({ data }) => {
|
const SystemStatusChart = ({ data }) => {
|
||||||
// Подсчет количества негативных статусов
|
// Обрезаем массив, оставляя только последние 20 точек
|
||||||
const processedData = data.map(entry => ({
|
const trimmedData = data.slice(-20);
|
||||||
time: entry.time,
|
|
||||||
negativeCount: entry.statuses.filter(status => ['yellow', 'orange', 'red'].includes(status)).length
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<LineChart
|
<LineChart
|
||||||
data={processedData}
|
data={trimmedData} // Используем обрезанный массив
|
||||||
margin={{
|
margin={{
|
||||||
top: 5, right: 30, left: 20, bottom: 5,
|
top: 5, right: 30, left: 20, bottom: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="time" />
|
<XAxis dataKey="time" />
|
||||||
<YAxis domain={[0, 'dataMax']} />
|
<YAxis domain={[0, 100]} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Line type="monotone" dataKey="negativeCount" stroke="#FF5733" activeDot={{ r: 8 }} />
|
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NegativeStatusChart;
|
export default SystemStatusChart;
|
||||||
|
|
@ -1,75 +1,79 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import SidebarMenu from "./SidebarMenu";
|
import SidebarMenu from "./SidebarMenu";
|
||||||
import TreeChart from "../TreeChart/TreeChart";
|
import TreeChart from "../TreeChart/TreeChart";
|
||||||
import "../../Style/Dashboard.css";
|
import "../../Style/Dashboard.css";
|
||||||
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
||||||
import Tabs from "../UI/Tabs";
|
import Tabs from "../UI/Tabs";
|
||||||
import menuData from "../TreeChart/menuData.json"; // Импортируем JSON-данные
|
import menuData from "../TreeChart/menuData.json";
|
||||||
import TreeTable from "../UI/TreeTable";
|
import TreeTable from "../UI/TreeTable";
|
||||||
import { updateStatuses } from "../TreeChart/dataUtils"; // Функция обновления статусов
|
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
||||||
import generateTabContent from "../TreeChart/tabContent"; // Импортируем функцию generateTabContent
|
import generateTabContent from "../TreeChart/tabContent";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [tabs, setTabs] = useState([]);
|
const [tabs, setTabs] = useState([]);
|
||||||
const [activeTab, setActiveTab] = useState("Главная");
|
const [activeTab, setActiveTab] = useState("Главная");
|
||||||
const [tabContent, setTabContent] = useState({}); // Состояние для контента вкладок
|
const [tabContent, setTabContent] = useState({});
|
||||||
const [treeData, setTreeData] = useState(menuData); // Загружаем меню в state
|
const [treeData1, setTreeData1] = useState(menuData);
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(250); // Начальная ширина сайдбара
|
const [treeData2, setTreeData2] = useState(menuData);
|
||||||
const [isResizing, setIsResizing] = useState(false); // Состояние перетаскивания
|
const [sidebarWidth, setSidebarWidth] = useState(250);
|
||||||
const [statusHistory, setStatusHistory] = useState([]); // История статусов для графика
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
const sidebarRef = useRef(null); // Референс на сайдбар
|
const [statusHistories, setStatusHistories] = useState({
|
||||||
|
history1: [],
|
||||||
|
history2: [],
|
||||||
|
});
|
||||||
|
const sidebarRef = useRef(null);
|
||||||
|
|
||||||
// Генерация контента для вкладок на основе menuData
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generatedTabContent = generateTabContent(menuData);
|
const generatedTabContent = generateTabContent(menuData);
|
||||||
setTabContent(generatedTabContent);
|
setTabContent(generatedTabContent);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Обновление treeData каждые 30 секунд
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setTreeData((prevData) => {
|
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
|
||||||
const updatedData = JSON.parse(JSON.stringify(prevData)); // Клонируем данные
|
const averageStatusValue1 = statusManager1.updateStatuses(updatedData1);
|
||||||
const averageStatusValue = updateStatuses(updatedData); // Обновляем статусы и получаем среднее значение
|
const statusPercentage1 = Math.max(0, Math.min(100, averageStatusValue1 * 100));
|
||||||
|
|
||||||
// Преобразуем среднее значение в проценты (0% - 100%)
|
const updatedData2 = JSON.parse(JSON.stringify(treeData2));
|
||||||
const statusPercentage = (1 - (averageStatusValue / 3)) * 100;
|
const averageStatusValue2 = statusManager2.updateStatuses(updatedData2);
|
||||||
|
const statusPercentage2 = Math.max(0, Math.min(100, averageStatusValue2 * 100));
|
||||||
|
|
||||||
// Добавляем новое состояние в историю
|
setStatusHistories((prevHistories) => ({
|
||||||
setStatusHistory((prevHistory) => [
|
history1: [
|
||||||
...prevHistory,
|
...prevHistories.history1.slice(-49),
|
||||||
{ time: new Date().toLocaleTimeString(), status: statusPercentage }
|
{ time: new Date().toLocaleTimeString(), status: statusPercentage1 },
|
||||||
]);
|
],
|
||||||
|
history2: [
|
||||||
|
...prevHistories.history2.slice(-49),
|
||||||
|
{ time: new Date().toLocaleTimeString(), status: statusPercentage2 },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
return updatedData;
|
setTreeData1(updatedData1);
|
||||||
});
|
setTreeData2(updatedData2);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, [treeData1, treeData2]);
|
||||||
|
|
||||||
// Обработчик начала перетаскивания
|
const startResizing = useCallback((e) => {
|
||||||
const startResizing = (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsResizing(true);
|
setIsResizing(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// Обработчик движения мыши
|
const resize = useCallback((e) => {
|
||||||
const resize = (e) => {
|
|
||||||
if (isResizing) {
|
if (isResizing) {
|
||||||
const newWidth = e.clientX; // Новая ширина сайдбара
|
const newWidth = e.clientX;
|
||||||
if (newWidth > 100 && newWidth < 400) { // Ограничиваем минимальную и максимальную ширину
|
if (newWidth > 100 && newWidth < 400) {
|
||||||
setSidebarWidth(newWidth);
|
setSidebarWidth(newWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}, [isResizing]);
|
||||||
|
|
||||||
// Обработчик окончания перетаскивания
|
const stopResizing = useCallback(() => {
|
||||||
const stopResizing = () => {
|
|
||||||
setIsResizing(false);
|
setIsResizing(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// Добавляем обработчики событий
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseMove = (e) => resize(e);
|
const handleMouseMove = (e) => resize(e);
|
||||||
const handleMouseUp = () => stopResizing();
|
const handleMouseUp = () => stopResizing();
|
||||||
|
|
@ -83,7 +87,7 @@ const Dashboard = () => {
|
||||||
window.removeEventListener("mousemove", handleMouseMove);
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
window.removeEventListener("mouseup", handleMouseUp);
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [isResizing]);
|
}, [isResizing, resize, stopResizing]);
|
||||||
|
|
||||||
const handleOpenTab = (id, title) => {
|
const handleOpenTab = (id, title) => {
|
||||||
if (!tabs.some((tab) => tab.id === id)) {
|
if (!tabs.some((tab) => tab.id === id)) {
|
||||||
|
|
@ -105,14 +109,16 @@ const Dashboard = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Общий мониторинг состояния системы</h2>
|
<h2>Общий мониторинг состояния системы</h2>
|
||||||
<label>Процент доверия системы </label>
|
<label>Процент доверия системы</label>
|
||||||
<SystemStatusChart data={statusHistory} /> {/* График состояния системы */}
|
<SystemStatusChart data={statusHistories.history1} />
|
||||||
<label>Статус компонентов системы </label>
|
<label>Функциональность системы</label>
|
||||||
<TreeTable data={treeData} /> {/* Используем актуальные данные */}
|
<SystemStatusChart data={statusHistories.history2} />
|
||||||
|
<label>Статус компонентов системы</label>
|
||||||
|
<TreeTable data={treeData1} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (activeTab === "Визуализация") {
|
} else if (activeTab === "Визуализация") {
|
||||||
return <TreeChart data={treeData} 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>;
|
||||||
|
|
@ -124,10 +130,9 @@ const Dashboard = () => {
|
||||||
<div
|
<div
|
||||||
className="sidebar"
|
className="sidebar"
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
style={{ width: sidebarWidth }} // Динамическая ширина сайдбара
|
style={{ width: sidebarWidth }}
|
||||||
>
|
>
|
||||||
<SidebarMenu data={treeData} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} />
|
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} />
|
||||||
{/* Элемент для перетаскивания */}
|
|
||||||
<div
|
<div
|
||||||
className="sidebar-resizer"
|
className="sidebar-resizer"
|
||||||
onMouseDown={startResizing}
|
onMouseDown={startResizing}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useRef, useEffect, useMemo } from "react";
|
import React, { useRef, useEffect, useMemo } from "react";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
import "../../Style/TreeChart.css";
|
||||||
import { getStatusColor } from "./dataUtils";
|
import { getStatusColor } from "./dataUtils";
|
||||||
|
|
||||||
const TreeChart = ({ data, onNodeClick }) => {
|
const TreeChart = ({ data, onNodeClick }) => {
|
||||||
|
|
@ -11,8 +12,16 @@ const TreeChart = ({ data, onNodeClick }) => {
|
||||||
if (!data || !data.items) return { root: null, nodes: [], links: [] };
|
if (!data || !data.items) return { root: null, nodes: [], links: [] };
|
||||||
|
|
||||||
const root = d3.hierarchy(data, (d) => d.items);
|
const root = d3.hierarchy(data, (d) => d.items);
|
||||||
const nodes = root.descendants();
|
const maxDepth = d3.max(root.descendants(), (d) => d.depth);
|
||||||
const links = root.links();
|
|
||||||
|
// Фильтруем узлы, исключая последний уровень
|
||||||
|
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) => {
|
nodes.forEach((node) => {
|
||||||
|
|
@ -20,10 +29,9 @@ const TreeChart = ({ data, onNodeClick }) => {
|
||||||
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.fx = prev.fx ?? null;
|
||||||
node.fy = prev.fy ?? null;
|
node.fy = prev.fy ?? null;
|
||||||
} else {
|
} else {
|
||||||
// Если узел новый, задаем ему позицию рядом с родителем
|
|
||||||
const parent = node.parent;
|
const parent = node.parent;
|
||||||
node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000;
|
node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000;
|
||||||
node.y = parent ? parent.y + 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)
|
const svg = d3.select(chartRef.current)
|
||||||
.attr("width", 2000)
|
.attr("width", 2000)
|
||||||
.attr("height", 1000)
|
.attr("height", 2000)
|
||||||
.attr("viewBox", [-500, -500, 1000, 1000])
|
.attr("viewBox", [-500, -500, 1000, 1000])
|
||||||
.attr("style", "max-width: 100%; height: auto;");
|
.attr("style", "max-width: 100%; height: auto;");
|
||||||
|
|
||||||
|
|
@ -53,25 +61,27 @@ const TreeChart = ({ data, onNodeClick }) => {
|
||||||
.force("charge", d3.forceManyBody().strength(-200))
|
.force("charge", d3.forceManyBody().strength(-200))
|
||||||
.force("center", d3.forceCenter(0, 0))
|
.force("center", d3.forceCenter(0, 0))
|
||||||
.force("collision", d3.forceCollide().radius(20))
|
.force("collision", d3.forceCollide().radius(20))
|
||||||
.force("x", d3.forceX(0).strength(0.05)) // Ограничиваем разлет по X
|
.force("x", d3.forceX(0).strength(0.05))
|
||||||
.force("y", d3.forceY(0).strength(0.05)) // Ограничиваем разлет по Y
|
.force("y", d3.forceY(0).strength(0.05))
|
||||||
.force("radial", d3.forceRadial(200, 0, 0).strength(0.02)) // Держим узлы ближе к центру
|
.force("radial", d3.forceRadial(200, 0, 0).strength(0.02))
|
||||||
.alphaDecay(0.02) // Замедляем затухание
|
.alphaDecay(0.02)
|
||||||
.alphaTarget(0.1);
|
.alphaTarget(0.1);
|
||||||
|
|
||||||
// Запускаем симуляцию на 15 секунд, затем отключаем
|
// Запускаем симуляцию на 15 секунд, затем отключаем
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
simulationRef.current.stop(); // Останавливаем симуляцию
|
if (simulationRef.current) {
|
||||||
nodes.forEach((node) => {
|
simulationRef.current.stop(); // Останавливаем симуляцию
|
||||||
node.fx = node.x; // Фиксируем текущие позиции узлов
|
nodes.forEach((node) => {
|
||||||
node.fy = node.y;
|
node.fx = node.x; // Фиксируем текущие позиции узлов
|
||||||
});
|
node.fy = node.y;
|
||||||
|
});
|
||||||
|
}
|
||||||
}, 15000); // 15 секунд
|
}, 15000); // 15 секунд
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!root || !chartRef.current) return;
|
if (!root || !chartRef.current || !simulationRef.current) return; // Проверяем, что симуляция инициализирована
|
||||||
|
|
||||||
const svg = d3.select(chartRef.current);
|
const svg = d3.select(chartRef.current);
|
||||||
const linkGroup = svg.select(".links");
|
const linkGroup = svg.select(".links");
|
||||||
|
|
@ -107,9 +117,11 @@ const TreeChart = ({ data, onNodeClick }) => {
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.data(nodes, (d) => d.data.id)
|
.data(nodes, (d) => d.data.id)
|
||||||
.join("text")
|
.join("text")
|
||||||
.text((d) => 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("pointer-events", "none"); // Запрет взаимодействия с текстом
|
||||||
|
|
||||||
// Обновляем симуляцию
|
// Обновляем симуляцию
|
||||||
simulationRef.current.nodes(nodes);
|
simulationRef.current.nodes(nodes);
|
||||||
|
|
@ -136,7 +148,7 @@ const TreeChart = ({ data, onNodeClick }) => {
|
||||||
|
|
||||||
const drag = () => {
|
const drag = () => {
|
||||||
function dragstarted(event, d) {
|
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.fx = d.x;
|
||||||
d.fy = d.y;
|
d.fy = d.y;
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +159,7 @@ const TreeChart = ({ data, onNodeClick }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragended(event, d) {
|
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 });
|
nodePositions.current.set(d.data.id, { x: d.x, y: d.y, fx: d.fx, fy: d.fy });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,85 @@
|
||||||
// Функция для генерации случайных статусов
|
const StatusManager = () => {
|
||||||
const getRandomStatus = () => {
|
const getRandomStatus = () => {
|
||||||
const statuses = [
|
const statuses = [
|
||||||
...Array(90).fill("green"), // 90% chance
|
...Array(90).fill("green"), // 90% шанс
|
||||||
...Array(6).fill("yellow"), // 6% chance
|
...Array(6).fill("yellow"), // 6% шанс
|
||||||
...Array(3).fill("orange"), // 3% chance
|
...Array(3).fill("orange"), // 3% шанс
|
||||||
...Array(1).fill("red"), // 1% chance
|
...Array(1).fill("red"), // 1% шанс
|
||||||
];
|
];
|
||||||
return statuses[Math.floor(Math.random() * statuses.length)];
|
return statuses[Math.floor(Math.random() * statuses.length)];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для получения числового значения статуса
|
const getStatusWeight = (status) => {
|
||||||
const getStatusValue = (status) => {
|
switch (status) {
|
||||||
switch (status) {
|
case "green": return 1; // 100% здоровья
|
||||||
case "green":
|
case "yellow": return 0.75;
|
||||||
return 0;
|
case "orange": return 0.5;
|
||||||
case "yellow":
|
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;
|
return 1;
|
||||||
case "orange":
|
}
|
||||||
return 2;
|
|
||||||
case "red":
|
// Вычисляем среднее арифметическое значение весов статусов
|
||||||
return 3;
|
const averageStatusWeight =
|
||||||
default:
|
childStatusWeights.reduce((sum, weight) => sum + weight, 0) / childStatusWeights.length;
|
||||||
return 0; // По умолчанию green
|
|
||||||
}
|
// Определяем статус текущего элемента
|
||||||
|
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) => {
|
export const statusManager1 = StatusManager();
|
||||||
if (value >= 3) return "red";
|
export const statusManager2 = StatusManager();
|
||||||
if (value >= 2) return "orange";
|
|
||||||
if (value >= 1) return "yellow";
|
// Функция для расчета процентов здоровья системы
|
||||||
return "green";
|
export const calculateStatusPercentage = (averageStatusValue) => {
|
||||||
|
return Math.max(0, Math.min(100, averageStatusValue * 100));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для обновления статусов в дереве
|
// Экспортируем getStatusColor отдельно
|
||||||
const updateStatuses = (data) => {
|
export const getStatusColor = (status) => {
|
||||||
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) => {
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "green":
|
case "green":
|
||||||
return "#4CAF50"; // Зеленый
|
return "#4CAF50"; // Зеленый
|
||||||
|
|
@ -68,5 +93,3 @@ const getStatusColor = (status) => {
|
||||||
return "#4CAF50"; // По умолчанию зеленый
|
return "#4CAF50"; // По умолчанию зеленый
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getRandomStatus, updateStatuses, getStatusColor };
|
|
||||||
|
|
@ -26,64 +26,38 @@ const getAllChildIds = (node) => {
|
||||||
const tabContent = (data) => {
|
const tabContent = (data) => {
|
||||||
const tabContent = {};
|
const tabContent = {};
|
||||||
|
|
||||||
|
// Функция для рекурсивного обхода и сбора данных
|
||||||
const generateContent = (nodes) => {
|
const generateContent = (nodes) => {
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
|
|
||||||
|
|
||||||
// Если у узла есть вложенные элементы, рекурсивно обрабатываем их
|
// Если у узла есть вложенные элементы, рекурсивно обрабатываем их
|
||||||
if (node.items && node.items.length > 0) {
|
if (node.items && node.items.length > 0) {
|
||||||
|
// Создаем контент для родителя
|
||||||
|
const childrenContent = generateContent(node.items);
|
||||||
|
|
||||||
generateContent(node.items);
|
const content = (
|
||||||
}
|
|
||||||
|
|
||||||
// Если у узла есть id, добавляем его в tabContent
|
|
||||||
if (node.id) {
|
|
||||||
|
|
||||||
|
|
||||||
let content = (
|
|
||||||
<div>
|
<div>
|
||||||
<h2>{node.title}</h2>
|
<h2>{node.title}</h2>
|
||||||
<p>Контент для {node.title}.</p>
|
<p>Контент для {node.title}.</p>
|
||||||
|
{childrenContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Если у узла есть потомки, добавляем графики для всех потомков
|
// Сохраняем контент для текущего id
|
||||||
if (node.items && node.items.length > 0) {
|
tabContent[node.id] = {
|
||||||
const childIds = getAllChildIds(node); // Получаем все id потомков
|
title: node.title,
|
||||||
const charts = childIds.map((id) => {
|
content: content,
|
||||||
const metricName = getMetricName(id);
|
};
|
||||||
return (
|
} else {
|
||||||
<div key={id}>
|
// Если у узла нет вложенных элементов, это самый нижний уровень
|
||||||
<h3>{node.title} - {id}</h3>
|
const metricName = getMetricName(node.id);
|
||||||
<Suspense fallback={<div>Загрузка графика...</div>}>
|
const content = (
|
||||||
<PrometheusChart metricName={metricName} />
|
<div key={node.id}>
|
||||||
</Suspense>
|
<h3>{node.title}</h3> {/* Используем title узла */}
|
||||||
</div>
|
<Suspense fallback={<div>Загрузка графика...</div>}>
|
||||||
);
|
<PrometheusChart metricName={metricName} />
|
||||||
});
|
</Suspense>
|
||||||
|
</div>
|
||||||
content = (
|
);
|
||||||
<div>
|
|
||||||
<h2>{node.title}</h2>
|
|
||||||
<p>Контент для {node.title}.</p>
|
|
||||||
{charts}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Если у узла нет потомков, добавляем график для него
|
|
||||||
|
|
||||||
const metricName = getMetricName(node.id);
|
|
||||||
content = (
|
|
||||||
<div>
|
|
||||||
<h2>{node.title}</h2>
|
|
||||||
<p>Контент для {node.title}.</p>
|
|
||||||
<Suspense fallback={<div>Загрузка графика...</div>}>
|
|
||||||
<PrometheusChart metricName={metricName} />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем контент для текущего id
|
// Сохраняем контент для текущего id
|
||||||
tabContent[node.id] = {
|
tabContent[node.id] = {
|
||||||
|
|
@ -92,6 +66,15 @@ const tabContent = (data) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Возвращаем контент для всех потомков
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<div key={node.id}>{tabContent[node.id].content}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Начинаем обработку с корневого уровня
|
// Начинаем обработку с корневого уровня
|
||||||
|
|
@ -104,4 +87,4 @@ const tabContent = (data) => {
|
||||||
return tabContent;
|
return tabContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default tabContent; // Экспортируем только функцию
|
export default tabContent;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import "../../Style/TreeTable.css";
|
import "../../Style/TreeTable.css";
|
||||||
import { getStatusColor } from "../TreeChart/dataUtils";
|
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
||||||
|
|
||||||
const TreeTable = ({ data }) => {
|
const TreeTable = ({ data }) => {
|
||||||
const tableRef = useRef(null);
|
const tableRef = useRef(null);
|
||||||
|
|
@ -61,7 +61,8 @@ const TreeTable = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}>
|
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}>
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: getStatusColor(item.status) }} />
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(item.status) }} />
|
||||||
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(item.status), marginLeft: "5px" }} />
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -82,7 +83,8 @@ const TreeTable = ({ data }) => {
|
||||||
{item.items.map((child) => (
|
{item.items.map((child) => (
|
||||||
<td key={child.id} className="tree-table-cell" title={child.title}>
|
<td key={child.id} className="tree-table-cell" title={child.title}>
|
||||||
<div className="cell-content">
|
<div className="cell-content">
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: getStatusColor(child.status) }} />
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(child.status) }} />
|
||||||
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(child.status), marginLeft: "5px" }} />
|
||||||
{child.title}
|
{child.title}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -93,7 +95,8 @@ const TreeTable = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<td key={item.id} className="tree-table-cell" title={item.title}>
|
<td key={item.id} className="tree-table-cell" title={item.title}>
|
||||||
<div className="cell-content">
|
<div className="cell-content">
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: getStatusColor(item.status) }} />
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(item.status) }} />
|
||||||
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(item.status), marginLeft: "5px" }} />
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -115,7 +118,8 @@ const TreeTable = ({ data }) => {
|
||||||
title={data.title}
|
title={data.title}
|
||||||
>
|
>
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: getStatusColor(data.status) }} />
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(data.status) }} />
|
||||||
|
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(data.status), marginLeft: "5px" }} />
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -132,7 +136,7 @@ const TreeTable = ({ data }) => {
|
||||||
<h3>Лог статусов</h3>
|
<h3>Лог статусов</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{log.map((entry, index) => (
|
{log.map((entry, index) => (
|
||||||
<li key={index} style={{ color: getStatusColor(entry.status) }}>
|
<li key={index} style={{ color: statusManager1.getStatusColor(entry.status) }}>
|
||||||
[{entry.time}] {entry.status}: {entry.title}
|
[{entry.time}] {entry.status}: {entry.title}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
svg {
|
||||||
|
user-select: none;
|
||||||
|
/* Запрет выделения текста */
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
pointer-events: none;
|
||||||
|
/* Запрет взаимодействия с текстом */
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue