Частично отрефакторил код, переделал sidebar menu и tabs с сипользованием MUI, обновил авторизацию
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details

pull/19/head
DmitriyA 2025-03-19 07:50:37 -04:00
parent cfaa31acee
commit a54b1d0c39
23 changed files with 639 additions and 519 deletions

9
.gitignore vendored
View File

@ -24,4 +24,11 @@ dist-ssr
*.sw?
*.les*
node_modules
node_modules
# Игнорировать .env файлы
.env
.env.local
.env.development
.env.production
.env.test

View File

@ -22,7 +22,8 @@
"react-datepicker": "^8.1.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7"
"@mui/material": "^6.4.7",
"@mui/icons-material": "^6.4.8"
},
"devDependencies": {
"@eslint/js": "^9.17.0",

View File

@ -1,26 +1,39 @@
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 LoginModal from "./Components/UI/LoginModal"; // Импортируем компонент авторизации
import "./Style/LoginModal.css"; // Импортируем стили
import LoginModal from "./Components/UI/LoginModal";
import { lightTheme, darkTheme } from "./Style/theme";
import "./Style/LoginModal.css";
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false); // Состояние авторизации
const [showLoginModal, setShowLoginModal] = useState(true); // Показывать ли модальное окно
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 handleLogin = () => {
setIsAuthenticated(true); // Устанавливаем авторизацию
setShowLoginModal(false); // Скрываем модальное окно
setIsAuthenticated(true);
setShowLoginModal(false);
};
return (
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
{!isAuthenticated && showLoginModal && (
<ThemeProvider theme={theme}>
<CssBaseline />
{!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>
)}
{isAuthenticated && <Dashboard />}
</div>
</ThemeProvider>
);
}
export default App;
export default App;

View File

@ -96,6 +96,11 @@ 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 || [];

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 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, setTabs] = useState([]);
const [activeTab, setActiveTab] = useState("Главная");
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
const { sidebarWidth, startResizing } = useSidebarResize(250);
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));
@ -56,106 +55,33 @@ 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>
<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>;
}
};
return (
<div className="dashboard-container">
<div
className="sidebar"
ref={sidebarRef}
style={{ width: sidebarWidth }}
>
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} />
<div
className="sidebar-resizer"
onMouseDown={startResizing}
/>
{/* Сайдбар */}
<div className="sidebar" style={{ width: sidebarWidth }}>
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} />
<div className="sidebar-resizer" onMouseDown={startResizing} />
</div>
<div className="main-content" style={{ marginLeft: sidebarWidth }}>
<Tabs
{/* Основной контент */}
<div className="main-content">
{/* Вкладки */}
<CustomTabs
tabs={tabs}
activeTab={activeTab}
onTabClick={(id) => setActiveTab(id)}
onTabClick={setActiveTab}
onCloseTab={handleCloseTab}
/>
{/* Контент вкладки */}
<div className="content">
{renderTabContent()}
<TabContent
activeTab={activeTab}
statusHistories={statusHistories}
treeData1={treeData1}
tabContent={tabContent}
handleOpenTab={handleOpenTab}
/>
</div>
</div>
</div>

119
src/Components/Layout/SidebarMenu.jsx Executable file → Normal file
View File

@ -1,86 +1,49 @@
import React, { useState } from "react";
import "../../Style/SidebarMenu.css";
import { ThemeProvider, createTheme, CssBaseline, Button } from "@mui/material";
import { getStatusColor } from "../TreeChart/dataUtils"; // Импортируем только нужную функцию
import React from "react";
import { Drawer, List } from "@mui/material";
import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
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();
onSelectItem(item);
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
const handleSelectItem = (id, title, children) => {
onOpenTab(id, title, children);
};
return (
<div className={`menu-item ${hasChildren ? "has-children" : ""}`} style={{ width: sidebarWidth - 20 }}>
<div
onClick={handleSingleClick}
className="menu-item-header"
>
{/* Круглый индикатор статуса */}
<div
className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`}
style={{ backgroundColor: statusColor }}
/>
{/* Текст элемента меню */}
<span>{item.title}</span>
<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>
{/* Иконки */}
{hasChildren && (
<div style={{ display: "flex", alignItems: "center" }}>
{/* Иконка для открытия родителя */}
<span
onClick={handleOpenParent}
className="open-parent-icon"
title="Открыть родителя"
>
📂
</span>
{/* Иконка для разворачивания/сворачивания */}
<span className="toggle-icon">
{isOpen ? "▲" : "▼"}
</span>
</div>
)}
</div>
{isOpen && hasChildren && (
<div className="submenu">
{item.items.map((child, index) => (
<MenuItem key={index} item={child} onSelectItem={onSelectItem} sidebarWidth={sidebarWidth} />
))}
</div>
)}
</div>
{/* Ресайзер */}
<div
onMouseDown={startResizing}
style={{
width: "5px",
cursor: "ew-resize",
backgroundColor: "#ccc",
height: "100%",
position: "absolute",
right: 0,
top: 0,
}}
/>
<SidebarFooter sidebarWidth={sidebarWidth} />
</Drawer>
);
};
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;

View File

@ -1,18 +1,8 @@
import React, { useState } from "react";
import Modal from "./Modal";
import "../../Style/LoginModal.css";
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Input from '@mui/material/Input';
import FilledInput from '@mui/material/FilledInput';
import OutlinedInput from '@mui/material/OutlinedInput';
import InputLabel from '@mui/material/InputLabel';
import InputAdornment from '@mui/material/InputAdornment';
import FormHelperText from '@mui/material/FormHelperText';
import FormControl from '@mui/material/FormControl';
import TextField from '@mui/material/TextField';
const LoginModal = ({ onLogin, onClose }) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@ -21,13 +11,31 @@ const LoginModal = ({ onLogin, onClose }) => {
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
if (username === "admin" && password === "admin") {
onLogin(); // Успешная авторизация
onClose(); // Закрыть модальное окно
} else {
setError("Неверный логин или пароль");
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("Ошибка при подключении к серверу");
}
};
@ -35,77 +43,28 @@ const LoginModal = ({ onLogin, onClose }) => {
<Modal onClose={onClose}>
<h2>Авторизация</h2>
<form onSubmit={handleSubmit}>
{/* <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> */}
{/* <Box
component="form"
sx={{ '& > :not(style)': { m: 1, width: '25ch' } }}
noValidate
autoComplete="off"
> */}
<TextField
fullWidth
id="user-login"
label="Логин"
variant="filled"
margin="normal"
required
onChange={(e) => setUsername(e.target.value)}
size="normal"
/>
<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"
/>
{/* <FormControl sx={{ m: 1, width: '25ch' }} variant="outlined">
<InputLabel htmlFor="outlined-adornment-password">Password</InputLabel>
<OutlinedInput
id="outlined-adornment-password"
type={showPassword ? 'text' : 'password'}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label={
showPassword ? 'hide the password' : 'display the password'
}
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
onMouseUp={handleMouseUpPassword}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
label="Password"
/>
</FormControl> */}
{/* </Box> */}
<TextField
fullWidth
id="user-password"
label="Пароль"
variant="filled"
margin="normal"
required
type={showPassword ? 'text' : 'password'}
onChange={(e) => setPassword(e.target.value)}
size="normal"
/>
{error && <p className="error">{error}</p>}
<button type="submit">Войти</button>

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;

View File

@ -27,12 +27,6 @@ const TreeTable = ({ data }) => {
}
};
useEffect(() => {
adjustFontSize();
window.addEventListener("resize", adjustFontSize);
return () => window.removeEventListener("resize", adjustFontSize);
}, [data]);
useEffect(() => {
const newLog = [];
const traverse = (items) => {
@ -41,7 +35,7 @@ const TreeTable = ({ data }) => {
newLog.push({
title: item.title,
status: item.status,
time: new Date().toLocaleTimeString() // Добавляем время
time: new Date().toLocaleTimeString(), // Добавляем время
});
}
if (item.items) {
@ -50,19 +44,31 @@ const TreeTable = ({ data }) => {
});
};
traverse(data.items);
setLog(newLog);
// Ограничиваем количество сообщений до 50
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
}, [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>
@ -70,41 +76,117 @@ const TreeTable = ({ data }) => {
});
};
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>
);
// Функция для отображения подзаголовков
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>
));
} else {
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="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
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>
</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 (
@ -118,15 +200,27 @@ 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>{renderRows(filteredData)}</tbody>
<tbody>
<tr className="tree-table-row">{renderData(filteredData)}</tr>
</tbody>
</table>
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
{isLogVisible ? "Скрыть лог" : "Показать лог"}

View File

@ -1,151 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import "../../Style/TreeTable.css";
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
const TreeTable = ({ data }) => {
const tableRef = useRef(null);
const [fontSize, setFontSize] = useState(16);
const [log, setLog] = useState([]);
const [isLogVisible, setIsLogVisible] = useState(true);
const adjustFontSize = () => {
if (tableRef.current) {
let newSize = 16;
const maxWidth = window.innerWidth;
while (tableRef.current.scrollWidth > maxWidth && newSize > 10) {
newSize -= 1;
tableRef.current.style.fontSize = `${newSize}px`;
}
while (tableRef.current.scrollWidth < maxWidth && newSize < 16) {
newSize += 1;
tableRef.current.style.fontSize = `${newSize}px`;
}
setFontSize(newSize);
}
};
useEffect(() => {
adjustFontSize();
window.addEventListener("resize", adjustFontSize);
return () => window.removeEventListener("resize", adjustFontSize);
}, [data]);
useEffect(() => {
const newLog = [];
const traverse = (items) => {
items.forEach((item) => {
if (["yellow", "orange", "red"].includes(item.status)) {
newLog.push({
title: item.title,
status: item.status,
time: new Date().toLocaleTimeString() // Добавляем время
});
}
if (item.items) {
traverse(item.items);
}
});
};
traverse(data.items);
setLog(newLog);
}, [data]);
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи");
const renderHeaders = (items) => {
return items.map((item) => {
// Если это последний уровень, не отображаем заголовок
if (!item.items || item.items.length === 0) {
return null;
}
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" }} />
{item.title}
</div>
</th>
);
});
};
const renderRows = (items) => {
if (!items || items.length === 0) return null;
// Если это последний уровень, не отображаем строки
if (items.every((item) => !item.items || item.items.length === 0)) {
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 null; // Не отображаем элементы последнего уровня
}
})}
</tr>
);
};
return (
<div className="tree-table-container">
<table ref={tableRef} className="tree-table" style={{ fontSize: `${fontSize}px` }}>
<thead>
<tr>
<th
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)}
className="tree-table-header"
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" }} />
{data.title}
</div>
</th>
</tr>
<tr>{renderHeaders(filteredData)}</tr>
</thead>
<tbody>{renderRows(filteredData)}</tbody>
</table>
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
{isLogVisible ? "Скрыть лог" : "Показать лог"}
</button>
{isLogVisible && (
<div className="status-log">
<h3>Лог статусов</h3>
<ul>
{log.map((entry, index) => (
<li key={index} style={{ color: statusManager1.getStatusColor(entry.status) }}>
[{entry.time}] {entry.status}: {entry.title}
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default TreeTable;

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;

View File

@ -2,24 +2,34 @@
.dashboard-container {
display: flex;
height: 100vh;
width: calc(100vw - 20px);
width: 100vw;
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: 1;
flex-grow: 1;
display: flex;
flex-direction: column;
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);

View File

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

View File

@ -1,31 +0,0 @@
import { createTheme, ThemeProvider } from "@mui/material/styles";
// Светлая тема
const lightTheme = createTheme({
palette: {
mode: "light",
background: {
default: "#FFFFFF",
paper: "#f4f4f4",
},
text: {
primary: "#000000",
secondary: "#333333",
},
},
});
// Темная тема
const darkTheme = createTheme({
palette: {
mode: "dark",
background: {
default: "#1E1E1E",
paper: "#2d2d2d",
},
text: {
primary: "#E0E0E0",
secondary: "#CCCCCC",
},
},
});

View File

@ -1,6 +1,7 @@
.tree-table-container {
width: 100%;
overflow-x: auto;
overflow-x: hidden;
/* Убираем горизонтальный скролл */
}
.tree-table {
@ -8,26 +9,33 @@
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 var(--table-border);
border: 1px solid black;
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 var(--table-border);
border: 1px solid black;
white-space: nowrap;
/* Текст не переносится */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие */
}
.cell-content,
@ -40,6 +48,12 @@
text-overflow: ellipsis;
}
.cell-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.status-indicator-bar {
width: 6px;
height: 20px;

0
src/Style/dark-theme.css Executable file → Normal file
View File

6
src/Style/light-theme.css Executable file → Normal file
View File

@ -1,7 +1,7 @@
/* Светлая тема по умолчанию */
:root {
--background-color: #FFFFFF;
--text-color: #FFFFFF;
--text-color: #000000;
--header-color: #333333;
/* Основной цвет текста (черный) */
--sidebar-color: #3d74c7;
@ -16,8 +16,8 @@
--table-cell-background: #FFFFFF;
--table-text-color: #000000;
/* Черный текст в таблице */
/* hover for buttons */
--hover-button: #2d62b1;
--hover-text-color : #FFFFFF
--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",
},
},
});

View File

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