Частично отрефакторил код, переделал sidebar menu и tabs с сипользованием MUI, обновил авторизацию
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
parent
cfaa31acee
commit
a54b1d0c39
|
|
@ -25,3 +25,10 @@ dist-ssr
|
||||||
|
|
||||||
*.les*
|
*.les*
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Игнорировать .env файлы
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.production
|
||||||
|
.env.test
|
||||||
|
|
@ -22,7 +22,8 @@
|
||||||
"react-datepicker": "^8.1.0",
|
"react-datepicker": "^8.1.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|
|
||||||
37
src/App.jsx
37
src/App.jsx
|
|
@ -1,25 +1,38 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { ThemeProvider, CssBaseline, Switch, Box } from "@mui/material";
|
||||||
import Dashboard from "./Components/Layout/Dashboard";
|
import Dashboard from "./Components/Layout/Dashboard";
|
||||||
import LoginModal from "./Components/UI/LoginModal"; // Импортируем компонент авторизации
|
import LoginModal from "./Components/UI/LoginModal";
|
||||||
import "./Style/LoginModal.css"; // Импортируем стили
|
import { lightTheme, darkTheme } from "./Style/theme";
|
||||||
|
import "./Style/LoginModal.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false); // Состояние авторизации
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [showLoginModal, setShowLoginModal] = useState(true); // Показывать ли модальное окно
|
const [showLoginModal, setShowLoginModal] = useState(true);
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
);
|
||||||
|
|
||||||
|
const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]);
|
||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
setIsAuthenticated(true); // Устанавливаем авторизацию
|
setIsAuthenticated(true);
|
||||||
setShowLoginModal(false); // Скрываем модальное окно
|
setShowLoginModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
|
<ThemeProvider theme={theme}>
|
||||||
{!isAuthenticated && showLoginModal && (
|
<CssBaseline />
|
||||||
|
{!isAuthenticated && showLoginModal ? (
|
||||||
<LoginModal onLogin={handleLogin} onClose={() => setShowLoginModal(false)} />
|
<LoginModal onLogin={handleLogin} onClose={() => setShowLoginModal(false)} />
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: "flex", height: "100vh", overflow: "hidden", bgcolor: "background.default", color: "text.primary" }}>
|
||||||
|
<Dashboard />
|
||||||
|
<Box sx={{ position: "absolute", top: 10, right: 10 }}>
|
||||||
|
<Switch checked={isDarkMode} onChange={() => setIsDarkMode((prev) => !prev)} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</ThemeProvider>
|
||||||
{isAuthenticated && <Dashboard />}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,11 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
params: { metric: metricName, start, end, step },
|
params: { metric: metricName, start, end, step },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
const response = await axios.get(`${process.env.REACT_APP_BACK_URL}/metrics`, {
|
||||||
|
params: { metric: metricName, start, end, step },
|
||||||
|
}); */
|
||||||
|
|
||||||
const result = response.data;
|
const result = response.data;
|
||||||
let metrics = Array.isArray(result) ? result : result.data || [];
|
let metrics = Array.isArray(result) ? result : result.data || [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,32 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import SidebarMenu from "./SidebarMenu";
|
import SidebarMenu from "./SidebarMenu";
|
||||||
import TreeChart from "../TreeChart/TreeChart";
|
|
||||||
import "../../Style/Dashboard.css";
|
import "../../Style/Dashboard.css";
|
||||||
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
|
||||||
import Tabs from "../UI/Tabs";
|
|
||||||
import menuData from "../TreeChart/menuData.json";
|
|
||||||
import TreeTable from "../UI/TreeTable";
|
|
||||||
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
||||||
import generateTabContent from "../TreeChart/tabContent";
|
import generateTabContent from "../TreeChart/tabContent";
|
||||||
|
import CustomTabs from "../UI/MUItabs";
|
||||||
|
import useTabs from "../hooks/useTabs";
|
||||||
|
import useSidebarResize from "../hooks/useSidebarResize";
|
||||||
|
import TabContent from "../hooks/TabContent";
|
||||||
|
import menuData from "../TreeChart/menuData.json";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [tabs, setTabs] = useState([]);
|
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
|
||||||
const [activeTab, setActiveTab] = useState("Главная");
|
const { sidebarWidth, startResizing } = useSidebarResize(250);
|
||||||
const [tabContent, setTabContent] = useState({});
|
const [tabContent, setTabContent] = useState({});
|
||||||
const [treeData1, setTreeData1] = useState(menuData);
|
const [treeData1, setTreeData1] = useState(menuData);
|
||||||
const [treeData2, setTreeData2] = useState(menuData);
|
const [treeData2, setTreeData2] = useState(menuData);
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(250);
|
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
|
||||||
const [statusHistories, setStatusHistories] = useState({
|
const [statusHistories, setStatusHistories] = useState({
|
||||||
history1: [],
|
history1: [],
|
||||||
history2: [],
|
history2: [],
|
||||||
});
|
});
|
||||||
const sidebarRef = useRef(null);
|
|
||||||
|
|
||||||
|
// Генерация контента для вкладок
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generatedTabContent = generateTabContent(menuData);
|
const generatedTabContent = generateTabContent(menuData);
|
||||||
setTabContent(generatedTabContent);
|
setTabContent(generatedTabContent);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Обновление статусов каждые 30 секунд
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
|
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
|
||||||
|
|
@ -56,106 +55,33 @@ const Dashboard = () => {
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [treeData1, treeData2]);
|
}, [treeData1, treeData2]);
|
||||||
|
|
||||||
const startResizing = useCallback((e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsResizing(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const resize = useCallback((e) => {
|
|
||||||
if (isResizing) {
|
|
||||||
const newWidth = e.clientX;
|
|
||||||
if (newWidth > 100 && newWidth < 400) {
|
|
||||||
setSidebarWidth(newWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isResizing]);
|
|
||||||
|
|
||||||
const stopResizing = useCallback(() => {
|
|
||||||
setIsResizing(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseMove = (e) => resize(e);
|
|
||||||
const handleMouseUp = () => stopResizing();
|
|
||||||
|
|
||||||
if (isResizing) {
|
|
||||||
window.addEventListener("mousemove", handleMouseMove);
|
|
||||||
window.addEventListener("mouseup", handleMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
window.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
};
|
|
||||||
}, [isResizing, resize, stopResizing]);
|
|
||||||
|
|
||||||
const handleOpenTab = (id, title) => {
|
|
||||||
if (!tabs.some((tab) => tab.id === id)) {
|
|
||||||
setTabs([...tabs, { id, title }]);
|
|
||||||
}
|
|
||||||
setActiveTab(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseTab = (id) => {
|
|
||||||
const newTabs = tabs.filter((tab) => tab.id !== id);
|
|
||||||
setTabs(newTabs);
|
|
||||||
if (activeTab === id) {
|
|
||||||
setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1].id : "Главная");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabContent = () => {
|
|
||||||
if (activeTab === "Главная") {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Общий мониторинг состояния системы</h2>
|
|
||||||
|
|
||||||
<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 (
|
return (
|
||||||
<div className="dashboard-container">
|
<div className="dashboard-container">
|
||||||
<div
|
{/* Сайдбар */}
|
||||||
className="sidebar"
|
<div className="sidebar" style={{ width: sidebarWidth }}>
|
||||||
ref={sidebarRef}
|
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} />
|
||||||
style={{ width: sidebarWidth }}
|
<div className="sidebar-resizer" onMouseDown={startResizing} />
|
||||||
>
|
|
||||||
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} />
|
|
||||||
<div
|
|
||||||
className="sidebar-resizer"
|
|
||||||
onMouseDown={startResizing}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main-content" style={{ marginLeft: sidebarWidth }}>
|
{/* Основной контент */}
|
||||||
<Tabs
|
<div className="main-content">
|
||||||
|
{/* Вкладки */}
|
||||||
|
<CustomTabs
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
onTabClick={(id) => setActiveTab(id)}
|
onTabClick={setActiveTab}
|
||||||
onCloseTab={handleCloseTab}
|
onCloseTab={handleCloseTab}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Контент вкладки */}
|
||||||
<div className="content">
|
<div className="content">
|
||||||
{renderTabContent()}
|
<TabContent
|
||||||
|
activeTab={activeTab}
|
||||||
|
statusHistories={statusHistories}
|
||||||
|
treeData1={treeData1}
|
||||||
|
tabContent={tabContent}
|
||||||
|
handleOpenTab={handleOpenTab}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,49 @@
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import "../../Style/SidebarMenu.css";
|
import { Drawer, List } from "@mui/material";
|
||||||
import { ThemeProvider, createTheme, CssBaseline, Button } from "@mui/material";
|
import MenuItem from "./SidebarMenuComponents/MenuItem";
|
||||||
import { getStatusColor } from "../TreeChart/dataUtils"; // Импортируем только нужную функцию
|
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
|
||||||
|
|
||||||
const MenuItem = ({ item, onSelectItem, sidebarWidth }) => {
|
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const handleSelectItem = (id, title, children) => {
|
||||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
onOpenTab(id, title, children);
|
||||||
const statusColor = getStatusColor(item.status);
|
|
||||||
|
|
||||||
const handleSingleClick = () => {
|
|
||||||
if (hasChildren) {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
} else {
|
|
||||||
onSelectItem(item);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenParent = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSelectItem(item);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`menu-item ${hasChildren ? "has-children" : ""}`} style={{ width: sidebarWidth - 20 }}>
|
<Drawer
|
||||||
<div
|
variant="permanent"
|
||||||
onClick={handleSingleClick}
|
sx={{
|
||||||
className="menu-item-header"
|
width: sidebarWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
"& .MuiDrawer-paper": {
|
||||||
|
width: sidebarWidth,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Круглый индикатор статуса */}
|
<List>
|
||||||
|
<h2 style={{ padding: "16px", fontWeight: "bold" }}>Меню</h2>
|
||||||
|
<MenuItem item={data} onSelectItem={handleSelectItem} />
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{/* Ресайзер */}
|
||||||
<div
|
<div
|
||||||
className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`}
|
onMouseDown={startResizing}
|
||||||
style={{ backgroundColor: statusColor }}
|
style={{
|
||||||
|
width: "5px",
|
||||||
|
cursor: "ew-resize",
|
||||||
|
backgroundColor: "#ccc",
|
||||||
|
height: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Текст элемента меню */}
|
|
||||||
<span>{item.title}</span>
|
|
||||||
|
|
||||||
{/* Иконки */}
|
<SidebarFooter sidebarWidth={sidebarWidth} />
|
||||||
{hasChildren && (
|
</Drawer>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,18 +1,8 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import "../../Style/LoginModal.css";
|
import "../../Style/LoginModal.css";
|
||||||
import 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';
|
import TextField from '@mui/material/TextField';
|
||||||
|
|
||||||
|
|
||||||
const LoginModal = ({ onLogin, onClose }) => {
|
const LoginModal = ({ onLogin, onClose }) => {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
@ -21,13 +11,31 @@ const LoginModal = ({ onLogin, onClose }) => {
|
||||||
|
|
||||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (username === "admin" && password === "admin") {
|
|
||||||
|
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(); // Успешная авторизация
|
onLogin(); // Успешная авторизация
|
||||||
onClose(); // Закрыть модальное окно
|
onClose(); // Закрыть модальное окно
|
||||||
} else {
|
} else {
|
||||||
setError("Неверный логин или пароль");
|
setError(data.message || "Неверный логин или пароль");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка при отправке запроса:', err);
|
||||||
|
setError("Ошибка при подключении к серверу");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -35,31 +43,6 @@ const LoginModal = ({ onLogin, onClose }) => {
|
||||||
<Modal onClose={onClose}>
|
<Modal onClose={onClose}>
|
||||||
<h2>Авторизация</h2>
|
<h2>Авторизация</h2>
|
||||||
<form onSubmit={handleSubmit}>
|
<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
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
id="user-login"
|
id="user-login"
|
||||||
|
|
@ -82,30 +65,6 @@ const LoginModal = ({ onLogin, onClose }) => {
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
size="normal"
|
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> */}
|
|
||||||
|
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
<button type="submit">Войти</button>
|
<button type="submit">Войти</button>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -27,12 +27,6 @@ const TreeTable = ({ data }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
adjustFontSize();
|
|
||||||
window.addEventListener("resize", adjustFontSize);
|
|
||||||
return () => window.removeEventListener("resize", adjustFontSize);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newLog = [];
|
const newLog = [];
|
||||||
const traverse = (items) => {
|
const traverse = (items) => {
|
||||||
|
|
@ -41,7 +35,7 @@ const TreeTable = ({ data }) => {
|
||||||
newLog.push({
|
newLog.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
time: new Date().toLocaleTimeString() // Добавляем время
|
time: new Date().toLocaleTimeString(), // Добавляем время
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
|
|
@ -50,19 +44,31 @@ const TreeTable = ({ data }) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
traverse(data.items);
|
traverse(data.items);
|
||||||
setLog(newLog);
|
|
||||||
|
// Ограничиваем количество сообщений до 50
|
||||||
|
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи");
|
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи");
|
||||||
|
|
||||||
|
// Функция для отображения заголовков
|
||||||
const renderHeaders = (items) => {
|
const renderHeaders = (items) => {
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
const colSpan = item.items ? item.items.length : 1;
|
const colSpan = item.items ? item.items.length : 1;
|
||||||
return (
|
return (
|
||||||
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}>
|
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}>
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(item.status) }} />
|
<div
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(item.status), marginLeft: "5px" }} />
|
className="status-indicator-bar"
|
||||||
|
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="status-indicator-bar"
|
||||||
|
style={{
|
||||||
|
backgroundColor: statusManager2.getStatusColor(item.status),
|
||||||
|
marginLeft: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -70,41 +76,117 @@ const TreeTable = ({ data }) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderRows = (items) => {
|
// Функция для отображения подзаголовков
|
||||||
if (!items || items.length === 0) return null;
|
const renderSubHeaders = (items) => {
|
||||||
const hasChildren = items.some((item) => item.items && item.items.length > 0);
|
return items.map((item) => {
|
||||||
if (!hasChildren) return null;
|
if (item.items) {
|
||||||
return (
|
return item.items.map((child) => (
|
||||||
<tr className="tree-table-row">
|
<th key={child.id} className="tree-table-header" title={child.title}>
|
||||||
{items.map((item) => {
|
<div className="header-content">
|
||||||
if (item.items && item.items.length > 0) {
|
<div
|
||||||
return (
|
className="status-indicator-bar"
|
||||||
<React.Fragment key={item.id}>
|
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
|
||||||
{item.items.map((child) => (
|
/>
|
||||||
<td key={child.id} className="tree-table-cell" title={child.title}>
|
<div
|
||||||
<div className="cell-content">
|
className="status-indicator-bar"
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(child.status) }} />
|
style={{
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(child.status), marginLeft: "5px" }} />
|
backgroundColor: statusManager2.getStatusColor(child.status),
|
||||||
|
marginLeft: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{child.title}
|
{child.title}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</th>
|
||||||
))}
|
));
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<td key={item.id} className="tree-table-cell" title={item.title}>
|
<th key={item.id} className="tree-table-header" title={item.title}>
|
||||||
<div className="cell-content">
|
<div className="header-content">
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(item.status) }} />
|
<div
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(item.status), marginLeft: "5px" }} />
|
className="status-indicator-bar"
|
||||||
|
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="status-indicator-bar"
|
||||||
|
style={{
|
||||||
|
backgroundColor: statusManager2.getStatusColor(item.status),
|
||||||
|
marginLeft: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для отображения данных
|
||||||
|
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={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",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="cell-text">{child.title}</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
});
|
||||||
</tr>
|
} else {
|
||||||
|
return (
|
||||||
|
<td key={item.id} className="tree-table-cell" title={item.title}>
|
||||||
|
<div className="cell-content">
|
||||||
|
<div
|
||||||
|
className="status-indicator-bar"
|
||||||
|
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="status-indicator-bar"
|
||||||
|
style={{
|
||||||
|
backgroundColor: statusManager2.getStatusColor(item.status),
|
||||||
|
marginLeft: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="cell-text">{item.title}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -118,15 +200,27 @@ const TreeTable = ({ data }) => {
|
||||||
title={data.title}
|
title={data.title}
|
||||||
>
|
>
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager1.getStatusColor(data.status) }} />
|
<div
|
||||||
<div className="status-indicator-bar" style={{ backgroundColor: statusManager2.getStatusColor(data.status), marginLeft: "5px" }} />
|
className="status-indicator-bar"
|
||||||
|
style={{ backgroundColor: statusManager1.getStatusColor(data.status) }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="status-indicator-bar"
|
||||||
|
style={{
|
||||||
|
backgroundColor: statusManager2.getStatusColor(data.status),
|
||||||
|
marginLeft: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>{renderHeaders(filteredData)}</tr>
|
<tr>{renderHeaders(filteredData)}</tr>
|
||||||
|
<tr>{renderSubHeaders(filteredData)}</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{renderRows(filteredData)}</tbody>
|
<tbody>
|
||||||
|
<tr className="tree-table-row">{renderData(filteredData)}</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
|
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button">
|
||||||
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -2,24 +2,34 @@
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: calc(100vw - 20px);
|
width: 100vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-left: 20px;
|
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Сайдбар */
|
||||||
|
.sidebar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--sidebar-color);
|
||||||
|
color: var(--sidebar-text-color);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* Основной контент */
|
/* Основной контент */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-left: 50px;
|
|
||||||
transition: margin-left 0.2s ease;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Контент */
|
/* Контент */
|
||||||
.content {
|
.content {
|
||||||
background-color: var(--modal-background);
|
background-color: var(--modal-background);
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #0056b3;
|
background-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
caption {
|
caption {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
.tree-table-container {
|
.tree-table-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: hidden;
|
||||||
|
/* Убираем горизонтальный скролл */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-table {
|
.tree-table {
|
||||||
|
|
@ -8,26 +9,33 @@
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
/* Фиксированная ширина колонок */
|
||||||
background-color: var(--table-cell-background);
|
background-color: var(--table-cell-background);
|
||||||
color: var(--table-text-color);
|
color: var(--table-text-color);
|
||||||
/* Используем переменную для цвета текста */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-table-header {
|
.tree-table-header {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--table-border);
|
border: 1px solid black;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
/* Текст не переносится */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Скрываем текст, который не помещается */
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
/* Добавляем многоточие */
|
||||||
background-color: var(--table-header-background);
|
background-color: var(--table-header-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-table-cell {
|
.tree-table-cell {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid var(--table-border);
|
border: 1px solid black;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
/* Текст не переносится */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Скрываем текст, который не помещается */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* Добавляем многоточие */
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-content,
|
.cell-content,
|
||||||
|
|
@ -40,6 +48,12 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-text {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.status-indicator-bar {
|
.status-indicator-bar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* Светлая тема по умолчанию */
|
/* Светлая тема по умолчанию */
|
||||||
:root {
|
:root {
|
||||||
--background-color: #FFFFFF;
|
--background-color: #FFFFFF;
|
||||||
--text-color: #FFFFFF;
|
--text-color: #000000;
|
||||||
--header-color: #333333;
|
--header-color: #333333;
|
||||||
/* Основной цвет текста (черный) */
|
/* Основной цвет текста (черный) */
|
||||||
--sidebar-color: #3d74c7;
|
--sidebar-color: #3d74c7;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -2,8 +2,8 @@ import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию
|
//import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию
|
||||||
import './Style/dark-theme.css'; // Подключаем темную тему
|
//import './Style/dark-theme.css'; // Подключаем темную тему
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue