Compare commits
No commits in common. "2713142c7d79f15a6d9528a4ea52d184e88fa0ed" and "3c60e9d14455ee2fb634d104ef1c374ee1c03b71" have entirely different histories.
2713142c7d
...
3c60e9d144
|
|
@ -24,11 +24,4 @@ dist-ssr
|
|||
*.sw?
|
||||
|
||||
*.les*
|
||||
node_modules
|
||||
|
||||
# Игнорировать .env файлы
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
node_modules
|
||||
25
index.html
25
index.html
|
|
@ -1,16 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/system_monitor_icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Модуль устойчивого функционирования</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/system_monitor_icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Модуль доверия</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,7 @@
|
|||
"chartjs-chart-box-and-violin-plot": "^4.0.0",
|
||||
"react-chartjs-2": "^5.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"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"
|
||||
"react-datepicker": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 729 B After Width: | Height: | Size: 729 B |
39
src/App.jsx
39
src/App.jsx
|
|
@ -1,39 +1,26 @@
|
|||
import React, { useState, useMemo } from "react";
|
||||
import { ThemeProvider, CssBaseline, Switch, Box } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import Dashboard from "./Components/Layout/Dashboard";
|
||||
import LoginModal from "./Components/UI/LoginModal";
|
||||
import { lightTheme, darkTheme } from "./Style/theme";
|
||||
import "./Style/LoginModal.css";
|
||||
import LoginModal from "./Components/UI/LoginModal"; // Импортируем компонент авторизации
|
||||
import "./Style/LoginModal.css"; // Импортируем стили
|
||||
|
||||
function App() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(true);
|
||||
const [isDarkMode, setIsDarkMode] = useState(
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
);
|
||||
|
||||
const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false); // Состояние авторизации
|
||||
const [showLoginModal, setShowLoginModal] = useState(true); // Показывать ли модальное окно
|
||||
|
||||
const handleLogin = () => {
|
||||
setIsAuthenticated(true);
|
||||
setShowLoginModal(false);
|
||||
setIsAuthenticated(true); // Устанавливаем авторизацию
|
||||
setShowLoginModal(false); // Скрываем модальное окно
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{!isAuthenticated && showLoginModal ? (
|
||||
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
|
||||
{!isAuthenticated && showLoginModal && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
|
@ -96,11 +96,6 @@ const PrometheusChart = ({ metricName }) => {
|
|||
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;
|
||||
let metrics = Array.isArray(result) ? result : result.data || [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,33 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import SidebarMenu from "./SidebarMenu";
|
||||
import TreeChart from "../TreeChart/TreeChart";
|
||||
import "../../Style/Dashboard.css";
|
||||
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
||||
import Tabs from "../UI/Tabs";
|
||||
import menuData from "../TreeChart/menuData.json";
|
||||
import TreeTable from "../UI/TreeTable";
|
||||
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
||||
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 { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
|
||||
const { sidebarWidth, startResizing } = useSidebarResize(250);
|
||||
const [tabs, setTabs] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState("Главная");
|
||||
const [tabContent, setTabContent] = useState({});
|
||||
const [treeData1, setTreeData1] = useState(menuData);
|
||||
const [treeData2, setTreeData2] = useState(menuData);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(250);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [statusHistories, setStatusHistories] = useState({
|
||||
history1: [],
|
||||
history2: [],
|
||||
});
|
||||
const sidebarRef = useRef(null);
|
||||
|
||||
// Генерация контента для вкладок
|
||||
useEffect(() => {
|
||||
const generatedTabContent = generateTabContent(menuData);
|
||||
setTabContent(generatedTabContent);
|
||||
}, []);
|
||||
|
||||
// Обновление статусов каждые 30 секунд
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
|
||||
|
|
@ -39,11 +40,11 @@ const Dashboard = () => {
|
|||
|
||||
setStatusHistories((prevHistories) => ({
|
||||
history1: [
|
||||
...prevHistories.history1.slice(-29),
|
||||
...prevHistories.history1.slice(-49),
|
||||
{ time: new Date().toLocaleTimeString(), status: statusPercentage1 },
|
||||
],
|
||||
history2: [
|
||||
...prevHistories.history2.slice(-29),
|
||||
...prevHistories.history2.slice(-49),
|
||||
{ time: new Date().toLocaleTimeString(), status: statusPercentage2 },
|
||||
],
|
||||
}));
|
||||
|
|
@ -55,33 +56,98 @@ const Dashboard = () => {
|
|||
return () => clearInterval(interval);
|
||||
}, [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 (
|
||||
<div className="dashboard-container">
|
||||
{/* Сайдбар */}
|
||||
<div className="sidebar" style={{ width: sidebarWidth }}>
|
||||
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} />
|
||||
<div className="sidebar-resizer" onMouseDown={startResizing} />
|
||||
<div
|
||||
className="sidebar"
|
||||
ref={sidebarRef}
|
||||
style={{ width: sidebarWidth }}
|
||||
>
|
||||
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} />
|
||||
<div
|
||||
className="sidebar-resizer"
|
||||
onMouseDown={startResizing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Основной контент */}
|
||||
<div className="main-content">
|
||||
{/* Вкладки */}
|
||||
<CustomTabs
|
||||
<div className="main-content" style={{ marginLeft: sidebarWidth }}>
|
||||
<Tabs
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabClick={setActiveTab}
|
||||
onTabClick={(id) => setActiveTab(id)}
|
||||
onCloseTab={handleCloseTab}
|
||||
/>
|
||||
|
||||
{/* Контент вкладки */}
|
||||
<div className="content">
|
||||
<TabContent
|
||||
activeTab={activeTab}
|
||||
statusHistories={statusHistories}
|
||||
treeData1={treeData1}
|
||||
tabContent={tabContent}
|
||||
handleOpenTab={handleOpenTab}
|
||||
/>
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,49 +1,82 @@
|
|||
import React from "react";
|
||||
import { Drawer, List } from "@mui/material";
|
||||
import MenuItem from "./SidebarMenuComponents/MenuItem";
|
||||
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
|
||||
import React, { useState } from "react";
|
||||
import "../../Style/SidebarMenu.css";
|
||||
import { getStatusColor } from "../TreeChart/dataUtils"; // Импортируем только нужную функцию
|
||||
|
||||
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
||||
const handleSelectItem = (id, title, children) => {
|
||||
onOpenTab(id, title, children);
|
||||
const MenuItem = ({ item, onSelectItem, sidebarWidth }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||
const statusColor = getStatusColor(item.status);
|
||||
|
||||
// Обработчик одинарного клика (разворачивание/сворачивание или открытие элемента)
|
||||
const handleSingleClick = () => {
|
||||
if (hasChildren) {
|
||||
setIsOpen(!isOpen); // Разворачиваем/сворачиваем дочерние элементы
|
||||
} else {
|
||||
onSelectItem(item); // Если нет потомков, открываем элемент как вкладку
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик клика для открытия родителя
|
||||
const handleOpenParent = (e) => {
|
||||
e.stopPropagation(); // Останавливаем всплытие события, чтобы не сработал handleSingleClick
|
||||
onSelectItem(item); // Открываем родителя
|
||||
};
|
||||
|
||||
return (
|
||||
<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 className="menu-item" style={{ width: sidebarWidth - 20 }}> {/* Динамическая ширина */}
|
||||
<div
|
||||
onMouseDown={startResizing}
|
||||
style={{
|
||||
width: "5px",
|
||||
cursor: "ew-resize",
|
||||
backgroundColor: "#ccc",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
/>
|
||||
onClick={handleSingleClick} // Одинарный клик для разворачивания/сворачивания или открытия
|
||||
className="menu-item-header"
|
||||
>
|
||||
{/* Круглый индикатор статуса */}
|
||||
<div
|
||||
className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`}
|
||||
style={{ backgroundColor: statusColor }}
|
||||
/>
|
||||
<span>{item.title}</span>
|
||||
|
||||
<SidebarFooter sidebarWidth={sidebarWidth} />
|
||||
</Drawer>
|
||||
{/* Иконка для открытия родителя */}
|
||||
{hasChildren && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarMenu;
|
||||
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;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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;
|
||||
|
|
@ -5,6 +5,7 @@ import { getStatusColor } from "./dataUtils";
|
|||
|
||||
const TreeChart = ({ data, onNodeClick }) => {
|
||||
const chartRef = useRef();
|
||||
const simulationRef = useRef(null);
|
||||
const nodePositions = useRef(new Map());
|
||||
|
||||
const { root, nodes, links } = useMemo(() => {
|
||||
|
|
@ -22,63 +23,22 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
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) => {
|
||||
const prev = nodePositions.current.get(node.data.id);
|
||||
if (prev) {
|
||||
node.x = prev.x;
|
||||
node.y = prev.y;
|
||||
node.fx = prev.fx ?? null;
|
||||
node.fy = prev.fy ?? null;
|
||||
} else {
|
||||
if (node.depth === 0) {
|
||||
// Центральный узел
|
||||
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;
|
||||
}
|
||||
const parent = node.parent;
|
||||
node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000;
|
||||
node.y = parent ? parent.y + Math.random() * 50 - 25 : Math.random() * 1000;
|
||||
}
|
||||
nodePositions.current.set(node.data.id, { x: node.x, y: node.y });
|
||||
nodePositions.current.set(node.data.id, { x: node.x, y: node.y, fx: node.fx, fy: node.fy });
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
return { root, nodes, links };
|
||||
}, [data]);
|
||||
|
||||
|
|
@ -88,33 +48,55 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
const svg = d3.select(chartRef.current)
|
||||
.attr("width", 2000)
|
||||
.attr("height", 2000)
|
||||
.attr("viewBox", [-500, -500, 1500, 1500])
|
||||
.attr("viewBox", [-500, -500, 1000, 1000])
|
||||
.attr("style", "max-width: 100%; height: auto;");
|
||||
|
||||
svg.append("g").attr("class", "links");
|
||||
svg.append("g").attr("class", "nodes");
|
||||
svg.append("g").attr("class", "labels");
|
||||
|
||||
// Очищаем предыдущие элементы
|
||||
svg.selectAll(".links line").remove();
|
||||
svg.selectAll(".nodes circle").remove();
|
||||
svg.selectAll(".labels text").remove();
|
||||
// Инициализация симуляции
|
||||
simulationRef.current = d3.forceSimulation()
|
||||
.force("link", d3.forceLink().id((d) => d.data.id).distance(80).strength(1))
|
||||
.force("charge", d3.forceManyBody().strength(-200))
|
||||
.force("center", d3.forceCenter(0, 0))
|
||||
.force("collision", d3.forceCollide().radius(20))
|
||||
.force("x", d3.forceX(0).strength(0.05))
|
||||
.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 nodeGroup = svg.select(".nodes");
|
||||
const labelGroup = svg.select(".labels");
|
||||
|
||||
// Обновляем связи
|
||||
const link = linkGroup
|
||||
.selectAll("line")
|
||||
.data(links, (d) => `${d.source.data.id}-${d.target.data.id}`)
|
||||
.join("line")
|
||||
.attr("stroke", "#999")
|
||||
.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);
|
||||
.attr("stroke-opacity", 0.6);
|
||||
|
||||
// Рисуем узлы
|
||||
const nodeGroup = svg.select(".nodes");
|
||||
// Обновляем узлы
|
||||
const node = nodeGroup
|
||||
.selectAll("circle")
|
||||
.data(nodes, (d) => d.data.id)
|
||||
|
|
@ -122,8 +104,6 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
.attr("fill", (d) => getStatusColor(d.data.status))
|
||||
.attr("stroke", "#fff")
|
||||
.attr("r", 7)
|
||||
.attr("cx", (d) => d.x)
|
||||
.attr("cy", (d) => d.y)
|
||||
.call(drag());
|
||||
|
||||
node.on("click", (event, d) => {
|
||||
|
|
@ -132,8 +112,7 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Рисуем текстовые метки
|
||||
const labelGroup = svg.select(".labels");
|
||||
// Обновляем текстовые метки
|
||||
const text = labelGroup
|
||||
.selectAll("text")
|
||||
.data(nodes, (d) => d.data.id)
|
||||
|
|
@ -142,42 +121,46 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
.attr("dx", 12)
|
||||
.attr("dy", 4)
|
||||
.style("user-select", "none") // Запрет выделения текста
|
||||
.style("pointer-events", "none") // Запрет взаимодействия с текстом
|
||||
.style("fill", "var(--TreeChart-text-color)") // Используем переменную для цвета текста
|
||||
.attr("x", (d) => d.x + 12)
|
||||
.attr("y", (d) => d.y + 4);
|
||||
.style("pointer-events", "none"); // Запрет взаимодействия с текстом
|
||||
|
||||
// Обновляем симуляцию
|
||||
simulationRef.current.nodes(nodes);
|
||||
simulationRef.current.force("link").links(links);
|
||||
simulationRef.current.alphaTarget(0.1).restart();
|
||||
|
||||
simulationRef.current.on("tick", () => {
|
||||
link
|
||||
.attr("x1", (d) => d.source.x)
|
||||
.attr("y1", (d) => d.source.y)
|
||||
.attr("x2", (d) => d.target.x)
|
||||
.attr("y2", (d) => d.target.y);
|
||||
|
||||
node
|
||||
.attr("cx", (d) => d.x)
|
||||
.attr("cy", (d) => d.y);
|
||||
|
||||
text
|
||||
.attr("x", (d) => d.x + 12)
|
||||
.attr("y", (d) => d.y + 4);
|
||||
});
|
||||
|
||||
}, [root, links, nodes, onNodeClick]);
|
||||
|
||||
const drag = () => {
|
||||
function dragstarted(event, d) {
|
||||
d3.select(this).raise().attr("stroke", "#000");
|
||||
if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.x = event.x;
|
||||
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);
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
d3.select(this).attr("stroke", "#fff");
|
||||
nodePositions.current.set(d.data.id, { x: d.x, y: d.y });
|
||||
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 });
|
||||
}
|
||||
|
||||
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
|
||||
|
|
@ -186,4 +169,4 @@ const TreeChart = ({ data, onNodeClick }) => {
|
|||
return <svg ref={chartRef} />;
|
||||
};
|
||||
|
||||
export default TreeChart;
|
||||
export default TreeChart;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,714 +0,0 @@
|
|||
{
|
||||
"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": "ПО"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,41 +1,19 @@
|
|||
import React, { useState } from "react";
|
||||
import Modal from "./Modal";
|
||||
import "../../Style/LoginModal.css";
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
||||
const LoginModal = ({ onLogin, onClose }) => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// Отправляем данные на бэкенд
|
||||
console.log("Отправляем данные:", { username, password });
|
||||
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("Ошибка при подключении к серверу");
|
||||
if (username === "admin" && password === "admin") {
|
||||
onLogin(); // Успешная авторизация
|
||||
onClose(); // Закрыть модальное окно
|
||||
} else {
|
||||
setError("Неверный логин или пароль");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -43,29 +21,24 @@ const LoginModal = ({ onLogin, onClose }) => {
|
|||
<Modal onClose={onClose}>
|
||||
<h2>Авторизация</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="user-login"
|
||||
label="Логин"
|
||||
variant="filled"
|
||||
margin="normal"
|
||||
required
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
size="normal"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="user-password"
|
||||
label="Пароль"
|
||||
variant="filled"
|
||||
margin="normal"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
size="normal"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label>Логин:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Пароль:</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<button type="submit">Войти</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
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;
|
||||
|
|
@ -27,6 +27,12 @@ const TreeTable = ({ data }) => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
adjustFontSize();
|
||||
window.addEventListener("resize", adjustFontSize);
|
||||
return () => window.removeEventListener("resize", adjustFontSize);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const newLog = [];
|
||||
const traverse = (items) => {
|
||||
|
|
@ -35,7 +41,7 @@ const TreeTable = ({ data }) => {
|
|||
newLog.push({
|
||||
title: item.title,
|
||||
status: item.status,
|
||||
time: new Date().toLocaleTimeString(), // Добавляем время
|
||||
time: new Date().toLocaleTimeString() // Добавляем время
|
||||
});
|
||||
}
|
||||
if (item.items) {
|
||||
|
|
@ -44,31 +50,19 @@ const TreeTable = ({ data }) => {
|
|||
});
|
||||
};
|
||||
traverse(data.items);
|
||||
|
||||
// Ограничиваем количество сообщений до 50
|
||||
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
|
||||
setLog(newLog);
|
||||
}, [data]);
|
||||
|
||||
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи");
|
||||
|
||||
// Функция для отображения заголовков
|
||||
const renderHeaders = (items) => {
|
||||
return items.map((item) => {
|
||||
const colSpan = item.items ? item.items.length : 1;
|
||||
return (
|
||||
<th key={item.id} colSpan={colSpan} 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",
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
|
|
@ -76,117 +70,41 @@ const TreeTable = ({ data }) => {
|
|||
});
|
||||
};
|
||||
|
||||
// Функция для отображения подзаголовков
|
||||
const renderSubHeaders = (items) => {
|
||||
return items.map((item) => {
|
||||
if (item.items) {
|
||||
return item.items.map((child) => (
|
||||
<th key={child.id} className="tree-table-header" title={child.title}>
|
||||
<div className="header-content">
|
||||
<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}
|
||||
</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>
|
||||
));
|
||||
const renderRows = (items) => {
|
||||
if (!items || items.length === 0) return null;
|
||||
const hasChildren = items.some((item) => item.items && item.items.length > 0);
|
||||
if (!hasChildren) return null;
|
||||
return (
|
||||
<tr className="tree-table-row">
|
||||
{items.map((item) => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
{item.items.map((child) => (
|
||||
<td key={child.id} className="tree-table-cell" title={child.title}>
|
||||
<div className="cell-content">
|
||||
<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}
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td key={child.id} className="tree-table-cell" title={child.title}>
|
||||
<td key={item.id} className="tree-table-cell" title={item.title}>
|
||||
<div className="cell-content">
|
||||
<div
|
||||
className="status-indicator-bar"
|
||||
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 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>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
});
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
});
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -200,27 +118,15 @@ const TreeTable = ({ data }) => {
|
|||
title={data.title}
|
||||
>
|
||||
<div className="header-content">
|
||||
<div
|
||||
className="status-indicator-bar"
|
||||
style={{ backgroundColor: statusManager1.getStatusColor(data.status) }}
|
||||
/>
|
||||
<div
|
||||
className="status-indicator-bar"
|
||||
style={{
|
||||
backgroundColor: statusManager2.getStatusColor(data.status),
|
||||
marginLeft: "5px",
|
||||
}}
|
||||
/>
|
||||
<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}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>{renderHeaders(filteredData)}</tr>
|
||||
<tr>{renderSubHeaders(filteredData)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="tree-table-row">{renderData(filteredData)}</tr>
|
||||
</tbody>
|
||||
<tbody>{renderRows(filteredData)}</tbody>
|
||||
</table>
|
||||
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
|
||||
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
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;
|
||||
|
|
@ -2,34 +2,24 @@
|
|||
.dashboard-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
width: calc(100vw - 20px);
|
||||
overflow: hidden;
|
||||
margin-left: 20px;
|
||||
background-color: var(--background-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 {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
margin-left: 50px;
|
||||
transition: margin-left 0.2s ease;
|
||||
overflow: auto;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
|
||||
/* Контент */
|
||||
.content {
|
||||
background-color: var(--modal-background);
|
||||
|
|
@ -44,7 +34,6 @@
|
|||
/* Заголовки */
|
||||
h2 {
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--modal-background);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -13,63 +13,45 @@
|
|||
.modal {
|
||||
background: var(--modal-background);
|
||||
padding: 20px;
|
||||
/* padding-right: 3%; */
|
||||
border-radius: 8px;
|
||||
/* box-shadow: 0 0.3vh 2vh #1E1E1E; */
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
color: var(--modal-text);
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 4vh;
|
||||
color: var(--header-color);
|
||||
}
|
||||
|
||||
.modal label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: var(--modal-text);
|
||||
font-size: larger;
|
||||
font-weight: bolder;
|
||||
padding-bottom: 1%;
|
||||
}
|
||||
|
||||
.modal input {
|
||||
/* width: 100%; */
|
||||
/* max-width: fit-content; */
|
||||
/* padding: 3%;
|
||||
padding-top: 3%;
|
||||
padding-bottom: 3%;
|
||||
margin-bottom: 10px; */
|
||||
/* border: 1px solid #ccc; */
|
||||
/* text-align: start; */
|
||||
/* border-radius: 4px; */
|
||||
/* font-size: larger; */
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: var(--modal-background);
|
||||
color: var(--modal-text);
|
||||
}
|
||||
|
||||
.modal button {
|
||||
/* padding: 10px 20px; */
|
||||
margin-top: 5vh;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 5px;
|
||||
background: var(--modal--btn-background);
|
||||
background: var(--accent-color);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0.3vh 1vh #2c2c2c;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding-top: 4%;
|
||||
padding-bottom: 4%;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.modal button:hover {
|
||||
background: var(--hover-button);
|
||||
color: var(--hover-text-color);
|
||||
background: var(--accent-hover-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
height: 100vh;
|
||||
background-color: var(--sidebar-color);
|
||||
color: var(--sidebar-text-color);
|
||||
/* Используем переменную для цвета текста */
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
|
@ -19,25 +20,23 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
/* Отступ справа для скроллбара */
|
||||
}
|
||||
|
||||
/* Заголовок меню */
|
||||
.sidebar-title {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--sidebar-text-color);
|
||||
/* Используем переменную для цвета текста */
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
/* font-size: 2vh; */
|
||||
}
|
||||
|
||||
/* Элементы меню */
|
||||
.menu-item {
|
||||
margin-bottom: 10px;
|
||||
color: var(--sidebar-text-color);
|
||||
/* Используем переменную для цвета текста */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -58,42 +57,13 @@
|
|||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Стили для заголовка элемента меню */
|
||||
.menu-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* Распределяем пространство между элементами */
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
|
|
@ -112,18 +82,9 @@
|
|||
/* Подменю */
|
||||
.submenu {
|
||||
margin-left: 20px;
|
||||
/* Отступ слева для вложенных элементов */
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Стили для элементов нижнего уровня вложенности */
|
||||
|
||||
/* Дополнительные отступы для элементов без иконок */
|
||||
.menu-item:not(.has-children) .menu-item-header {
|
||||
padding-right: 25px;
|
||||
/* Добавляем отступ справа для элементов без иконок */
|
||||
}
|
||||
|
||||
/* Футер сайдбара */
|
||||
.sidebar-footer {
|
||||
padding: 10px;
|
||||
|
|
@ -137,6 +98,7 @@
|
|||
.help,
|
||||
.settings {
|
||||
color: var(--sidebar-text-color);
|
||||
/* Используем переменную для цвета текста */
|
||||
margin: 5px 0;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ button {
|
|||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #000000;
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
caption {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
.tree-table-container {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
/* Убираем горизонтальный скролл */
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tree-table {
|
||||
|
|
@ -9,33 +8,26 @@
|
|||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
table-layout: fixed;
|
||||
/* Фиксированная ширина колонок */
|
||||
background-color: var(--table-cell-background);
|
||||
color: var(--table-text-color);
|
||||
/* Используем переменную для цвета текста */
|
||||
}
|
||||
|
||||
.tree-table-header {
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
border: 1px solid var(--table-border);
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
/* Текст не переносится */
|
||||
overflow: hidden;
|
||||
/* Скрываем текст, который не помещается */
|
||||
text-overflow: ellipsis;
|
||||
/* Добавляем многоточие */
|
||||
background-color: var(--table-header-background);
|
||||
}
|
||||
|
||||
.tree-table-cell {
|
||||
padding: 8px;
|
||||
border: 1px solid black;
|
||||
border: 1px solid var(--table-border);
|
||||
white-space: nowrap;
|
||||
/* Текст не переносится */
|
||||
overflow: hidden;
|
||||
/* Скрываем текст, который не помещается */
|
||||
text-overflow: ellipsis;
|
||||
/* Добавляем многоточие */
|
||||
}
|
||||
|
||||
.cell-content,
|
||||
|
|
@ -48,12 +40,6 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cell-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-indicator-bar {
|
||||
width: 6px;
|
||||
height: 20px;
|
||||
|
|
|
|||
|
|
@ -3,23 +3,17 @@
|
|||
:root {
|
||||
--background-color: #1E1E1E;
|
||||
--text-color: #E0E0E0;
|
||||
--header-color: #FFFFFF;
|
||||
/* Основной цвет текста (светлый) */
|
||||
--sidebar-color: #2d2d2d;
|
||||
/* Темный цвет сайдбара */
|
||||
--sidebar-text-color: #E0E0E0;
|
||||
/* Светлый текст в сайдбаре */
|
||||
--modal-background: #2d2d2d;
|
||||
--modal--btn-background: #333333;
|
||||
--modal-background: #333333;
|
||||
--modal-text: #FFFFFF;
|
||||
--table-border: #444444;
|
||||
--table-header-background: #2d2d2d;
|
||||
--table-cell-background: #333333;
|
||||
--table-text-color: #E0E0E0;
|
||||
/* Светлый текст в таблице */
|
||||
--TreeChart-text-color: #ffffff;
|
||||
--scrollbar-track-color: #333;
|
||||
/* hover for buttons */
|
||||
--hover-button: #333d4d;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,17 @@
|
|||
/* Светлая тема по умолчанию */
|
||||
:root {
|
||||
--background-color: #FFFFFF;
|
||||
--text-color: #000000;
|
||||
--header-color: #333333;
|
||||
--text-color: #333333;
|
||||
/* Основной цвет текста (черный) */
|
||||
--sidebar-color: #3d74c7;
|
||||
/* Синий цвет сайдбара */
|
||||
--sidebar-text-color: #FFFFFF;
|
||||
/* Белый текст в сайдбаре и вкладках */
|
||||
--modal-background: #FFFFFF;
|
||||
--modal--btn-background: #0f55bec2;
|
||||
--modal-text: #333333;
|
||||
--table-border: #ddd;
|
||||
--table-header-background: #f9f9f9;
|
||||
--table-cell-background: #FFFFFF;
|
||||
--table-text-color: #000000;
|
||||
/* Черный текст в таблице */
|
||||
|
||||
/* hover for buttons */
|
||||
--hover-button: #2d62b1;
|
||||
--hover-text-color: #FFFFFF
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -75,32 +75,24 @@ button:focus-visible {
|
|||
|
||||
/* Глобальный стиль для WebKit-браузеров (Chrome, Edge, Safari) */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
/* Толщина вертикального скролла */
|
||||
height: 10px;
|
||||
/* Толщина горизонтального скролла */
|
||||
width: 10px; /* Толщина вертикального скролла */
|
||||
height: 10px; /* Толщина горизонтального скролла */
|
||||
}
|
||||
|
||||
/* Фон скроллбара */
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track-color, #f1f1f1);
|
||||
/* Цвет фона */
|
||||
border-radius: 10px;
|
||||
/* Скругление углов */
|
||||
background: #f1f1f1; /* Цвет фона */
|
||||
border-radius: 10px; /* Скругление углов */
|
||||
}
|
||||
|
||||
/* Ползунок */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3d74c7;
|
||||
/* Основной цвет */
|
||||
border-radius: 10px;
|
||||
/* Скругляем края */
|
||||
border: 1px solid #1c36c9;
|
||||
/* Белая обводка */
|
||||
background: #3d74c7; /* Основной цвет */
|
||||
border-radius: 10px; /* Скругляем края */
|
||||
border: 1px solid #1c36c9; /* Белая обводка */
|
||||
}
|
||||
|
||||
/* Эффект при наведении */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #2b5aa5;
|
||||
/* Чуть темнее при наведении */
|
||||
background: #2b5aa5; /* Чуть темнее при наведении */
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ import { StrictMode } from 'react'
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
//import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию
|
||||
//import './Style/dark-theme.css'; // Подключаем темную тему
|
||||
import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию
|
||||
import './Style/dark-theme.css'; // Подключаем темную тему
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
|
|
|
|||
Loading…
Reference in New Issue