Добавил второй индикатор, исправил отображение потомков в уровнях вложенности, уменьшил граф
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details

pull/17/head
DmitriyA 2025-03-12 12:19:43 -04:00
parent 88b63959be
commit a53942b264
7 changed files with 217 additions and 184 deletions

View File

@ -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;

View File

@ -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}

View File

@ -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(() => {
if (simulationRef.current) {
simulationRef.current.stop(); // Останавливаем симуляцию simulationRef.current.stop(); // Останавливаем симуляцию
nodes.forEach((node) => { nodes.forEach((node) => {
node.fx = node.x; // Фиксируем текущие позиции узлов node.fx = node.x; // Фиксируем текущие позиции узлов
node.fy = node.y; 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 });
} }

View File

@ -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": case "green": return 1; // 100% здоровья
return 0; case "yellow": return 0.75;
case "yellow": case "orange": return 0.5;
return 1; case "red": return 0.25; // 25% здоровья
case "orange": default: return 1; // По умолчанию "green"
return 2;
case "red":
return 3;
default:
return 0; // По умолчанию green
} }
}; };
// Функция для получения статуса по числовому значению const updateStatuses = (data) => {
const getStatusFromValue = (value) => {
if (value >= 3) return "red";
if (value >= 2) return "orange";
if (value >= 1) return "yellow";
return "green";
};
// Функция для обновления статусов в дереве
const updateStatuses = (data) => {
if (!data.items || data.items.length === 0) { if (!data.items || data.items.length === 0) {
// Если это элемент нижнего уровня, генерируем случайный статус // Если это элемент нижнего уровня, генерируем случайный статус
data.status = getRandomStatus(); data.status = getRandomStatus();
return getStatusValue(data.status); return getStatusWeight(data.status);
} }
// Рекурсивно обновляем статусы для всех дочерних элементов // Рекурсивно обновляем статусы для всех дочерних элементов
let childStatusValues = data.items.map((child) => updateStatuses(child)); let childStatusWeights = data.items.map((child) => updateStatuses(child));
// Вычисляем среднее арифметическое значение статусов // Проверяем, есть ли дочерние элементы (избегаем деления на 0)
const averageStatusValue = childStatusValues.reduce((sum, value) => sum + value, 0) / childStatusValues.length; if (childStatusWeights.length === 0) {
data.status = "green";
return 1;
}
// Определяем статус текущего элемента на основе среднего значения // Вычисляем среднее арифметическое значение весов статусов
data.status = getStatusFromValue(averageStatusValue); const averageStatusWeight =
childStatusWeights.reduce((sum, weight) => sum + weight, 0) / childStatusWeights.length;
return getStatusValue(data.status); // Определяем статус текущего элемента
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 getStatusColor = (status) => { export const statusManager1 = StatusManager();
export const statusManager2 = StatusManager();
// Функция для расчета процентов здоровья системы
export const calculateStatusPercentage = (averageStatusValue) => {
return Math.max(0, Math.min(100, averageStatusValue * 100));
};
// Экспортируем getStatusColor отдельно
export 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 };

View File

@ -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 (
<div key={id}>
<h3>{node.title} - {id}</h3>
<Suspense fallback={<div>Загрузка графика...</div>}>
<PrometheusChart metricName={metricName} />
</Suspense>
</div>
);
});
content = (
<div>
<h2>{node.title}</h2>
<p>Контент для {node.title}.</p>
{charts}
</div>
);
} else { } else {
// Если у узла нет потомков, добавляем график для него // Если у узла нет вложенных элементов, это самый нижний уровень
const metricName = getMetricName(node.id); const metricName = getMetricName(node.id);
content = ( const content = (
<div> <div key={node.id}>
<h2>{node.title}</h2> <h3>{node.title}</h3> {/* Используем title узла */}
<p>Контент для {node.title}.</p>
<Suspense fallback={<div>Загрузка графика...</div>}> <Suspense fallback={<div>Загрузка графика...</div>}>
<PrometheusChart metricName={metricName} /> <PrometheusChart metricName={metricName} />
</Suspense> </Suspense>
</div> </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;

View File

@ -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>
))} ))}

9
src/Style/TreeChart.css Normal file
View File

@ -0,0 +1,9 @@
svg {
user-select: none;
/* Запрет выделения текста */
}
text {
pointer-events: none;
/* Запрет взаимодействия с текстом */
}