Compare commits
No commits in common. "517d3893bec0a15fd7ac8f8227c529907a6f9c8e" and "ffc9f82e5aa9a45778b3a9d309e611c3e5bc4769" have entirely different histories.
517d3893be
...
ffc9f82e5a
|
|
@ -1,27 +1 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
*.les*
|
|
||||||
node_modules
|
|
||||||
11
Dockerfile
11
Dockerfile
|
|
@ -1,11 +0,0 @@
|
||||||
FROM node:22.13.0
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json package-lock.json vite.config.js eslint.config.js ./
|
|
||||||
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENTRYPOINT ["npm", "run", "dev"]
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
### Запуск проекта
|
|
||||||
- ```docker build -t <image_name> .```
|
|
||||||
- ```docker run --rm --name <unique-name> -v $(pwd)/src/:/var/www/trust-module/src -v $(pwd)/public/:/var/www/trust-module/public -p <hostPort>:<containerPort> <image_name>:latest```
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import react from 'eslint-plugin-react'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{ ignores: ['dist'] },
|
|
||||||
{
|
|
||||||
files: ['**/*.{js,jsx}'],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
ecmaFeatures: { jsx: true },
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
settings: { react: { version: '18.3' } },
|
|
||||||
plugins: {
|
|
||||||
react,
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...js.configs.recommended.rules,
|
|
||||||
...react.configs.recommended.rules,
|
|
||||||
...react.configs['jsx-runtime'].rules,
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
'react/jsx-no-target-blank': 'off',
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
13
index.html
13
index.html
|
|
@ -1,13 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/system_monitor_icon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Модуль доверия</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,33 +1,34 @@
|
||||||
{
|
{
|
||||||
"name": "trust-module",
|
"name": "trust_module",
|
||||||
"private": true,
|
"version": "0.1.0",
|
||||||
"version": "0.0.0",
|
"devDependencies": {
|
||||||
"type": "module",
|
"autoprefixer": "^10.4.20",
|
||||||
"scripts": {
|
"postcss": "^8.5.1",
|
||||||
"dev": "vite --port 5173",
|
"postcss-cli": "^11.0.0",
|
||||||
"build": "vite build",
|
"tailwindcss": "^4.0.1"
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"cra-template": "^1.2.0",
|
||||||
"d3": "^7.9.0",
|
"react": "^19.0.0",
|
||||||
"react": "^18.3.1",
|
"react-dom": "^19.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-scripts": "^5.0.1"
|
||||||
"chart.js": "^4.0.0",
|
|
||||||
"react-chartjs-2": "^5.0.0",
|
|
||||||
"axios": "^1.7.9"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"scripts": {
|
||||||
"@eslint/js": "^9.17.0",
|
"start": "react-scripts start",
|
||||||
"@types/react": "^18.3.18",
|
"build": "react-scripts build",
|
||||||
"@types/react-dom": "^18.3.5",
|
"test": "react-scripts test",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"eject": "react-scripts eject"
|
||||||
"eslint": "^9.17.0",
|
},
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"browserslist": {
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"production": [
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
">0.2%",
|
||||||
"globals": "^15.14.0",
|
"not dead",
|
||||||
"vite": "^6.0.5"
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Число участников конференции",
|
|
||||||
"value": 50,
|
|
||||||
"status": "normal",
|
|
||||||
"details": "Число участников конференции в пределах нормы."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "Подключение пользователей",
|
|
||||||
"value": 75,
|
|
||||||
"status": "warning",
|
|
||||||
"details": "Пользователи имеют проблемы с подключением."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "Максимальное число комнат",
|
|
||||||
"value": 100,
|
|
||||||
"status": "critical",
|
|
||||||
"details": "Количество комнат превысило норму."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"timestamp": "2025-02-18 12:00",
|
|
||||||
"value": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2025-02-18 12:05",
|
|
||||||
"value": 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2025-02-18 12:10",
|
|
||||||
"value": 15
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2025-02-18 12:15",
|
|
||||||
"value": 13
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"timestamp": "2025-02-18 12:20",
|
|
||||||
"value": 17
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div> <!-- Здесь появится наше React-приложение -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<svg
|
|
||||||
width="100" height="100"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none" stroke="black" stroke-width="5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<!-- Окружность -->
|
|
||||||
<circle cx="50" cy="50" r="45" stroke="#4CAF50" stroke-width="5" fill="none" />
|
|
||||||
|
|
||||||
<!-- График нагрузки -->
|
|
||||||
<polyline points="20,70 35,40 50,60 65,30 80,50" stroke="#4CAF50" stroke-width="5" fill="none" />
|
|
||||||
|
|
||||||
<!-- Крестик в центре, символизирующий мониторинг -->
|
|
||||||
<line x1="45" y1="45" x2="55" y2="55" stroke="#4CAF50" stroke-width="4" />
|
|
||||||
<line x1="55" y1="45" x2="45" y2="55" stroke="#4CAF50" stroke-width="4" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 729 B |
|
|
@ -1,23 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "CPU Usage",
|
|
||||||
"value": 50,
|
|
||||||
"status": "normal",
|
|
||||||
"details": "Current CPU usage is within normal limits."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "RAM Usage",
|
|
||||||
"value": 75,
|
|
||||||
"status": "warning",
|
|
||||||
"details": "RAM usage is high. Consider freeing up memory."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "GPU Usage",
|
|
||||||
"value": 30,
|
|
||||||
"status": "normal",
|
|
||||||
"details": "GPU usage is within normal limits."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
45
src/App.css
45
src/App.css
|
|
@ -1,45 +0,0 @@
|
||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from "react";
|
||||||
|
import SidebarMenu from "./components/SidebarMenu"; // Импорт компонента бокового меню
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<SidebarMenu />
|
||||||
|
<div style={{ padding: "20px", flex: 1 }}>Рабочая область</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
26
src/App.jsx
26
src/App.jsx
|
|
@ -1,26 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import Dashboard from "./Components/Dashboard";
|
|
||||||
import LoginModal from "./Components/LoginModal"; // Импортируем компонент авторизации
|
|
||||||
import "./Style/LoginModal.css"; // Импортируем стили
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false); // Состояние авторизации
|
|
||||||
const [showLoginModal, setShowLoginModal] = useState(true); // Показывать ли модальное окно
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
|
||||||
setIsAuthenticated(true); // Устанавливаем авторизацию
|
|
||||||
setShowLoginModal(false); // Скрываем модальное окно
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
|
|
||||||
{!isAuthenticated && showLoginModal && (
|
|
||||||
<LoginModal onLogin={handleLogin} onClose={() => setShowLoginModal(false)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isAuthenticated && <Dashboard />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { Line } from "react-chartjs-2";
|
|
||||||
import axios from "axios";
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
LinearScale,
|
|
||||||
CategoryScale,
|
|
||||||
} from "chart.js";
|
|
||||||
import ExpandableInfo from "../Components/ExpandableInfo"
|
|
||||||
|
|
||||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale);
|
|
||||||
|
|
||||||
const GpuTemperatureChart = () => {
|
|
||||||
const chartRef = useRef(null);
|
|
||||||
const [data, setData] = useState({
|
|
||||||
labels: Array(10).fill("").map((_, i) => i), // 20 точек по X
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Температура GPU (°C)",
|
|
||||||
data: [], // Начальные значения (например, 50°C)
|
|
||||||
borderColor: "blue",
|
|
||||||
borderWidth: 2,
|
|
||||||
fill: false,
|
|
||||||
cubicInterpolationMode: "monotone", // Сглаживание
|
|
||||||
tension: 0.4, // Делаем линию плавнее
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу
|
|
||||||
setData({
|
|
||||||
labels: response.data.labels,
|
|
||||||
datasets: [{ ...data.datasets[0], data: response.data.datasets[0].data }],
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка загрузки данных:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Пример данных для меню "Подробнее"
|
|
||||||
const details = [
|
|
||||||
{ label: "Использование", value: " 20%" },
|
|
||||||
{ label: "Оперативная память ГП", value: " 1,2/7,9 ГБ" },
|
|
||||||
{ label: "Общая память ГП", value: " 1,2/7,9 ГБ" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">График температуры ГП</h2>
|
|
||||||
<Line
|
|
||||||
ref={chartRef}
|
|
||||||
data={data}
|
|
||||||
options={{
|
|
||||||
animation: false, // Отключаем анимацию обновления (чтобы был плавный сдвиг)
|
|
||||||
scales: {
|
|
||||||
x: { display: true },
|
|
||||||
y: { min: 30, max: 80 }, // Ограничиваем Y (например, 30-80°C)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ExpandableInfo details={details} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GpuTemperatureChart;
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { Line } from "react-chartjs-2";
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
LinearScale,
|
|
||||||
CategoryScale,
|
|
||||||
} from "chart.js";
|
|
||||||
import ExpandableInfo from "../Components/ExpandableInfo"
|
|
||||||
|
|
||||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale);
|
|
||||||
|
|
||||||
const RamUsageChart = () => {
|
|
||||||
const chartRef = useRef(null);
|
|
||||||
const [data, setData] = useState({
|
|
||||||
labels: Array(10).fill("").map((_, i) => i), // 20 точек по X
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Загруженность RAM (%)",
|
|
||||||
data: Array(20).fill(50), // Начальные значения (например, 50%)
|
|
||||||
borderColor: "green",
|
|
||||||
borderWidth: 2,
|
|
||||||
fill: false,
|
|
||||||
cubicInterpolationMode: "monotone", // Сглаживание
|
|
||||||
tension: 0.4, // Делаем линию плавнее
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setData((prevData) => {
|
|
||||||
const newTemp = Math.floor(Math.random() * 20) + 40; // Генерируем новую температуру (50-600°C)
|
|
||||||
const newLabels = [...prevData.labels.slice(1), prevData.labels[prevData.labels.length - 1] + 1]; // Сдвигаем ось X
|
|
||||||
const newDataset = [...prevData.datasets[0].data.slice(1), newTemp]; // Сдвигаем данные влево
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels: newLabels,
|
|
||||||
datasets: [{ ...prevData.datasets[0], data: newDataset }],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 1000); // Обновление каждую секунду
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Пример данных для меню "Подробнее"
|
|
||||||
const details = [
|
|
||||||
{ label: "Используется", value: " 6,2 ГБ" },
|
|
||||||
{ label: "Доступно", value: " 9,5 ГБ" },
|
|
||||||
{ label: "Выделено", value: " 6,8/18,2 ГБ" },
|
|
||||||
{ label: "Скорость", value: " 3200 МГц" },
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">График загруженности ОЗУ</h2>
|
|
||||||
<Line
|
|
||||||
ref={chartRef}
|
|
||||||
data={data}
|
|
||||||
options={{
|
|
||||||
animation: false, // Отключаем анимацию обновления (чтобы был плавный сдвиг)
|
|
||||||
scales: {
|
|
||||||
x: { display: true },
|
|
||||||
y: { min: 0, max: 100 }, // Ограничиваем Y (например, 30-80°C)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ExpandableInfo details={details} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RamUsageChart;
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import "../Style/SystemStatusTable.css";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SystemStatusTable = () => {
|
|
||||||
const [systemData, setSystemData] = useState([]);
|
|
||||||
const [expandedRow, setExpandedRow] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
// Загрузка данных с бэкенда
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get("/trust.json"); // Укажите ваш endpoint
|
|
||||||
setSystemData(response.data);
|
|
||||||
setLoading(false);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Обработчик для кнопки "Подробнее"
|
|
||||||
const handleDetailsClick = (id) => {
|
|
||||||
setExpandedRow(expandedRow === id ? null : id);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <p>Загрузка данных...</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p>Ошибка: {error}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table>
|
|
||||||
<caption>
|
|
||||||
<h2>Состояние системы</h2>
|
|
||||||
</caption>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Метрика</th>
|
|
||||||
<th>Значение</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
<th>Детали</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{systemData.map((item) => (
|
|
||||||
<React.Fragment key={item.id}>
|
|
||||||
<tr>
|
|
||||||
<td>{item.name}</td>
|
|
||||||
<td>{item.value}%</td>
|
|
||||||
<td>
|
|
||||||
<span className={`status ${item.status}`}>{item.status}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button onClick={() => handleDetailsClick(item.id)}>
|
|
||||||
{expandedRow === item.id ? "Скрыть" : "Подробнее"}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{expandedRow === item.id && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan="4">
|
|
||||||
<div className="details">
|
|
||||||
<p>{item.details}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemStatusTable;
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import "../Style/SystemStatusTable.css";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SystemStatusTableSoftware = () => {
|
|
||||||
const [systemData, setSystemData] = useState([]);
|
|
||||||
const [expandedRow, setExpandedRow] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
// Загрузка данных с бэкенда
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get("/TrustSoftware.json"); // Укажите ваш endpoint
|
|
||||||
setSystemData(response.data);
|
|
||||||
setLoading(false);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Обработчик для кнопки "Подробнее"
|
|
||||||
const handleDetailsClick = (id) => {
|
|
||||||
setExpandedRow(expandedRow === id ? null : id);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <p>Загрузка данных...</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p>Ошибка: {error}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table>
|
|
||||||
<caption>
|
|
||||||
<h2>Состояние ПО</h2>
|
|
||||||
</caption>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Метрика</th>
|
|
||||||
<th>Значение</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
<th>Детали</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{systemData.map((item) => (
|
|
||||||
<React.Fragment key={item.id}>
|
|
||||||
<tr>
|
|
||||||
<td>{item.name}</td>
|
|
||||||
<td>{item.value}%</td>
|
|
||||||
<td>
|
|
||||||
<span className={`status ${item.status}`}>{item.status}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button onClick={() => handleDetailsClick(item.id)}>
|
|
||||||
{expandedRow === item.id ? "Скрыть" : "Подробнее"}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{expandedRow === item.id && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan="4">
|
|
||||||
<div className="details">
|
|
||||||
<p>{item.details}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemStatusTableSoftware;
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale,
|
|
||||||
} from 'chart.js';
|
|
||||||
import 'chartjs-adapter-date-fns'; // Импортируем адаптер дат
|
|
||||||
|
|
||||||
// Регистрируем компоненты Chart.js
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale // Регистрируем временную шкалу
|
|
||||||
);
|
|
||||||
|
|
||||||
const NetworkSpeedChart = () => {
|
|
||||||
const [chartData, setChartData] = useState({
|
|
||||||
labels: [],
|
|
||||||
datasets: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const chartRef = useRef(null); // Референс на график
|
|
||||||
|
|
||||||
// Функция для загрузки данных
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('http://192.168.2.33:3000/metrics?metric=zvks_abonents_total');
|
|
||||||
const newData = response.data;
|
|
||||||
|
|
||||||
console.log('New data from backend:', newData); // Проверяем новые данные
|
|
||||||
|
|
||||||
// Обновляем состояние, добавляя новые данные к существующим
|
|
||||||
setChartData((prevChartData) => {
|
|
||||||
// Группируем новые данные по устройству (device)
|
|
||||||
const newGroupedData = newData.reduce((acc, entry) => {
|
|
||||||
const device = entry.device;
|
|
||||||
if (!acc[device]) {
|
|
||||||
acc[device] = [];
|
|
||||||
}
|
|
||||||
acc[device].push(entry);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Создаем новый набор данных
|
|
||||||
const newDatasets = Object.keys(newGroupedData).map((device, index) => {
|
|
||||||
// Находим существующий dataset для этого устройства
|
|
||||||
const existingDataset = prevChartData.datasets.find((dataset) => dataset.label === `Device: ${device}`);
|
|
||||||
|
|
||||||
// Если dataset уже существует, добавляем новые данные к нему
|
|
||||||
if (existingDataset) {
|
|
||||||
return {
|
|
||||||
...existingDataset,
|
|
||||||
data: [
|
|
||||||
...existingDataset.data,
|
|
||||||
...newGroupedData[device].map((entry) => ({
|
|
||||||
x: new Date(entry.timestamp), // Временная метка
|
|
||||||
y: entry.value, // Значение
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если dataset не существует, создаем новый
|
|
||||||
return {
|
|
||||||
label: `Device: ${device}`,
|
|
||||||
data: newGroupedData[device].map((entry) => ({
|
|
||||||
x: new Date(entry.timestamp),
|
|
||||||
y: entry.value,
|
|
||||||
})),
|
|
||||||
borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`,
|
|
||||||
backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`,
|
|
||||||
tension: 0.2,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновляем labels (метки времени)
|
|
||||||
const newLabels = [
|
|
||||||
...prevChartData.labels,
|
|
||||||
...newData.map((entry) => new Date(entry.timestamp)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels: newLabels,
|
|
||||||
datasets: newDatasets,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при загрузке метрик:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Загружаем данные при монтировании компонента и обновляем каждые 5 секунд
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
const interval = setInterval(fetchData, 5000);
|
|
||||||
|
|
||||||
// Очищаем интервал и уничтожаем график при размонтировании компонента
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
if (chartRef.current) {
|
|
||||||
chartRef.current.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Опции графика
|
|
||||||
const options = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'top',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'node_network_receive_bytes_total',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time', // Используем временную шкалу
|
|
||||||
time: {
|
|
||||||
unit: 'second', // Единица времени
|
|
||||||
displayFormats: {
|
|
||||||
second: 'HH:mm:ss', // Формат отображения времени
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Time',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Данные',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
duration: 1000, // Длительность анимации
|
|
||||||
easing: 'linear', // Тип анимации
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '800px', height: '400px' }}>
|
|
||||||
<Line ref={chartRef} data={chartData} options={options} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkSpeedChart;
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale,
|
|
||||||
} from 'chart.js';
|
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale
|
|
||||||
);
|
|
||||||
|
|
||||||
const MAX_DATA_POINTS = 50;
|
|
||||||
|
|
||||||
const NetworkSpeedChart2 = () => {
|
|
||||||
const [chartData, setChartData] = useState({ labels: [], datasets: [] });
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('http://192.168.2.33:3000/metrics?metric=node_time_seconds');
|
|
||||||
const newData = response.data;
|
|
||||||
|
|
||||||
setChartData((prevChartData) => {
|
|
||||||
const newGroupedData = newData.reduce((acc, entry) => {
|
|
||||||
if (!acc[entry.device]) acc[entry.device] = [];
|
|
||||||
acc[entry.device].push({ x: new Date(entry.timestamp), y: entry.value });
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const newDatasets = Object.keys(newGroupedData).map((device, index) => {
|
|
||||||
const existingDataset = prevChartData.datasets.find((d) => d.label === `Device: ${device}`);
|
|
||||||
const updatedData = existingDataset ? [...existingDataset.data, ...newGroupedData[device]] : newGroupedData[device];
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: `Device: ${device}`,
|
|
||||||
data: updatedData.slice(-MAX_DATA_POINTS),
|
|
||||||
borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`,
|
|
||||||
backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`,
|
|
||||||
tension: 0.2,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { labels: newDatasets[0]?.data.map((d) => d.x) || [], datasets: newDatasets };
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при загрузке метрик:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
const interval = setInterval(fetchData, 5000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: { position: 'top' },
|
|
||||||
title: { display: true, text: 'node_time_seconds' },
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
time: { unit: 'second', displayFormats: { second: 'HH:mm:ss' } },
|
|
||||||
title: { display: true, text: 'Time' },
|
|
||||||
},
|
|
||||||
y: { title: { display: true, text: 'Value' } },
|
|
||||||
},
|
|
||||||
animation: { duration: 1000, easing: 'linear' },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '800px', height: '400px' }}>
|
|
||||||
<Line data={chartData} options={options} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkSpeedChart2;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale } from 'chart.js';
|
|
||||||
|
|
||||||
// Регистрация компонентов Chart.js
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale);
|
|
||||||
|
|
||||||
const SimpleGraph = () => {
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
// Загружаем данные из файла с использованием axios
|
|
||||||
const response = await axios.get('/data.json'); // Путь должен быть относительно папки public
|
|
||||||
const rawData = response.data;
|
|
||||||
|
|
||||||
// Проверяем, что данные действительно массив
|
|
||||||
if (Array.isArray(rawData)) {
|
|
||||||
const chartData = rawData.map(item => ({
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
value: item.value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setData(chartData);
|
|
||||||
} else {
|
|
||||||
throw new Error('Ошибка: Данные не являются массивом.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (data.length === 0) return <div>Loading...</div>;
|
|
||||||
|
|
||||||
// Настройки графика
|
|
||||||
const chartOptions = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Simple Data Graph',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartData = {
|
|
||||||
labels: data.map(item => item.timestamp), // Массив меток для оси X
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: 'Value',
|
|
||||||
data: data.map(item => item.value), // Массив значений для оси Y
|
|
||||||
borderColor: 'rgb(75, 192, 192)',
|
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
||||||
fill: false,
|
|
||||||
tension: 0.1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Line data={chartData} options={chartOptions} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimpleGraph;
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import SidebarMenu from "./SidebarMenu";
|
|
||||||
import SystemStatusTable from "../Charts/SystemStatusTable";
|
|
||||||
import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware";
|
|
||||||
import TreeChart from "./TreeChart";
|
|
||||||
import "../Style/Dashboard.css";
|
|
||||||
import ErrorIndicator from "./ErrorIndicator";
|
|
||||||
import tabContentData from "./tabContent";
|
|
||||||
import menuData from "./menuData.json"; // Загружаем новое меню
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
|
||||||
const [tabs, setTabs] = useState([]);
|
|
||||||
const [activeTab, setActiveTab] = useState("Главная");
|
|
||||||
const [tabContent, setTabContent] = useState({});
|
|
||||||
const [treeData, setTreeData] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTabContent(tabContentData);
|
|
||||||
setTreeData(menuData); // Теперь menuData - объект, а не массив
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOpenTab = (id, title) => {
|
|
||||||
if (!tabs.includes(id)) {
|
|
||||||
setTabs([...tabs, id]);
|
|
||||||
}
|
|
||||||
setActiveTab(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseTab = (id) => {
|
|
||||||
const newTabs = tabs.filter((tab) => tab !== id);
|
|
||||||
setTabs(newTabs);
|
|
||||||
if (activeTab === id) {
|
|
||||||
setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabContent = () => {
|
|
||||||
if (activeTab === "Главная") {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Общий мониторинг</h2>
|
|
||||||
<ErrorIndicator />
|
|
||||||
<SystemStatusTable />
|
|
||||||
<SystemStatusTableSoftware />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (activeTab === "Визуализация") {
|
|
||||||
return <TreeChart data={treeData} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
|
|
||||||
} else {
|
|
||||||
const tabData = tabContent[activeTab];
|
|
||||||
return tabData ? tabData.content : <p>Нет данных</p>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="dashboard-container">
|
|
||||||
<SidebarMenu onOpenTab={handleOpenTab} />
|
|
||||||
|
|
||||||
<div className="main-content">
|
|
||||||
<div className="tabs">
|
|
||||||
<div
|
|
||||||
className={`tab ${activeTab === "Главная" ? "active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("Главная")}
|
|
||||||
>
|
|
||||||
Главная
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`tab ${activeTab === "Визуализация" ? "active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("Визуализация")}
|
|
||||||
>
|
|
||||||
Визуализация
|
|
||||||
</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 className="content">
|
|
||||||
{renderTabContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import criticalIcon from "../assets/images/critical.png"; // Красный треугольник
|
|
||||||
import warningIcon from "../assets/images/warning.png"; // Желтый треугольник
|
|
||||||
import "../Style/ErrorIndicator.css"; // Подключаем стили
|
|
||||||
|
|
||||||
const ErrorIndicator = ({ criticalCount, warningCount }) => {
|
|
||||||
return (
|
|
||||||
<div className="error-indicator">
|
|
||||||
{/* Красный индикатор (критические ошибки) */}
|
|
||||||
<div className="error-item critical">
|
|
||||||
<img src={criticalIcon} alt="Критическая ошибка" />
|
|
||||||
<span>{criticalCount}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Желтый индикатор (предупреждения) */}
|
|
||||||
<div className="error-item warning">
|
|
||||||
<img src={warningIcon} alt="Предупреждение" />
|
|
||||||
<span>{warningCount}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ErrorIndicator;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import "../Style/Expandable.css"
|
|
||||||
|
|
||||||
const ExpandableInfo = ({ details }) => {
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
|
|
||||||
const toggleExpand = () => {
|
|
||||||
setIsExpanded(!isExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="expandable-info">
|
|
||||||
<button onClick={toggleExpand} className="expand-button">
|
|
||||||
{isExpanded ? "Скрыть" : "Подробнее"}
|
|
||||||
</button>
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="details-menu">
|
|
||||||
{details.map((detail, index) => (
|
|
||||||
<div key={index} className="detail-item">
|
|
||||||
<span className="label">{detail.label}:</span>
|
|
||||||
<span className="value">{detail.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExpandableInfo;
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
const Login = ({ onLogin, onClose }) => {
|
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (username === "admin" && password === "admin") {
|
|
||||||
onLogin(); // Успешная авторизация
|
|
||||||
onClose(); // Закрыть модальное окно
|
|
||||||
} else {
|
|
||||||
setError("Неверный логин или пароль");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="modal-overlay">
|
|
||||||
<div className="modal">
|
|
||||||
<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>
|
|
||||||
{error && <p className="error">{error}</p>}
|
|
||||||
<button type="submit">Войти</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import "../Style/SidebarMenu.css";
|
|
||||||
import menuData from "./menuData.json";
|
|
||||||
|
|
||||||
const MenuItem = ({ item, onSelectItem }) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
|
||||||
|
|
||||||
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={child} onSelectItem={onSelectItem} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function SidebarMenu({ onOpenTab }) {
|
|
||||||
const handleSelectItem = (item) => {
|
|
||||||
onOpenTab(item.id, item.title); // Передаем id и title
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sidebar">
|
|
||||||
<h2 className="sidebar-title">Меню</h2>
|
|
||||||
<MenuItem item={menuData} onSelectItem={handleSelectItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SidebarMenu;
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
const TreeChart = ({ data, onNodeClick }) => {
|
|
||||||
const chartRef = useRef();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// Очищаем старый граф перед отрисовкой
|
|
||||||
d3.select(chartRef.current).selectAll("*").remove();
|
|
||||||
|
|
||||||
const width = 928;
|
|
||||||
const height = 600;
|
|
||||||
|
|
||||||
const root = d3.hierarchy(data, (d) => d.items);
|
|
||||||
const links = root.links();
|
|
||||||
const nodes = root.descendants();
|
|
||||||
|
|
||||||
const simulation = d3
|
|
||||||
.forceSimulation(nodes)
|
|
||||||
.force("link", d3.forceLink(links).id((d) => d.data.title).distance(80).strength(1)) // Увеличил дистанцию
|
|
||||||
.force("charge", d3.forceManyBody().strength(-500)) // Увеличил отталкивание узлов
|
|
||||||
.force("x", d3.forceX())
|
|
||||||
.force("y", d3.forceY());
|
|
||||||
|
|
||||||
const svg = d3
|
|
||||||
.select(chartRef.current)
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", height)
|
|
||||||
.attr("viewBox", [-width / 2, -height / 2, width, height])
|
|
||||||
.attr("style", "max-width: 100%; height: auto;");
|
|
||||||
|
|
||||||
const link = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("stroke", "#999")
|
|
||||||
.attr("stroke-opacity", 0.6)
|
|
||||||
.selectAll("line")
|
|
||||||
.data(links)
|
|
||||||
.join("line");
|
|
||||||
|
|
||||||
const node = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("stroke", "#000")
|
|
||||||
.attr("stroke-width", 1.5)
|
|
||||||
.selectAll("circle")
|
|
||||||
.data(nodes)
|
|
||||||
.join("circle")
|
|
||||||
.attr("fill", (d) => (d.children ? "#555" : "#000"))
|
|
||||||
.attr("stroke", "#fff")
|
|
||||||
.attr("r", 7) // Немного увеличил размер узлов для удобства клика
|
|
||||||
.call(drag(simulation));
|
|
||||||
|
|
||||||
// Добавляем текстовые подписи
|
|
||||||
const text = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("fill", "#000")
|
|
||||||
.attr("font-family", "Arial")
|
|
||||||
.attr("font-size", 12)
|
|
||||||
.attr("pointer-events", "none") // Отключаем обработку событий текста
|
|
||||||
.selectAll("text")
|
|
||||||
.data(nodes)
|
|
||||||
.join("text")
|
|
||||||
.text((d) => d.data.title)
|
|
||||||
.attr("dx", 12) // Отодвигаем текст дальше от узла
|
|
||||||
.attr("dy", 4) // Немного поднимаем текст
|
|
||||||
|
|
||||||
node.append("title").text((d) => d.data.title);
|
|
||||||
|
|
||||||
node.on("click", (event, d) => {
|
|
||||||
if (onNodeClick) {
|
|
||||||
onNodeClick(d.data.id, d.data.title); // Передаем id и title
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
simulation.on("tick", () => {
|
|
||||||
link
|
|
||||||
.attr("x1", (d) => d.source.x)
|
|
||||||
.attr("y1", (d) => d.source.y)
|
|
||||||
.attr("x2", (d) => d.target.x)
|
|
||||||
.attr("y2", (d) => d.target.y);
|
|
||||||
|
|
||||||
node
|
|
||||||
.attr("cx", (d) => d.x)
|
|
||||||
.attr("cy", (d) => d.y);
|
|
||||||
|
|
||||||
text
|
|
||||||
.attr("x", (d) => d.x + 12) // Смещаем текст правее узла
|
|
||||||
.attr("y", (d) => d.y + 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
simulation.stop();
|
|
||||||
};
|
|
||||||
}, [data, onNodeClick]);
|
|
||||||
|
|
||||||
const drag = (simulation) => {
|
|
||||||
function dragstarted(event, d) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragged(event, d) {
|
|
||||||
d.fx = event.x;
|
|
||||||
d.fy = event.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragended(event, d) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <svg ref={chartRef}></svg>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TreeChart;
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
"title": "Сервис ВКС",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "Функциональные задачи",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "system_control",
|
|
||||||
"title": "Контроль системы"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "system_management",
|
|
||||||
"title": "Система управления"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "conference",
|
|
||||||
"title": "Проведение ВКС"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "backup",
|
|
||||||
"title": "Резервное копирование"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "relay_info",
|
|
||||||
"title": "Ретрансляция информации"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Аппаратное ПО",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "hardware_software_1",
|
|
||||||
"title": "ПО1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_2",
|
|
||||||
"title": "ПО2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_3",
|
|
||||||
"title": "ПО3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_4",
|
|
||||||
"title": "ПО4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_5",
|
|
||||||
"title": "ПО5"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import NetworkSpeedChart2 from '../Charts/TestCharts2';
|
|
||||||
|
|
||||||
const tabContent = {
|
|
||||||
service1: { title: "Сервис ВКС", content: <div><h2>Сервис ВКС</h2></div> },
|
|
||||||
system_control: { title: "Контроль системы", content: <div><h2>Контроль системы</h2><p>Описание контроля.</p></div> },
|
|
||||||
system_management: { title: "Система управления", content: <div><h2>Система управления</h2><p>Описание системы управления.</p></div> },
|
|
||||||
conference: { title: "Проведение ВКС", content: <div><h2>Проведение ВКС</h2><p>Информация о проведении ВКС.</p></div> },
|
|
||||||
backup: { title: "Резервное копирование", content: <div><h2>Резервное копирование</h2><p>Процесс резервного копирования.</p></div> },
|
|
||||||
relay_info: { title: "Ретрансляция информации", content: <div><h2>Ретрансляция информации</h2><p>Детали ретрансляции.</p></div> },
|
|
||||||
hardware_software_1: { title: "График скорости сети", content: <div><h2>График скорости сети</h2><NetworkSpeedChart2 /></div> },
|
|
||||||
hardware_software_2: { title: "ПО2", content: <div><h2>ПО2</h2></div> },
|
|
||||||
hardware_software_3: { title: "ПО3", content: <div><h2>ПО3</h2></div> },
|
|
||||||
hardware_software_4: { title: "ПО4", content: <div><h2>ПО4</h2></div> },
|
|
||||||
hardware_software_5: { title: "ПО5", content: <div><h2>ПО5</h2></div> },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tabContent;
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
.dashboard-container {
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
/* Запрещаем появление скролла */
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 400px;
|
|
||||||
max-width: calc(100vw - 250px);
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-y: auto;
|
|
||||||
/* Добавляем вертикальную прокрутку */
|
|
||||||
height: 100vh;
|
|
||||||
/* Ограничиваем высоту */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Вкладки */
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #222;
|
|
||||||
border-bottom: 2px solid #444;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
.error-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-item img {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-item span {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.critical span {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning span {
|
|
||||||
color: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
.expandable-info {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-button {
|
|
||||||
background-color: #444;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-button:hover {
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-menu {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal h2 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal button:hover {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
/* Боковое меню */
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background-color: #333;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-right: 1px solid #444;
|
|
||||||
height: 100vh;
|
|
||||||
/* Занимает всю высоту экрана */
|
|
||||||
overflow-y: auto;
|
|
||||||
/* Прокрутка внутри меню, если контент не помещается */
|
|
||||||
position: sticky;
|
|
||||||
/* Фиксируем меню */
|
|
||||||
top: 0;
|
|
||||||
/* Прилипаем к верху */
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-title {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: white
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #444;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item-header:hover {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submenu {
|
|
||||||
margin-left: 20px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #444;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab:hover {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
table-layout: fixed;
|
|
||||||
/* Фиксированная ширина столбцов */
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
width: 25%;
|
|
||||||
/* Равномерное распределение ширины для 4 столбцов */
|
|
||||||
padding: 10px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.normal {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.warning {
|
|
||||||
background-color: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.critical {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
caption {
|
|
||||||
position: relative;
|
|
||||||
margin-right: 100%;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,51 @@
|
||||||
|
/* SidebarMenu.css */
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #444;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-button:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-items {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
padding: 5px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import "./SidebarMenu.css"; // Импортируем стили для компонента
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
title: "Выбор сервиса",
|
||||||
|
items: ["Сервис ВКС", "Сервис 2", "Сервис 3"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Функциональные задачи",
|
||||||
|
items: ["Контроль системы", "Система управления", "Проведение ВКС", "Резервное копирование", "Ретрансляция информации"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Программное обеспечение",
|
||||||
|
items: ["ПО 1", "ПО 2", "ПО 3"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Аппаратное обеспечение",
|
||||||
|
items: ["Оборудование 1", "Оборудование 2", "Оборудование 3"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function SidebarMenu() {
|
||||||
|
const [openSections, setOpenSections] = useState({});
|
||||||
|
|
||||||
|
const toggleSection = (title) => {
|
||||||
|
setOpenSections((prev) => ({ ...prev, [title]: !prev[title] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sidebar">
|
||||||
|
<h2 className="sidebar-title">Меню</h2>
|
||||||
|
{menuItems.map((section) => (
|
||||||
|
<div key={section.title} className="sidebar-section">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarMenu;
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
|
root.render(<App />);
|
||||||
10
src/main.jsx
10
src/main.jsx
|
|
@ -1,10 +0,0 @@
|
||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import './index.css'
|
|
||||||
import App from './App.jsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
server: {
|
|
||||||
host: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue