Compare commits

..

9 Commits

55 changed files with 5401 additions and 1055 deletions

7
.gitignore vendored
View File

@ -25,3 +25,10 @@ dist-ssr
*.les* *.les*
node_modules node_modules
# Игнорировать .env файлы
.env
.env.local
.env.development
.env.production
.env.test

0
Jenkinsfile vendored Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

View File

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

View File

@ -19,7 +19,11 @@
"chartjs-chart-box-and-violin-plot": "^4.0.0", "chartjs-chart-box-and-violin-plot": "^4.0.0",
"react-chartjs-2": "^5.0.0", "react-chartjs-2": "^5.0.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"react-datepicker": "^8.1.0" "react-datepicker": "^8.1.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7",
"@mui/icons-material": "^6.4.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",

0
public/TrustSoftware.json Normal file → Executable file
View File

0
public/data.json Normal file → Executable file
View File

0
public/system_monitor_icon.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 729 B

0
public/trust.json Normal file → Executable file
View File

View File

@ -1,25 +1,38 @@
import React, { useState } from "react"; import React, { useState, useMemo } from "react";
import { ThemeProvider, CssBaseline, Switch, Box } from "@mui/material";
import Dashboard from "./Components/Layout/Dashboard"; import Dashboard from "./Components/Layout/Dashboard";
import LoginModal from "./Components/UI/LoginModal"; // Импортируем компонент авторизации import LoginModal from "./Components/UI/LoginModal";
import "./Style/LoginModal.css"; // Импортируем стили import { lightTheme, darkTheme } from "./Style/theme";
import "./Style/LoginModal.css";
function App() { function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false); // Состояние авторизации const [isAuthenticated, setIsAuthenticated] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(true); // Показывать ли модальное окно const [showLoginModal, setShowLoginModal] = useState(true);
const [isDarkMode, setIsDarkMode] = useState(
window.matchMedia("(prefers-color-scheme: dark)").matches
);
const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]);
const handleLogin = () => { const handleLogin = () => {
setIsAuthenticated(true); // Устанавливаем авторизацию setIsAuthenticated(true);
setShowLoginModal(false); // Скрываем модальное окно setShowLoginModal(false);
}; };
return ( return (
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}> <ThemeProvider theme={theme}>
{!isAuthenticated && showLoginModal && ( <CssBaseline />
{!isAuthenticated && showLoginModal ? (
<LoginModal onLogin={handleLogin} onClose={() => setShowLoginModal(false)} /> <LoginModal onLogin={handleLogin} onClose={() => setShowLoginModal(false)} />
) : (
<Box sx={{ display: "flex", height: "100vh", overflow: "hidden", bgcolor: "background.default", color: "text.primary" }}>
<Dashboard />
<Box sx={{ position: "absolute", top: 10, right: 10 }}>
<Switch checked={isDarkMode} onChange={() => setIsDarkMode((prev) => !prev)} />
</Box>
</Box>
)} )}
</ThemeProvider>
{isAuthenticated && <Dashboard />}
</div>
); );
} }

0
src/Charts/Components/BarChartComponent.jsx Normal file → Executable file
View File

0
src/Charts/Components/CounterComponent.jsx Normal file → Executable file
View File

0
src/Charts/Components/LineChartComponent.jsx Normal file → Executable file
View File

0
src/Charts/Components/ScatterChartComponent.jsx Normal file → Executable file
View File

0
src/Charts/NegativeStatusChart.jsx Normal file → Executable file
View File

5
src/Charts/PrometheusChart.jsx Normal file → Executable file
View File

@ -96,6 +96,11 @@ const PrometheusChart = ({ metricName }) => {
params: { metric: metricName, start, end, step }, params: { metric: metricName, start, end, step },
}); });
/*
const response = await axios.get(`${process.env.REACT_APP_BACK_URL}/metrics`, {
params: { metric: metricName, start, end, step },
}); */
const result = response.data; const result = response.data;
let metrics = Array.isArray(result) ? result : result.data || []; let metrics = Array.isArray(result) ? result : result.data || [];

0
src/Charts/SystemStatusChart.jsx Normal file → Executable file
View File

0
src/Charts/SystemStatusTable.jsx Normal file → Executable file
View File

0
src/Charts/SystemStatusTableSoftware.jsx Normal file → Executable file
View File

126
src/Components/Layout/Dashboard.jsx Normal file → Executable file
View File

@ -1,33 +1,32 @@
import React, { useState, useEffect, useRef, useCallback } from "react"; import React, { useState, useEffect } from "react";
import SidebarMenu from "./SidebarMenu"; import SidebarMenu from "./SidebarMenu";
import TreeChart from "../TreeChart/TreeChart";
import "../../Style/Dashboard.css"; import "../../Style/Dashboard.css";
import SystemStatusChart from "../../Charts/SystemStatusChart";
import Tabs from "../UI/Tabs";
import menuData from "../TreeChart/menuData.json";
import TreeTable from "../UI/TreeTable";
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
import generateTabContent from "../TreeChart/tabContent"; import generateTabContent from "../TreeChart/tabContent";
import CustomTabs from "../UI/MUItabs";
import useTabs from "../hooks/useTabs";
import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent";
import menuData from "../TreeChart/menuData.json";
const Dashboard = () => { const Dashboard = () => {
const [tabs, setTabs] = useState([]); const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
const [activeTab, setActiveTab] = useState("Главная"); const { sidebarWidth, startResizing } = useSidebarResize(250);
const [tabContent, setTabContent] = useState({}); const [tabContent, setTabContent] = useState({});
const [treeData1, setTreeData1] = useState(menuData); const [treeData1, setTreeData1] = useState(menuData);
const [treeData2, setTreeData2] = useState(menuData); const [treeData2, setTreeData2] = useState(menuData);
const [sidebarWidth, setSidebarWidth] = useState(250);
const [isResizing, setIsResizing] = useState(false);
const [statusHistories, setStatusHistories] = useState({ const [statusHistories, setStatusHistories] = useState({
history1: [], history1: [],
history2: [], history2: [],
}); });
const sidebarRef = useRef(null);
// Генерация контента для вкладок
useEffect(() => { useEffect(() => {
const generatedTabContent = generateTabContent(menuData); const generatedTabContent = generateTabContent(menuData);
setTabContent(generatedTabContent); setTabContent(generatedTabContent);
}, []); }, []);
// Обновление статусов каждые 30 секунд
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
const updatedData1 = JSON.parse(JSON.stringify(treeData1)); const updatedData1 = JSON.parse(JSON.stringify(treeData1));
@ -40,11 +39,11 @@ const Dashboard = () => {
setStatusHistories((prevHistories) => ({ setStatusHistories((prevHistories) => ({
history1: [ history1: [
...prevHistories.history1.slice(-49), ...prevHistories.history1.slice(-29),
{ time: new Date().toLocaleTimeString(), status: statusPercentage1 }, { time: new Date().toLocaleTimeString(), status: statusPercentage1 },
], ],
history2: [ history2: [
...prevHistories.history2.slice(-49), ...prevHistories.history2.slice(-29),
{ time: new Date().toLocaleTimeString(), status: statusPercentage2 }, { time: new Date().toLocaleTimeString(), status: statusPercentage2 },
], ],
})); }));
@ -56,98 +55,33 @@ const Dashboard = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [treeData1, treeData2]); }, [treeData1, treeData2]);
const startResizing = useCallback((e) => {
e.preventDefault();
setIsResizing(true);
}, []);
const resize = useCallback((e) => {
if (isResizing) {
const newWidth = e.clientX;
if (newWidth > 100 && newWidth < 400) {
setSidebarWidth(newWidth);
}
}
}, [isResizing]);
const stopResizing = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
const handleMouseMove = (e) => resize(e);
const handleMouseUp = () => stopResizing();
if (isResizing) {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [isResizing, resize, stopResizing]);
const handleOpenTab = (id, title) => {
if (!tabs.some((tab) => tab.id === id)) {
setTabs([...tabs, { id, title }]);
}
setActiveTab(id);
};
const handleCloseTab = (id) => {
const newTabs = tabs.filter((tab) => tab.id !== id);
setTabs(newTabs);
if (activeTab === id) {
setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1].id : "Главная");
}
};
const renderTabContent = () => {
if (activeTab === "Главная") {
return (
<div>
<h2>Общий мониторинг состояния системы</h2>
<label>Процент доверия системы</label>
<SystemStatusChart data={statusHistories.history1} />
<label>Функциональность системы</label>
<SystemStatusChart data={statusHistories.history2} />
<label>Статус компонентов системы</label>
<TreeTable data={treeData1} />
</div>
);
} else if (activeTab === "Визуализация") {
return <TreeChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
} else {
const tabData = tabContent[activeTab];
return tabData ? tabData.content : <p>Нет данных</p>;
}
};
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
<div {/* Сайдбар */}
className="sidebar" <div className="sidebar" style={{ width: sidebarWidth }}>
ref={sidebarRef} <SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} />
style={{ width: sidebarWidth }} <div className="sidebar-resizer" onMouseDown={startResizing} />
>
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} />
<div
className="sidebar-resizer"
onMouseDown={startResizing}
/>
</div> </div>
<div className="main-content" style={{ marginLeft: sidebarWidth }}> {/* Основной контент */}
<Tabs <div className="main-content">
{/* Вкладки */}
<CustomTabs
tabs={tabs} tabs={tabs}
activeTab={activeTab} activeTab={activeTab}
onTabClick={(id) => setActiveTab(id)} onTabClick={setActiveTab}
onCloseTab={handleCloseTab} onCloseTab={handleCloseTab}
/> />
{/* Контент вкладки */}
<div className="content"> <div className="content">
{renderTabContent()} <TabContent
activeTab={activeTab}
statusHistories={statusHistories}
treeData1={treeData1}
tabContent={tabContent}
handleOpenTab={handleOpenTab}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,82 +1,49 @@
import React, { useState } from "react"; import React from "react";
import "../../Style/SidebarMenu.css"; import { Drawer, List } from "@mui/material";
import { getStatusColor } from "../TreeChart/dataUtils"; // Импортируем только нужную функцию import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
const MenuItem = ({ item, onSelectItem, sidebarWidth }) => { const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
const [isOpen, setIsOpen] = useState(false); const handleSelectItem = (id, title, children) => {
const hasChildren = Array.isArray(item.items) && item.items.length > 0; onOpenTab(id, title, children);
const statusColor = getStatusColor(item.status);
// Обработчик одинарного клика (разворачивание/сворачивание или открытие элемента)
const handleSingleClick = () => {
if (hasChildren) {
setIsOpen(!isOpen); // Разворачиваем/сворачиваем дочерние элементы
} else {
onSelectItem(item); // Если нет потомков, открываем элемент как вкладку
}
};
// Обработчик клика для открытия родителя
const handleOpenParent = (e) => {
e.stopPropagation(); // Останавливаем всплытие события, чтобы не сработал handleSingleClick
onSelectItem(item); // Открываем родителя
}; };
return ( return (
<div className="menu-item" style={{ width: sidebarWidth - 20 }}> {/* Динамическая ширина */} <Drawer
variant="permanent"
sx={{
width: sidebarWidth,
flexShrink: 0,
"& .MuiDrawer-paper": {
width: sidebarWidth,
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
},
}}
>
<List>
<h2 style={{ padding: "16px", fontWeight: "bold" }}>Меню</h2>
<MenuItem item={data} onSelectItem={handleSelectItem} />
</List>
{/* Ресайзер */}
<div <div
onClick={handleSingleClick} // Одинарный клик для разворачивания/сворачивания или открытия onMouseDown={startResizing}
className="menu-item-header" style={{
> width: "5px",
{/* Круглый индикатор статуса */} cursor: "ew-resize",
<div backgroundColor: "#ccc",
className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`} height: "100%",
style={{ backgroundColor: statusColor }} position: "absolute",
/> right: 0,
<span>{item.title}</span> top: 0,
}}
/>
{/* Иконка для открытия родителя */} <SidebarFooter sidebarWidth={sidebarWidth} />
{hasChildren && ( </Drawer>
<span
onClick={handleOpenParent}
className="open-parent-icon"
title="Открыть родителя"
>
📂
</span>
)}
{/* Иконка для разворачивания/сворачивания */}
{hasChildren && <span>{isOpen ? "▲" : "▼"}</span>}
</div>
{isOpen && hasChildren && (
<div className="submenu">
{item.items.map((child, index) => (
<MenuItem key={index} item={child} onSelectItem={onSelectItem} sidebarWidth={sidebarWidth} />
))}
</div>
)}
</div>
); );
}; };
function SidebarMenu({ data, onOpenTab, sidebarWidth }) {
const handleSelectItem = (item) => {
onOpenTab(item.id, item.title);
};
return (
<div className="sidebar">
<div className="sidebar-content" style={{ width: sidebarWidth }}> {/* Динамическая ширина */}
<h2 className="sidebar-title">Меню</h2>
<MenuItem item={data} onSelectItem={handleSelectItem} sidebarWidth={sidebarWidth} />
</div>
<div className="sidebar-footer" style={{ width: sidebarWidth }}> {/* Динамическая ширина */}
<h2 className="help">Помощь</h2>
<h2 className="settings">Настройка</h2>
</div>
</div>
);
}
export default SidebarMenu; export default SidebarMenu;

View File

@ -0,0 +1,55 @@
import React from "react";
import { Drawer, List, ListItem, ListItemIcon, ListItemText, Collapse } from "@mui/material";
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
// Функция для сбора всех потомков
const getAllChildren = (node) => {
let children = [];
if (node.items && node.items.length > 0) {
node.items.forEach((child) => {
children.push(child); // Добавляем текущий элемент
children = children.concat(getAllChildren(child)); // Рекурсивно добавляем потомков
});
}
return children;
};
const MenuItem = ({ item, onSelectItem }) => {
const [isOpen, setIsOpen] = React.useState(false);
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
const handleToggle = () => {
setIsOpen(!isOpen);
};
const handleOpenTab = (e) => {
e.stopPropagation(); // Останавливаем всплытие события
const allChildren = getAllChildren(item); // Собираем всех потомков
onSelectItem(item.id, item.title, allChildren); // Передаем данные в родительский компонент
};
return (
<>
<ListItem component="div" onClick={handleToggle}>
<ListItemIcon>
<div onClick={handleOpenTab} style={{ cursor: "pointer" }}>
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
</div>
</ListItemIcon>
<ListItemText primary={item.title} />
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
</ListItem>
{hasChildren && (
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.items.map((child, index) => (
<MenuItem key={index} item={child} onSelectItem={onSelectItem} />
))}
</List>
</Collapse>
)}
</>
);
};
export default MenuItem;

View File

@ -0,0 +1,17 @@
import React from "react";
import { List, ListItem, ListItemText } from "@mui/material";
const SidebarFooter = ({ sidebarWidth }) => {
return (
<List sx={{ marginTop: "auto", backgroundColor: "#ffffff", padding: "10px 0" }}>
<ListItem button={true}>
<ListItemText primary="Помощь" sx={{ color: "#000000" }} />
</ListItem>
<ListItem button={true}>
<ListItemText primary="Настройка" sx={{ color: "#000000" }} />
</ListItem>
</List>
);
};
export default SidebarFooter;

163
src/Components/TreeChart/TreeChart.jsx Normal file → Executable file
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 {
const parent = node.parent; if (node.depth === 0) {
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.x = center.x;
node.y = center.y;
} else if (node.depth === 1) {
// Первый уровень - равномерно по окружности
const parent = node.parent;
const index = parent.children.indexOf(node);
const totalSiblings = parent.children.length;
const radius = baseRadius * node.depth;
const sectorAngle = (Math.PI * 2) / totalSiblings;
const angle = index * sectorAngle;
node.x = parent.x + radius * Math.cos(angle);
node.y = parent.y + radius * Math.sin(angle);
node.angle = angle; // Запоминаем угол для веток
} else {
// Второй уровень и дальше - ветка растет в направлении родителя
const parent = node.parent;
const siblings = parent.children || [];
const index = siblings.indexOf(node);
const totalSiblings = siblings.length;
const direction = parent.angle || 0;
const offsetAngle = ((index - (totalSiblings - 1) / 2) * angleOffset) / totalSiblings;
let distance = branchOffset;
if (!node.children || node.children.length === 0) {
// Если это последний узел, увеличиваем расстояние
distance *= spreadFactor + node.depth * 0.2; // Чем глубже, тем больше разброс
}
node.x = parent.x + distance * Math.cos(direction + offsetAngle);
node.y = parent.y + distance * Math.sin(direction + offsetAngle);
node.angle = direction + offsetAngle;
}
} }
nodePositions.current.set(node.data.id, { x: node.x, y: node.y, fx: node.fx, fy: node.fy }); nodePositions.current.set(node.data.id, { x: node.x, y: node.y });
}); });
return { root, nodes, links }; return { root, nodes, links };
}, [data]); }, [data]);
@ -48,55 +88,33 @@ const TreeChart = ({ data, onNodeClick }) => {
const svg = d3.select(chartRef.current) const svg = d3.select(chartRef.current)
.attr("width", 2000) .attr("width", 2000)
.attr("height", 2000) .attr("height", 2000)
.attr("viewBox", [-500, -500, 1000, 1000]) .attr("viewBox", [-500, -500, 1500, 1500])
.attr("style", "max-width: 100%; height: auto;"); .attr("style", "max-width: 100%; height: auto;");
svg.append("g").attr("class", "links"); svg.append("g").attr("class", "links");
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)") // Используем переменную для цвета текста
// Обновляем симуляцию .attr("x", (d) => d.x + 12)
simulationRef.current.nodes(nodes); .attr("y", (d) => d.y + 4);
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("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);

0
src/Components/TreeChart/dataUtils.jsx Normal file → Executable file
View File

4516
src/Components/TreeChart/menuData.json Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,714 @@
{
"title": "Сервис ЗВКС",
"id": "1",
"items": [
{
"title": "Функциональные задачи",
"id": "functional_tasks",
"items": [
{
"id": "system_control",
"title": "Контроль системы"
},
{
"id": "system_management",
"title": "Система управления"
},
{
"id": "conference",
"title": "Проведение ВКС"
},
{
"id": "backup",
"title": "Резервное копирование"
},
{
"id": "relay_info",
"title": "Ретрансляция информации"
}
]
},
{
"id": "18",
"title": "Graviton S2082I (device$18)",
"items": [
{
"id": "4",
"title": "OS Linux (module$4) АО",
"items": [
{
"id": "190",
"title": "Загрузка процессора за 1 минуту"
},
{
"id": "191",
"title": "Загрузка процессора за 5 минут"
},
{
"id": "192",
"title": "Загрузка процессора за 15 минут"
},
{
"id": "197",
"title": "Общий объем SWAP-файла"
},
{
"id": "198",
"title": "Используемый объем SWAP-файла"
},
{
"id": "199",
"title": "Общий объем физической оперативной памяти"
},
{
"id": "200",
"title": "Доступный объем физической оперативной памяти"
},
{
"id": "201",
"title": "Свободный объем физической и виртуальной оперативной памяти"
},
{
"id": "202",
"title": "Буферизованный объем оперативной памяти"
},
{
"id": "203",
"title": "Кэшированый объем оперативной памяти"
},
{
"id": "274",
"title": "Используемый объем SWAP-файла"
},
{
"id": "275",
"title": "Время затраченное процессором на процессы с пониженным приоритетом"
},
{
"id": "276",
"title": "Время затраченное процессором на процессы ядра ОС"
},
{
"id": "277",
"title": "Время простоя процессора"
},
{
"id": "278",
"title": "Общая емкость жестких дисков"
},
{
"id": "279",
"title": "Доступная емкость жестких дисков"
}
]
},
{
"id": "5",
"title": "Vinteo (module$5) ПО",
"items": [
{
"id": "31",
"title": "Общее количество участников"
},
{
"id": "32",
"title": "Ожидание соединения"
},
{
"id": "33",
"title": "Зарегистрированные абоненты"
},
{
"id": "34",
"title": "Количество пользоватей HLS"
},
{
"id": "35",
"title": "Общее количество P2P комнат"
},
{
"id": "36",
"title": "Общее количество конференций"
},
{
"id": "37",
"title": "Общее количество активных конференций"
},
{
"id": "38",
"title": "Статус записи"
},
{
"id": "39",
"title": "Общее количество сохранённых записей"
}
]
},
{
"id": "280",
"title": "Сетевой адаптер №1 (port$261) Eth_1",
"items": [
{
"id": "207",
"title": "Скорость порта Eth_1"
},
{
"id": "209",
"title": "Административное состояние порта Eth_1"
},
{
"id": "210",
"title": "Оперативное состояние порта Eth_1"
},
{
"id": "211",
"title": "Общее количество отправленных октетов Eth_1"
},
{
"id": "212",
"title": "Количество входящих Multicast пакетов Eth_1"
},
{
"id": "213",
"title": "Количество иcходящих Multiicast пакетов Eth_1"
},
{
"id": "214",
"title": "Количество входящих Broadcast пакетов Eth_1"
},
{
"id": "215",
"title": "Количество иcходящих Broadcast пакетов Eth_1"
},
{
"id": "216",
"title": "Количество входящих Unicast пакетов Eth_1"
},
{
"id": "217",
"title": "Количество иcходящих Unicast пакетов Eth_1"
},
{
"id": "218",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_1"
},
{
"id": "219",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_1"
},
{
"id": "220",
"title": "Количество входящих пакетов с ошибкой Eth_1"
},
{
"id": "221",
"title": "Количество исходящих пакетов с ошибкой Eth_1"
},
{
"id": "222",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_1"
}
]
},
{
"id": "281",
"title": "Сетевой адаптер №2 (port$262) Eth_2",
"items": [
{
"id": "224",
"title": "Скорость порта Eth_2"
},
{
"id": "226",
"title": "Административное состояние порта Eth_2"
},
{
"id": "227",
"title": "Оперативное состояние порта Eth_2"
},
{
"id": "228",
"title": "Общее количество отправленных октетов Eth_2"
},
{
"id": "229",
"title": "Количество входящих Multicast пакетов Eth_2"
},
{
"id": "230",
"title": "Количество иcходящих Multiicast пакетов Eth_2"
},
{
"id": "231",
"title": "Количество входящих Broadcast пакетов Eth_2"
},
{
"id": "232",
"title": "Количество иcходящих Broadcast пакетов Eth_2"
},
{
"id": "233",
"title": "Количество входящих Unicast пакетов Eth_2"
},
{
"id": "234",
"title": "Количество иcходящих Unicast пакетов Eth_2"
},
{
"id": "235",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_2"
},
{
"id": "236",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_2"
},
{
"id": "237",
"title": "Количество входящих пакетов с ошибкой Eth_2"
},
{
"id": "238",
"title": "Количество исходящих пакетов с ошибкой Eth_2"
},
{
"id": "239",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_2"
}
]
},
{
"id": "282",
"title": "Сетевой адаптер №3 (port$263) Eth_3",
"items": [
{
"id": "241",
"title": "Скорость порта Eth_3"
},
{
"id": "243",
"title": "Административное состояние порта Eth_3"
},
{
"id": "244",
"title": "Оперативное состояние порта Eth_3"
},
{
"id": "245",
"title": "Общее количество отправленных октетов Eth_3"
},
{
"id": "246",
"title": "Количество входящих Multicast пакетов Eth_3"
},
{
"id": "247",
"title": "Количество иcходящих Multiicast пакетов Eth_3"
},
{
"id": "248",
"title": "Количество входящих Broadcast пакетов Eth_3"
},
{
"id": "249",
"title": "Количество иcходящих Broadcast пакетов Eth_3"
},
{
"id": "250",
"title": "Количество входящих Unicast пакетов Eth_3"
},
{
"id": "251",
"title": "Количество иcходящих Unicast пакетов Eth_3"
},
{
"id": "252",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_3"
},
{
"id": "253",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_3"
},
{
"id": "254",
"title": "Количество входящих пакетов с ошибкой Eth_3"
},
{
"id": "255",
"title": "Количество исходящих пакетов с ошибкой Eth_3"
},
{
"id": "256",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_3"
}
]
},
{
"id": "283",
"title": "Сетевой адаптер №4 (port$264) Eth_4",
"items": [
{
"id": "258",
"title": "Скорость порта Eth_4"
},
{
"id": "260",
"title": "Административное состояние порта Eth_4"
},
{
"id": "261",
"title": "Оперативное состояние порта Eth_4"
},
{
"id": "262",
"title": "Общее количество отправленных октетов Eth_4"
},
{
"id": "263",
"title": "Количество входящих Multicast пакетов Eth_4"
},
{
"id": "264",
"title": "Количество иcходящих Multiicast пакетов Eth_4"
},
{
"id": "265",
"title": "Количество входящих Broadcast пакетов Eth_4"
},
{
"id": "266",
"title": "Количество иcходящих Broadcast пакетов Eth_4"
},
{
"id": "267",
"title": "Количество входящих Unicast пакетов Eth_4"
},
{
"id": "268",
"title": "Количество иcходящих Unicast пакетов Eth_4"
},
{
"id": "269",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_4"
},
{
"id": "270",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_4"
},
{
"id": "271",
"title": "Количество входящих пакетов с ошибкой Eth_4"
},
{
"id": "272",
"title": "Количество исходящих пакетов с ошибкой Eth_4"
},
{
"id": "273",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_4"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_1",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_1",
"items": [
{
"id": "media_system_software_1_2",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_2",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_2",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_2",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_1",
"items": [
{
"id": "media_software_1_2",
"title": "ПО"
},
{
"id": "media_software_2_2",
"title": "ПО"
},
{
"id": "media_software_3_2",
"title": "ПО"
},
{
"id": "media_software_4_2",
"title": "ПО"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_2",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_2",
"items": [
{
"id": "media_system_software_1_3",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_3",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_3",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_3",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_2",
"items": [
{
"id": "media_software_1_3",
"title": "ПО"
},
{
"id": "media_software_2_3",
"title": "ПО"
},
{
"id": "media_software_3_3",
"title": "ПО"
},
{
"id": "media_software_4_3",
"title": "ПО"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_3",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_3",
"items": [
{
"id": "media_system_software_1_4",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_4",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_4",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_4",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_3",
"items": [
{
"id": "media_software_1_4",
"title": "ПО"
},
{
"id": "media_software_2_4",
"title": "ПО"
},
{
"id": "media_software_3_4",
"title": "ПО"
},
{
"id": "media_software_4_4",
"title": "ПО"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_4",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_4",
"items": [
{
"id": "media_system_software_1_5",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_5",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_5",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_5",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_4",
"items": [
{
"id": "media_software_1_5",
"title": "ПО"
},
{
"id": "media_software_2_5",
"title": "ПО"
},
{
"id": "media_software_3_5",
"title": "ПО"
},
{
"id": "media_software_4_5",
"title": "ПО"
}
]
}
]
},
{
"title": "Сервер систем",
"id": "system_server_1",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_5",
"items": [
{
"id": "copy_system_software_1",
"title": "Центральный процессор"
},
{
"id": "copy_system_software_2",
"title": "Оперативная память"
},
{
"id": "copy_system_software_3",
"title": "Жесткий диск"
},
{
"id": "copy_system_software_4",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_5",
"items": [
{
"id": "copy_software_1",
"title": "ПО"
},
{
"id": "copy_software_2",
"title": "ПО"
},
{
"id": "copy_software_3",
"title": "ПО"
},
{
"id": "copy_software_4",
"title": "ПО"
}
]
}
]
},
{
"title": "Сервер систем",
"id": "system_server_2",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_6",
"items": [
{
"id": "control_system_software_1",
"title": "Центральный процессор"
},
{
"id": "control_system_software_2",
"title": "Оперативная память"
},
{
"id": "control_system_software_3",
"title": "Жесткий диск"
},
{
"id": "control_system_software_4",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_6",
"items": [
{
"id": "control_software_1",
"title": "ПО"
},
{
"id": "control_software_2",
"title": "ПО"
},
{
"id": "control_software_3",
"title": "ПО"
},
{
"id": "control_software_4",
"title": "ПО"
}
]
}
]
}
]
}

0
src/Components/TreeChart/tabContent.jsx Normal file → Executable file
View File

0
src/Components/UI/ErrorIndicator.jsx Normal file → Executable file
View File

0
src/Components/UI/ExpandableInfo.jsx Normal file → Executable file
View File

75
src/Components/UI/LoginModal.jsx Normal file → Executable file
View File

@ -1,19 +1,41 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Modal from "./Modal"; import Modal from "./Modal";
import "../../Style/LoginModal.css"; import "../../Style/LoginModal.css";
import TextField from '@mui/material/TextField';
const LoginModal = ({ onLogin, onClose }) => { const LoginModal = ({ onLogin, onClose }) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [showPassword, setShowPassword] = React.useState(false);
const handleSubmit = (e) => { const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (username === "admin" && password === "admin") {
onLogin(); // Успешная авторизация try {
onClose(); // Закрыть модальное окно // Отправляем данные на бэкенд
} else { console.log("Отправляем данные:", { username, password });
setError("Неверный логин или пароль"); const response = await fetch('http://192.168.2.39:3000/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ login: username, password }),
});
const data = await response.json();
if (data.success) {
onLogin(); // Успешная авторизация
onClose(); // Закрыть модальное окно
} else {
setError(data.message || "Неверный логин или пароль");
}
} catch (err) {
console.error('Ошибка при отправке запроса:', err);
setError("Ошибка при подключении к серверу");
} }
}; };
@ -21,24 +43,29 @@ const LoginModal = ({ onLogin, onClose }) => {
<Modal onClose={onClose}> <Modal onClose={onClose}>
<h2>Авторизация</h2> <h2>Авторизация</h2>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div> <TextField
<label>Логин:</label> fullWidth
<input id="user-login"
type="text" label="Логин"
value={username} variant="filled"
onChange={(e) => setUsername(e.target.value)} margin="normal"
required required
/> onChange={(e) => setUsername(e.target.value)}
</div> size="normal"
<div> />
<label>Пароль:</label>
<input <TextField
type="password" fullWidth
value={password} id="user-password"
onChange={(e) => setPassword(e.target.value)} label="Пароль"
required variant="filled"
/> margin="normal"
</div> required
type={showPassword ? 'text' : 'password'}
onChange={(e) => setPassword(e.target.value)}
size="normal"
/>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
<button type="submit">Войти</button> <button type="submit">Войти</button>
</form> </form>

View File

@ -0,0 +1,64 @@
import React from "react";
import { Tabs, Tab, Box } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
const handleMouseDown = (e, id) => {
if (e.button === 1) {
e.preventDefault();
onCloseTab(id);
}
};
const handleChange = (event, newValue) => {
onTabClick(newValue);
};
return (
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={activeTab}
onChange={handleChange}
variant="scrollable"
scrollButtons="auto"
aria-label="tabs"
>
{/* Всегда отображаемые вкладки */}
<Tab
label="Главная"
value="Главная"
onMouseDown={(e) => handleMouseDown(e, "Главная")}
/>
<Tab
label="Визуализация"
value="Визуализация"
onMouseDown={(e) => handleMouseDown(e, "Визуализация")}
/>
{/* Динамически добавляемые вкладки */}
{tabs.map((tab) => (
<Tab
key={tab.id}
label={
<Box sx={{ display: "flex", alignItems: "center" }}>
<span>{tab.title}</span>
<CloseIcon
fontSize="small"
sx={{ ml: 1, cursor: "pointer" }}
onClick={(e) => {
e.stopPropagation();
onCloseTab(tab.id);
}}
/>
</Box>
}
value={tab.id}
onMouseDown={(e) => handleMouseDown(e, tab.id)}
/>
))}
</Tabs>
</Box>
);
};
export default CustomTabs;

0
src/Components/UI/Modal.jsx Normal file → Executable file
View File

0
src/Components/UI/Tabs.jsx Normal file → Executable file
View File

176
src/Components/UI/TreeTable.jsx Normal file → Executable file
View File

@ -27,12 +27,6 @@ const TreeTable = ({ data }) => {
} }
}; };
useEffect(() => {
adjustFontSize();
window.addEventListener("resize", adjustFontSize);
return () => window.removeEventListener("resize", adjustFontSize);
}, [data]);
useEffect(() => { useEffect(() => {
const newLog = []; const newLog = [];
const traverse = (items) => { const traverse = (items) => {
@ -41,7 +35,7 @@ const TreeTable = ({ data }) => {
newLog.push({ newLog.push({
title: item.title, title: item.title,
status: item.status, status: item.status,
time: new Date().toLocaleTimeString() // Добавляем время time: new Date().toLocaleTimeString(), // Добавляем время
}); });
} }
if (item.items) { if (item.items) {
@ -50,19 +44,31 @@ const TreeTable = ({ data }) => {
}); });
}; };
traverse(data.items); traverse(data.items);
setLog(newLog);
// Ограничиваем количество сообщений до 50
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
}, [data]); }, [data]);
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи"); const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи");
// Функция для отображения заголовков
const renderHeaders = (items) => { const renderHeaders = (items) => {
return items.map((item) => { return items.map((item) => {
const colSpan = item.items ? item.items.length : 1; const colSpan = item.items ? item.items.length : 1;
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: statusManager1.getStatusColor(item.status) }} /> <div
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(item.status), marginLeft: "5px" }} /> 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>
@ -70,41 +76,117 @@ const TreeTable = ({ data }) => {
}); });
}; };
const renderRows = (items) => { // Функция для отображения подзаголовков
if (!items || items.length === 0) return null; const renderSubHeaders = (items) => {
const hasChildren = items.some((item) => item.items && item.items.length > 0); return items.map((item) => {
if (!hasChildren) return null; if (item.items) {
return ( return item.items.map((child) => (
<tr className="tree-table-row"> <th key={child.id} className="tree-table-header" title={child.title}>
{items.map((item) => { <div className="header-content">
if (item.items && item.items.length > 0) { <div
return ( className="status-indicator-bar"
<React.Fragment key={item.id}> style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
{item.items.map((child) => ( />
<td key={child.id} className="tree-table-cell" title={child.title}> <div
<div className="cell-content"> className="status-indicator-bar"
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(child.status) }} /> style={{
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(child.status), marginLeft: "5px" }} /> backgroundColor: statusManager2.getStatusColor(child.status),
{child.title} marginLeft: "5px",
</div> }}
</td> />
))} {child.title}
</React.Fragment> </div>
); </th>
));
} else {
return (
<th key={item.id} className="tree-table-header" title={item.title}>
<div className="header-content">
<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}
</div>
</th>
);
}
});
};
// Функция для отображения данных
const renderData = (items) => {
return items.map((item) => {
if (item.items) {
return item.items.map((child) => {
if (child.items) {
return child.items.map((subChild) => (
<td key={subChild.id} className="tree-table-cell" title={subChild.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(subChild.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(subChild.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{subChild.title}</span>
</div>
</td>
));
} else { } else {
return ( return (
<td key={item.id} className="tree-table-cell" title={item.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: statusManager1.getStatusColor(item.status) }} /> <div
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(item.status), marginLeft: "5px" }} /> className="status-indicator-bar"
{item.title} style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(child.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{child.title}</span>
</div> </div>
</td> </td>
); );
} }
})} });
</tr> } else {
); return (
<td key={item.id} className="tree-table-cell" title={item.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{item.title}</span>
</div>
</td>
);
}
});
}; };
return ( return (
@ -118,15 +200,27 @@ const TreeTable = ({ data }) => {
title={data.title} title={data.title}
> >
<div className="header-content"> <div className="header-content">
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(data.status) }} /> <div
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(data.status), marginLeft: "5px" }} /> 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>
</tr> </tr>
<tr>{renderHeaders(filteredData)}</tr> <tr>{renderHeaders(filteredData)}</tr>
<tr>{renderSubHeaders(filteredData)}</tr>
</thead> </thead>
<tbody>{renderRows(filteredData)}</tbody> <tbody>
<tr className="tree-table-row">{renderData(filteredData)}</tr>
</tbody>
</table> </table>
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button"> <button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
{isLogVisible ? "Скрыть лог" : "Показать лог"} {isLogVisible ? "Скрыть лог" : "Показать лог"}

View File

@ -0,0 +1,32 @@
import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable";
import TreeChart from "../TreeChart/TreeChart";
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
if (activeTab === "Главная") {
return (
<div>
<h2>Общий мониторинг состояния системы</h2>
<div>
<div style={{ display: 'inline-block', width: '48%', marginRight: '2%' }}>
<label>Надежность системы</label>
<SystemStatusChart data={statusHistories.history1} />
</div>
<div style={{ display: 'inline-block', width: '48%' }}>
<label>Функциональность системы</label>
<SystemStatusChart data={statusHistories.history2} />
</div>
</div>
<label>Статус компонентов системы</label>
<TreeTable data={treeData1} />
</div>
);
} else if (activeTab === "Визуализация") {
return <TreeChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
} else {
const tabData = tabContent[activeTab];
return tabData ? tabData.content : <p>Нет данных</p>;
}
};
export default TabContent;

View File

@ -0,0 +1,43 @@
import { useState, useCallback, useEffect } from "react";
const useSidebarResize = (initialWidth = 250) => {
const [sidebarWidth, setSidebarWidth] = useState(initialWidth);
const [isResizing, setIsResizing] = useState(false);
const startResizing = useCallback((e) => {
e.preventDefault();
setIsResizing(true);
}, []);
const resize = useCallback((e) => {
if (isResizing) {
const newWidth = e.clientX;
if (newWidth > 100 && newWidth < 400) {
setSidebarWidth(newWidth);
}
}
}, [isResizing]);
const stopResizing = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
const handleMouseMove = (e) => resize(e);
const handleMouseUp = () => stopResizing();
if (isResizing) {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [isResizing, resize, stopResizing]);
return { sidebarWidth, startResizing };
};
export default useSidebarResize;

View File

@ -0,0 +1,26 @@
import { useState, useCallback } from "react";
const useTabs = (initialTab) => {
const [tabs, setTabs] = useState([]);
const [activeTab, setActiveTab] = useState(initialTab);
const handleOpenTab = useCallback((id, title) => {
setTabs((prevTabs) =>
prevTabs.some((tab) => tab.id === id)
? prevTabs
: [...prevTabs, { id, title }]
);
setActiveTab(id);
}, []);
const handleCloseTab = useCallback((id) => {
setTabs((prevTabs) => prevTabs.filter((tab) => tab.id !== id));
if (activeTab === id) {
setActiveTab(tabs.length > 1 ? tabs[tabs.length - 2].id : initialTab);
}
}, [activeTab, tabs, initialTab]);
return { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab };
};
export default useTabs;

21
src/Style/Dashboard.css Normal file → Executable file
View File

@ -2,24 +2,34 @@
.dashboard-container { .dashboard-container {
display: flex; display: flex;
height: 100vh; height: 100vh;
width: calc(100vw - 20px); width: 100vw;
overflow: hidden; overflow: hidden;
margin-left: 20px;
background-color: var(--background-color); background-color: var(--background-color);
color: var(--text-color); color: var(--text-color);
} }
/* Сайдбар */
.sidebar {
flex-shrink: 0;
height: 100vh;
overflow-y: auto;
background-color: var(--sidebar-color);
color: var(--sidebar-text-color);
transition: width 0.2s ease;
}
/* Основной контент */ /* Основной контент */
.main-content { .main-content {
flex: 1; flex-grow: 1;
display: flex;
flex-direction: column;
padding: 20px; padding: 20px;
margin-left: 50px;
transition: margin-left 0.2s ease;
overflow: auto; overflow: auto;
background-color: var(--background-color); background-color: var(--background-color);
color: var(--text-color); color: var(--text-color);
} }
/* Контент */ /* Контент */
.content { .content {
background-color: var(--modal-background); background-color: var(--modal-background);
@ -34,6 +44,7 @@
/* Заголовки */ /* Заголовки */
h2 { h2 {
color: var(--text-color); color: var(--text-color);
text-align: center;
} }
p { p {

0
src/Style/DatePicker.css Normal file → Executable file
View File

0
src/Style/ErrorIndicator.css Normal file → Executable file
View File

0
src/Style/Expandable.css Normal file → Executable file
View File

40
src/Style/LoginModal.css Normal file → Executable file
View File

@ -4,7 +4,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); background: var(--modal-background);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -13,45 +13,63 @@
.modal { .modal {
background: var(--modal-background); background: var(--modal-background);
padding: 20px; padding: 20px;
/* padding-right: 3%; */
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* box-shadow: 0 0.3vh 2vh #1E1E1E; */
max-width: 400px; max-width: 400px;
width: 100%; width: 100%;
color: var(--modal-text);
} }
.modal h2 { .modal h2 {
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
font-size: 4vh;
color: var(--header-color);
} }
.modal label { .modal label {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
color: var(--modal-text); color: var(--modal-text);
font-size: larger;
font-weight: bolder;
padding-bottom: 1%;
} }
.modal input { .modal input {
width: 100%; /* width: 100%; */
padding: 8px; /* max-width: fit-content; */
margin-bottom: 10px; /* padding: 3%;
border: 1px solid #ccc; padding-top: 3%;
border-radius: 4px; padding-bottom: 3%;
margin-bottom: 10px; */
/* border: 1px solid #ccc; */
/* text-align: start; */
/* border-radius: 4px; */
/* font-size: larger; */
background-color: var(--modal-background); background-color: var(--modal-background);
color: var(--modal-text); color: var(--modal-text);
} }
.modal button { .modal button {
padding: 10px 20px; /* padding: 10px 20px; */
margin-top: 5vh;
margin-bottom: 5px; margin-bottom: 5px;
background: var(--accent-color); background: var(--modal--btn-background);
color: var(--text-color); color: var(--text-color);
border: none; border: none;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 0.3vh 1vh #2c2c2c;
cursor: pointer; cursor: pointer;
width: 100%;
padding-top: 4%;
padding-bottom: 4%;
transition: 0.2s;
} }
.modal button:hover { .modal button:hover {
background: var(--accent-hover-color); background: var(--hover-button);
color: var(--hover-text-color);
} }
.error { .error {

48
src/Style/SidebarMenu.css Normal file → Executable file
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,23 +19,25 @@
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px;
/* Отступ справа для скроллбара */
} }
/* Заголовок меню */ /* Заголовок меню */
.sidebar-title { .sidebar-title {
margin-bottom: 20px; margin-bottom: 20px;
font-size: 18px; font-size: 1.5em;
font-weight: bold; font-weight: bold;
color: var(--sidebar-text-color); color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
padding: 10px; padding: 10px;
text-align: center;
/* font-size: 2vh; */
} }
/* Элементы меню */ /* Элементы меню */
.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 +58,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 +112,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 +137,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;

2
src/Style/SystemStatusTable.css Normal file → Executable file
View File

@ -48,7 +48,7 @@ button {
} }
button:hover { button:hover {
background-color: #0056b3; background-color: #000000;
} }
caption { caption {

0
src/Style/TreeChart.css Normal file → Executable file
View File

22
src/Style/TreeTable.css Normal file → Executable file
View File

@ -1,6 +1,7 @@
.tree-table-container { .tree-table-container {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: hidden;
/* Убираем горизонтальный скролл */
} }
.tree-table { .tree-table {
@ -8,26 +9,33 @@
border-collapse: collapse; border-collapse: collapse;
text-align: center; text-align: center;
table-layout: fixed; table-layout: fixed;
/* Фиксированная ширина колонок */
background-color: var(--table-cell-background); background-color: var(--table-cell-background);
color: var(--table-text-color); color: var(--table-text-color);
/* Используем переменную для цвета текста */
} }
.tree-table-header { .tree-table-header {
padding: 10px; padding: 10px;
border: 1px solid var(--table-border); border: 1px solid black;
font-weight: bold; font-weight: bold;
white-space: nowrap; white-space: nowrap;
/* Текст не переносится */
overflow: hidden; overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis; text-overflow: ellipsis;
/* Добавляем многоточие */
background-color: var(--table-header-background); background-color: var(--table-header-background);
} }
.tree-table-cell { .tree-table-cell {
padding: 8px; padding: 8px;
border: 1px solid var(--table-border); border: 1px solid black;
white-space: nowrap; white-space: nowrap;
/* Текст не переносится */
overflow: hidden; overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие */
} }
.cell-content, .cell-content,
@ -40,6 +48,12 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.cell-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.status-indicator-bar { .status-indicator-bar {
width: 6px; width: 6px;
height: 20px; height: 20px;

0
src/Style/common.css Normal file → Executable file
View File

View File

@ -3,17 +3,23 @@
:root { :root {
--background-color: #1E1E1E; --background-color: #1E1E1E;
--text-color: #E0E0E0; --text-color: #E0E0E0;
--header-color: #FFFFFF;
/* Основной цвет текста (светлый) */ /* Основной цвет текста (светлый) */
--sidebar-color: #2d2d2d; --sidebar-color: #2d2d2d;
/* Темный цвет сайдбара */ /* Темный цвет сайдбара */
--sidebar-text-color: #E0E0E0; --sidebar-text-color: #E0E0E0;
/* Светлый текст в сайдбаре */ /* Светлый текст в сайдбаре */
--modal-background: #333333; --modal-background: #2d2d2d;
--modal--btn-background: #333333;
--modal-text: #FFFFFF; --modal-text: #FFFFFF;
--table-border: #444444; --table-border: #444444;
--table-header-background: #2d2d2d; --table-header-background: #2d2d2d;
--table-cell-background: #333333; --table-cell-background: #333333;
--table-text-color: #E0E0E0; --table-text-color: #E0E0E0;
/* Светлый текст в таблице */ /* Светлый текст в таблице */
--TreeChart-text-color: #ffffff;
--scrollbar-track-color: #333;
/* hover for buttons */
--hover-button: #333d4d;
} }
} }

View File

@ -1,17 +1,23 @@
/* Светлая тема по умолчанию */ /* Светлая тема по умолчанию */
:root { :root {
--background-color: #FFFFFF; --background-color: #FFFFFF;
--text-color: #333333; --text-color: #000000;
--header-color: #333333;
/* Основной цвет текста (черный) */ /* Основной цвет текста (черный) */
--sidebar-color: #3d74c7; --sidebar-color: #3d74c7;
/* Синий цвет сайдбара */ /* Синий цвет сайдбара */
--sidebar-text-color: #FFFFFF; --sidebar-text-color: #FFFFFF;
/* Белый текст в сайдбаре и вкладках */ /* Белый текст в сайдбаре и вкладках */
--modal-background: #FFFFFF; --modal-background: #FFFFFF;
--modal--btn-background: #0f55bec2;
--modal-text: #333333; --modal-text: #333333;
--table-border: #ddd; --table-border: #ddd;
--table-header-background: #f9f9f9; --table-header-background: #f9f9f9;
--table-cell-background: #FFFFFF; --table-cell-background: #FFFFFF;
--table-text-color: #000000; --table-text-color: #000000;
/* Черный текст в таблице */ /* Черный текст в таблице */
/* hover for buttons */
--hover-button: #2d62b1;
--hover-text-color: #FFFFFF
} }

73
src/Style/theme.jsx Normal file
View File

@ -0,0 +1,73 @@
import { createTheme } from "@mui/material/styles";
export const lightTheme = createTheme({
palette: {
mode: "light",
background: {
default: "#FFFFFF",
paper: "#FFFFFF",
},
text: {
primary: "#000000",
},
primary: {
main: "#3d74c7",
},
secondary: {
main: "#0f55bec2",
},
custom: {
background: "#FFFFFF",
text: "#000000",
sidebar: "#3d74c7",
sidebarText: "#FFFFFF",
modalBackground: "#FFFFFF",
modalBtnBackground: "#0f55bec2",
modalText: "#333333",
tableBorder: "#ddd",
tableHeaderBackground: "#f9f9f9",
tableCellBackground: "#FFFFFF",
tableText: "#000000",
treeChartText: "#000000",
scrollbarTrack: "#f1f1f1",
hoverButton: "#2d62b1",
hoverText: "#FFFFFF",
},
},
});
export const darkTheme = createTheme({
palette: {
mode: "dark",
background: {
default: "#1E1E1E",
paper: "#2d2d2d",
},
text: {
primary: "#E0E0E0",
},
primary: {
main: "#2d2d2d",
},
secondary: {
main: "#333333",
},
custom: {
background: "#1E1E1E",
text: "#E0E0E0",
sidebar: "#2d2d2d",
sidebarText: "#E0E0E0",
modalBackground: "#2d2d2d",
modalBtnBackground: "#333333",
modalText: "#FFFFFF",
tableBorder: "#444444",
tableHeaderBackground: "#2d2d2d",
tableCellBackground: "#333333",
tableText: "#E0E0E0",
treeChartText: "#FFFFFF",
scrollbarTrack: "#333",
hoverButton: "#333d4d",
hoverText: "#E0E0E0",
},
},
});

0
src/assets/images/critical.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

0
src/assets/images/warning.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

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;
/* Чуть темнее при наведении */
} }

View File

@ -2,8 +2,8 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import App from './App.jsx' import App from './App.jsx'
import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию //import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию
import './Style/dark-theme.css'; // Подключаем темную тему //import './Style/dark-theme.css'; // Подключаем темную тему
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>