Обновил интерфейс, добавил вкладки

feature/#23
DmitriyA 2025-02-07 13:06:10 +00:00
parent 46a8bbb35d
commit 637b559fe8
14 changed files with 374 additions and 222 deletions

View File

@ -13,7 +13,8 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"chart.js": "^4.0.0", "chart.js": "^4.0.0",
"react-chartjs-2": "^5.0.0" "react-chartjs-2": "^5.0.0",
"axios": "^1.7.9"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",

9
public/data.json Normal file
View File

@ -0,0 +1,9 @@
{
"labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"datasets": [
{
"data": [50, 52, 55, 53, 60, 58, 65, 62, 66, 72],
"data2": [20,56,74,45,21,20,56,74,45,21]
}
]
}

View File

@ -11,9 +11,11 @@
will-change: filter; will-change: filter;
transition: filter 300ms; transition: filter 300ms;
} }
.logo:hover { .logo:hover {
filter: drop-shadow(0 0 2em #646cffaa); filter: drop-shadow(0 0 2em #646cffaa);
} }
.logo.react:hover { .logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa); filter: drop-shadow(0 0 2em #61dafbaa);
} }
@ -22,6 +24,7 @@
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
@ -39,4 +42,4 @@
.read-the-docs { .read-the-docs {
color: #888; color: #888;
} }

View File

@ -1,32 +1,13 @@
import React from "react"; import React from "react";
import SidebarMenu from "./SidebarMenu/SidebarMenu"; // Импорт компонента бокового меню
import CpuTemperatureChart from './Charts/CpuTemperatureChart';
import ErrorIndicator from "./SidebarMenu/ErrorIndicator"; // Индикатор ошибок import ErrorIndicator from "./SidebarMenu/ErrorIndicator"; // Индикатор ошибок
import GpuTemperatureChart from './Charts/GpuTemperatureChart';
import RamUsageChart from './Charts/RamUsageChart'
import Dashboard from "./SidebarMenu/Dashboard"; import Dashboard from "./SidebarMenu/Dashboard";
function App() { function App() {
return ( return (
<div style={{ display: "flex" }}> <div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
<SidebarMenu /> <Dashboard />
<div style={{ marginLeft: "250px" }}>
<h1 className="text-xl font-bold">Мониторинг состояния системы</h1>
<div className="indicator-container">
{/* Индикатор ошибок */}
<h2 className="sidebar-indicator">Индикатор ошибок: </h2>
<ErrorIndicator criticalCount={3} warningCount={7} />
</div>
{/* График температуры CPU
<div className="flex justify-center items-center h-96 bg-gray-100">
<CpuTemperatureChart />
<GpuTemperatureChart />
<RamUsageChart />
</div> */}
<Dashboard/>
</div>
</div> </div>
); );
} }
export default App; export default App;

View File

@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import axios from "axios"
import { import {
Chart as ChartJS, Chart as ChartJS,
LineElement, LineElement,
@ -29,18 +30,28 @@ const CpuTemperatureChart = () => {
}); });
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const fetchData = async () => {
setData((prevData) => { try {
const newTemp = Math.floor(Math.random() * 20) + 50; // Генерируем новую температуру (50-70°C) //const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу
const newLabels = [...prevData.labels.slice(1), prevData.labels[prevData.labels.length - 1] + 1]; // Сдвигаем ось X const response = await axios.get(
const newDataset = [...prevData.datasets[0].data.slice(1), newTemp]; // Сдвигаем данные влево 'http://backend:9101/metrics',
{
params: {
metric: 'CPU'
}
}
);
setData({
labels: response.data.labels,
data: response.data,
});
} catch (error) {
console.error("Ошибка загрузки данных:", error);
}
};
return { fetchData();
labels: newLabels, const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд
datasets: [{ ...prevData.datasets[0], data: newDataset }],
};
});
}, 1000); // Обновление каждую секунду
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
@ -55,7 +66,7 @@ const CpuTemperatureChart = () => {
return ( return (
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col"> <div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
<h2 className="text-xl font-semibold mb-4">График температуры ЦП</h2> <h2 className="text-xl font-semibold mb-4">График температуры ЦП</h2>
<Line <Line
ref={chartRef} ref={chartRef}
data={data} data={data}

View File

@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import axios from "axios";
import { import {
Chart as ChartJS, Chart as ChartJS,
LineElement, LineElement,
@ -18,7 +19,7 @@ const GpuTemperatureChart = () => {
datasets: [ datasets: [
{ {
label: "Температура GPU (°C)", label: "Температура GPU (°C)",
data: Array(20).fill(50), // Начальные значения (например, 50°C) data: [], // Начальные значения (например, 50°C)
borderColor: "blue", borderColor: "blue",
borderWidth: 2, borderWidth: 2,
fill: false, fill: false,
@ -29,18 +30,20 @@ const GpuTemperatureChart = () => {
}); });
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const fetchData = async () => {
setData((prevData) => { try {
const newTemp = Math.floor(Math.random() * 20) + 40; // Генерируем новую температуру (50-600°C) const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу
const newLabels = [...prevData.labels.slice(1), prevData.labels[prevData.labels.length - 1] + 1]; // Сдвигаем ось X setData({
const newDataset = [...prevData.datasets[0].data.slice(1), newTemp]; // Сдвигаем данные влево labels: response.data.labels,
datasets: [{ ...data.datasets[0], data: response.data.datasets[0].data }],
});
} catch (error) {
console.error("Ошибка загрузки данных:", error);
}
};
return { fetchData();
labels: newLabels, const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд
datasets: [{ ...prevData.datasets[0], data: newDataset }],
};
});
}, 1000); // Обновление каждую секунду
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
@ -54,7 +57,7 @@ const GpuTemperatureChart = () => {
return ( return (
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col"> <div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
<h2 className="text-xl font-semibold mb-4">График температуры ГП</h2> <h2 className="text-xl font-semibold mb-4">График температуры ГП</h2>
<Line <Line
ref={chartRef} ref={chartRef}
data={data} data={data}

View File

@ -51,12 +51,12 @@ const RamUsageChart = () => {
{ label: "Доступно", value: " 9,5 ГБ" }, { label: "Доступно", value: " 9,5 ГБ" },
{ label: "Выделено", value: " 6,8/18,2 ГБ" }, { label: "Выделено", value: " 6,8/18,2 ГБ" },
{ label: "Скорость", value: " 3200 МГц" }, { label: "Скорость", value: " 3200 МГц" },
]; ];
return ( return (
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col"> <div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
<h2 className="text-xl font-semibold mb-4">График загруженности ОЗУ</h2> <h2 className="text-xl font-semibold mb-4">График загруженности ОЗУ</h2>
<Line <Line
ref={chartRef} ref={chartRef}
data={data} data={data}

View File

@ -1,42 +1,84 @@
import React from "react"; import React, { useState } from "react";
import SidebarMenu from "./SidebarMenu";
import CpuTemperatureChart from "../Charts/CpuTemperatureChart"; import CpuTemperatureChart from "../Charts/CpuTemperatureChart";
import GpuTemperatureChart from "../Charts/GpuTemperatureChart"; import GpuTemperatureChart from "../Charts/GpuTemperatureChart";
import RamUsageChart from "../Charts/RamUsageChart"; import RamUsageChart from "../Charts/RamUsageChart";
import "../Style/Dashboard.css";
import ErrorIndicator from "./ErrorIndicator";
import "../Style/Dashboard.css"; // Подключаем стили const tabContent = {
"Сервис ВКС": <div><h2>Сервис 1</h2></div>,
"Сервис 2": <div><h2>Сервис 2</h2></div>,
"Сервис 3": <div><h2>Сервис 3</h2></div>,
"Контроль системы": <div><h2>Контроль системы</h2><p>Описание контроля.</p></div>,
"Система управления": <div><h2>Система управления</h2><p>Описание системы управления.</p></div>,
"Проведение ВКС": <div><h2>Проведение ВКС</h2><p>Информация о проведении ВКС.</p></div>,
"Резервное копирование": <div><h2>Резервное копирование</h2><p>Процесс резервного копирования.</p></div>,
"Ретрансляция информации": <div><h2>Ретрансляция информации</h2><p>Детали ретрансляции.</p></div>,
};
const Dashboard = () => { const Dashboard = () => {
const [tabs, setTabs] = useState([]); // Открытые вкладки
const [activeTab, setActiveTab] = useState("Главная"); // Текущая активная вкладка
const handleOpenTab = (tabName) => {
if (!tabs.includes(tabName)) {
setTabs([...tabs, tabName]);
}
setActiveTab(tabName);
};
const handleCloseTab = (tabName) => {
const newTabs = tabs.filter(tab => tab !== tabName);
setTabs(newTabs);
if (activeTab === tabName) {
setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная");
}
};
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
{/* Левая колонка (Графики) */} <SidebarMenu onOpenTab={handleOpenTab} />
<div className="left-column">
<CpuTemperatureChart />
<GpuTemperatureChart /> {/* Можно заменить на другие графики */}
<RamUsageChart />
</div>
{/* Правая колонка (Информационный блок) */} <div className="main-content">
<div className="right-column"> {/* Вкладки */}
<h2>Информационный блок</h2> <div className="tabs">
<p>Здесь можно выводить любые данные о системе.</p> <div
className={`tab ${activeTab === "Главная" ? "active" : ""}`}
<div className="info-item"> onClick={() => setActiveTab("Главная")}
<span className="label">Температура CPU:</span> >
<span className="value">65°C</span> Главная
</div>
{tabs.map(tab => (
<div
key={tab}
className={`tab ${activeTab === tab ? "active" : ""}`}
onClick={() => setActiveTab(tab)}
>
{tab}
<button
className="close-tab"
onClick={(e) => { e.stopPropagation(); handleCloseTab(tab); }}
>
×
</button>
</div>
))}
</div> </div>
<div className="info-item"> {/* Контент */}
<span className="label">Загрузка процессора:</span> <div className="content">
<span className="value">45%</span> {activeTab === "Главная" ? (
</div> <div>
<h2>Общий мониторинг</h2>
<div className="info-item"> <ErrorIndicator />
<span className="label">Ошибки аппаратного обеспечения:</span> <CpuTemperatureChart />
<span className="value error">2</span> <GpuTemperatureChart />
</div> <RamUsageChart />
<div className="info-item"> </div>
<span className="label">Ошибки программного обеспечения:</span> ) : (
<span className="value error">1</span> tabContent[activeTab] || <p>Нет контента</p>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import "../Style/SidebarMenu.css"; // Импортируем стили для компонента import "../Style/SidebarMenu.css";
const menuItems = [ const menuItems = [
{ {
title: "Выбор сервиса", title: "Выбор сервиса",
@ -11,42 +12,78 @@ const menuItems = [
}, },
{ {
title: "Программное обеспечение", title: "Программное обеспечение",
items: ["ПО 1", "ПО 2", "ПО 3"], items: [
{
title: "ПО 1",
items: ["компонент ПО1", "компонент ПО2"],
},
{
title: "ПО 2",
items: ["компонент ПО3"],
},
],
}, },
{ {
title: "Аппаратное обеспечение", title: "Аппаратное обеспечение",
items: ["Оборудование 1", "Оборудование 2", "Оборудование 3"], items: [
{
title: "Оборудование 1",
items: ["компонент Оборудование 1"],
},
{
title: "Оборудование 2",
items: ["компонент Оборудование 2"],
},
],
}, },
]; ];
function SidebarMenu() { // Рекурсивный компонент для отображения меню
const [openSections, setOpenSections] = useState({}); const MenuItem = ({ item, onSelectItem }) => {
const [isOpen, setIsOpen] = useState(false);
const toggleSection = (title) => { const hasChildren = Array.isArray(item.items) && item.items.length > 0;
setOpenSections((prev) => ({ ...prev, [title]: !prev[title] }));
const handleClick = () => {
if (hasChildren) {
setIsOpen(!isOpen); // Раскрываем/сворачиваем подменю
} else {
onSelectItem(item); // Выбираем конечный элемент
}
};
return (
<div className="menu-item">
<div onClick={handleClick} className="menu-item-header">
<span>{item.title}</span>
{hasChildren && <span>{isOpen ? "▲" : "▼"}</span>}
</div>
{isOpen && hasChildren && (
<div className="submenu">
{item.items.map((child, index) => (
<MenuItem
key={index}
item={typeof child === "string" ? { title: child } : child}
onSelectItem={onSelectItem}
/>
))}
</div>
)}
</div>
);
};
// Основной компонент SidebarMenu
function SidebarMenu({ onOpenTab }) {
const handleSelectItem = (item) => {
onOpenTab(item.title); // Передаем название вкладки в родительский компонент
}; };
return ( return (
<div className="sidebar"> <div className="sidebar">
<h2 className="sidebar-title">Меню</h2> <h2 className="sidebar-title">Меню</h2>
{menuItems.map((section) => ( {menuItems.map((section, index) => (
<div key={section.title} className="sidebar-section"> <MenuItem key={index} item={section} onSelectItem={handleSelectItem} />
<button
onClick={() => toggleSection(section.title)}
className="sidebar-button"
>
{section.title}
</button>
{openSections[section.title] && (
<ul className="sidebar-items">
{section.items.map((item) => (
<li key={item} className="sidebar-item">
{item}
</li>
))}
</ul>
)}
</div>
))} ))}
</div> </div>
); );

View File

@ -1,42 +1,89 @@
.dashboard-container { .dashboard-container {
display: flex; display: flex;
gap: 20px; height: 100vh;
padding: 20px; width: 100vw;
} overflow: hidden;
/* Запрещаем появление скролла */
.left-column { }
flex: 2;
} .main-content {
flex: 1;
.right-column { min-width: 400px;
flex: 1; max-width: calc(100vw - 250px);
background: #f3f3f3; padding: 20px;
padding: 20px; box-sizing: border-box;
border-radius: 8px; overflow-y: auto;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); /* Добавляем вертикальную прокрутку */
} height: 100vh;
/* Ограничиваем высоту */
h2 { }
font-size: 20px;
margin-bottom: 10px;
} /* Вкладки */
.tabs {
.info-item { display: flex;
display: flex; gap: 5px;
justify-content: space-between; padding: 5px;
padding: 10px 0; background-color: #222;
border-bottom: 1px solid #ddd; border-bottom: 2px solid #444;
} overflow-x: auto;
white-space: nowrap;
.label { }
font-weight: bold;
} .tab {
display: flex;
.value { align-items: center;
color: #007bff; background-color: #333;
font-weight: bold; color: white;
} padding: 5px 10px;
border-radius: 5px 5px 0 0;
.value.error { cursor: pointer;
color: red; max-width: 250px;
} /* Ограничиваем максимальную ширину */
min-width: 100px;
/* Минимальная ширина */
flex-shrink: 0;
/* Не позволяет вкладкам сжиматься */
position: relative;
}
.tab.active {
background-color: #555;
}
.close-tab {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 0;
}
/* Контент */
.content {
background-color: #f9f9f9;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.default-content {
display: flex;
flex-direction: column;
gap: 50px;
}
.tab-content {
background-color: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
h2 {
color: #444;
}
p {
color: #333;
}

View File

@ -26,4 +26,11 @@
.warning span { .warning span {
color: orange; color: orange;
}
.indicator-container {
display: flex;
align-items: center;
gap: 15px;
justify-content: center;
} }

View File

@ -1,38 +1,39 @@
.expandable-info { .expandable-info {
margin-top: 10px; margin-top: 10px;
} }
.expand-button { .expand-button {
background-color: #444; background-color: #444;
color: white; color: white;
border: none; border: none;
padding: 8px 16px; padding: 8px 16px;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
} }
.expand-button:hover { .expand-button:hover {
background-color: #333; background-color: #333;
} }
.details-menu { .details-menu {
margin-top: 10px; margin-top: 10px;
padding: 10px; padding: 10px;
border: 1px solid #ddd; border: 1px solid #333;
border-radius: 4px; border-radius: 4px;
background-color: #f9f9f9; background-color: white;
} }
.detail-item { .detail-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 5px; margin-bottom: 5px;
} }
.label { .label {
font-weight: bold; font-weight: bold;
} color: #333
}
.value {
color: #555; .value {
} color: #333;
}

View File

@ -1,66 +1,70 @@
/* SidebarMenu.css */ /* Боковое меню */
.sidebar { .sidebar {
position: fixed;
height: 100vh;
width: 250px; width: 250px;
/* height: 100vh; */
background-color: #333; background-color: #333;
color: white;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
border-right: 1px solid #444;
height: 100vh;
/* Занимает всю высоту экрана */
overflow-y: auto;
/* Прокрутка внутри меню, если контент не помещается */
position: sticky;
/* Фиксируем меню */
top: 0;
/* Прилипаем к верху */
} }
.sidebar-title { .sidebar-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px; margin-bottom: 20px;
}
.sidebar-indicator {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
margin-bottom: 15px; color: white;
} }
.sidebar-section { .menu-item {
margin-bottom: 15px; margin-bottom: 10px;
color: white;
} }
.sidebar-button { h2 {
width: 100%; color: white
}
.menu-item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px; padding: 10px;
background-color: #444; background-color: #444;
border: none;
color: white;
text-align: left;
cursor: pointer;
font-size: 16px;
border-radius: 5px; border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
} }
.sidebar-button:hover { .menu-item-header:hover {
background-color: #555; background-color: #222;
} }
.sidebar-items { .submenu {
list-style: none; margin-left: 20px;
padding-left: 20px;
margin-top: 10px; margin-top: 10px;
} }
.sidebar-item { .tabs-container {
padding: 5px 0; margin-top: 20px;
}
.tab {
padding: 10px;
background-color: #444;
border: 1px solid #333;
border-radius: 5px;
margin-bottom: 5px;
cursor: pointer; cursor: pointer;
font-size: 14px; transition: background-color 0.3s ease;
} }
.sidebar-item:hover { .tab:hover {
color: #ccc; background-color: #222;
}
.indicator-container {
display: flex;
align-items: center;
gap: 15px;
} }

View File

@ -18,6 +18,7 @@ a {
color: #646cff; color: #646cff;
text-decoration: inherit; text-decoration: inherit;
} }
a:hover { a:hover {
color: #535bf2; color: #535bf2;
} }
@ -31,8 +32,9 @@ body {
} }
h1 { h1 {
text-align: center;
font-size: 3.2em; font-size: 3.2em;
line-height: 1.1; line-height: 1;
} }
button { button {
@ -46,9 +48,11 @@ button {
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
} }
button:hover { button:hover {
border-color: #646cff; border-color: #646cff;
} }
button:focus, button:focus,
button:focus-visible { button:focus-visible {
outline: 4px auto -webkit-focus-ring-color; outline: 4px auto -webkit-focus-ring-color;
@ -59,10 +63,12 @@ button:focus-visible {
color: #213547; color: #213547;
background-color: #ffffff; background-color: #ffffff;
} }
a:hover { a:hover {
color: #747bff; color: #747bff;
} }
button { button {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
} }