Compare commits
No commits in common. "a3484553c35a0f911f4a30d82697f07bb6f428ec" and "ffc9f82e5aa9a45778b3a9d309e611c3e5bc4769" have entirely different histories.
a3484553c3
...
ffc9f82e5a
|
|
@ -1,27 +1 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
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 -it --name <unique-name> -v $(pwd)/src/:/app/src -v $(pwd)/public/:/app/public -p <hostPort>:5173 <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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
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
|
||||
|
|
@ -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,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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -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,31 +0,0 @@
|
|||
{
|
||||
"name": "trust-module",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5173",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"chart.js": "^4.0.0",
|
||||
"react-chartjs-2": "^5.0.0",
|
||||
"axios": "^1.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,9 +0,0 @@
|
|||
{
|
||||
"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]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import React from "react";
|
||||
import Dashboard from "./Components/Dashboard";
|
||||
import NetworkSpeedChart from './Charts/TestCharts';
|
||||
import NetworkSpeedChart2 from './Charts/TestCharts2'
|
||||
import NetworkSpeedChart3 from './Charts/TestCharts3'
|
||||
|
||||
function App() {
|
||||
/*return (
|
||||
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
|
||||
<Dashboard />
|
||||
<h1>График</h1>
|
||||
<CpuChart />
|
||||
</div>
|
||||
); */
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px" }}>
|
||||
<h1>Dashboard</h1>
|
||||
<Dashboard />
|
||||
<div style={{ marginBottom: "40px" }}>
|
||||
<h2>Примеры импорта данных</h2>
|
||||
<NetworkSpeedChart />
|
||||
<NetworkSpeedChart2 />
|
||||
<NetworkSpeedChart3 />
|
||||
</div>
|
||||
</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,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 NetworkSpeedChart2 = () => {
|
||||
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=node_time_seconds');
|
||||
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_time_seconds',
|
||||
},
|
||||
},
|
||||
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 NetworkSpeedChart2;
|
||||
|
|
@ -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 NetworkSpeedChart3 = () => {
|
||||
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=node_memory_MemAvailable_bytes');
|
||||
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_memory_MemAvailable_bytes',
|
||||
},
|
||||
},
|
||||
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 NetworkSpeedChart3;
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import SidebarMenu from "./SidebarMenu";
|
||||
import SystemStatusTable from "../Charts/SystemStatusTable";
|
||||
import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware";
|
||||
import "../Style/Dashboard.css";
|
||||
import ErrorIndicator from "./ErrorIndicator";
|
||||
|
||||
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 [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 (
|
||||
<div className="dashboard-container">
|
||||
<SidebarMenu onOpenTab={handleOpenTab} />
|
||||
|
||||
<div className="main-content">
|
||||
{/* Вкладки */}
|
||||
<div className="tabs">
|
||||
<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">
|
||||
{activeTab === "Главная" ? (
|
||||
<div>
|
||||
<h2>Общий мониторинг</h2>
|
||||
<ErrorIndicator />
|
||||
<SystemStatusTable />
|
||||
<SystemStatusTableSoftware />
|
||||
</div>
|
||||
) : (
|
||||
tabContent[activeTab] || <p>Нет контента</p>
|
||||
)}
|
||||
</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,93 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import "../Style/SidebarMenu.css";
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: "Выбор сервиса",
|
||||
items: ["Сервис ВКС", "Сервис 2", "Сервис 3"],
|
||||
},
|
||||
{
|
||||
title: "Функциональные задачи",
|
||||
items: ["Контроль системы", "Система управления", "Проведение ВКС", "Резервное копирование", "Ретрансляция информации"],
|
||||
},
|
||||
{
|
||||
title: "Программное обеспечение",
|
||||
items: [
|
||||
{
|
||||
title: "ПО 1",
|
||||
items: ["компонент ПО1", "компонент ПО2"],
|
||||
},
|
||||
{
|
||||
title: "ПО 2",
|
||||
items: ["компонент ПО3"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Аппаратное обеспечение",
|
||||
items: [
|
||||
{
|
||||
title: "Оборудование 1",
|
||||
items: ["компонент Оборудование 1"],
|
||||
},
|
||||
{
|
||||
title: "Оборудование 2",
|
||||
items: ["компонент Оборудование 2"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Рекурсивный компонент для отображения меню
|
||||
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={typeof child === "string" ? { title: child } : child}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Основной компонент SidebarMenu
|
||||
function SidebarMenu({ onOpenTab }) {
|
||||
const handleSelectItem = (item) => {
|
||||
onOpenTab(item.title); // Передаем название вкладки в родительский компонент
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<h2 className="sidebar-title">Уровень доверия:</h2>
|
||||
<h2 className="sidebar-title">Меню</h2>
|
||||
{menuItems.map((section, index) => (
|
||||
<MenuItem key={index} item={section} onSelectItem={handleSelectItem} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarMenu;
|
||||
|
|
@ -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,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,53 +0,0 @@
|
|||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed; /* Фиксированная ширина столбцов */
|
||||
}
|
||||
|
||||
th, td {
|
||||
width: 25%; /* Равномерное распределение ширины для 4 столбцов */
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.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 |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
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,31 +1,34 @@
|
|||
{
|
||||
"name": "trust-module",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5173",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"name": "trust_module",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"tailwindcss": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"chart.js": "^4.0.0",
|
||||
"react-chartjs-2": "^5.0.0",
|
||||
"axios": "^1.7.9"
|
||||
"cra-template": "^1.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"vite": "^6.0.5"
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"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]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 |
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;
|
||||
13
src/App.jsx
13
src/App.jsx
|
|
@ -1,13 +0,0 @@
|
|||
import React from "react";
|
||||
import ErrorIndicator from "./SidebarMenu/ErrorIndicator"; // Индикатор ошибок
|
||||
import Dashboard from "./SidebarMenu/Dashboard";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
|
||||
<Dashboard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,86 +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 "../SidebarMenu/ExpandableInfo"
|
||||
|
||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale);
|
||||
|
||||
const CpuTemperatureChart = () => {
|
||||
const chartRef = useRef(null);
|
||||
const [data, setData] = useState({
|
||||
labels: Array(10).fill("").map((_, i) => i), // 20 точек по X
|
||||
datasets: [
|
||||
{
|
||||
label: "Температура CPU (°C)",
|
||||
data: Array(20).fill(50), // Начальные значения (например, 50°C)
|
||||
borderColor: "red",
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
cubicInterpolationMode: "monotone", // Сглаживание
|
||||
tension: 0.4, // Делаем линию плавнее
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
//const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу
|
||||
const response = await axios.get(
|
||||
'http://backend:9101/metrics',
|
||||
{
|
||||
params: {
|
||||
metric: 'CPU'
|
||||
}
|
||||
}
|
||||
);
|
||||
setData({
|
||||
labels: response.data.labels,
|
||||
data: response.data,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка загрузки данных:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Пример данных для меню "Подробнее"
|
||||
const details = [
|
||||
{ label: "Использование", value: " 45%" },
|
||||
{ label: "Скорость", value: " 3.6 GHz" },
|
||||
{ label: "Процессы", value: " 120" },
|
||||
{ label: "Время работы", value: " 2 ч 30 мин" },
|
||||
];
|
||||
|
||||
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 CpuTemperatureChart;
|
||||
|
|
@ -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 "../SidebarMenu/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 "../SidebarMenu/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,88 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import SidebarMenu from "./SidebarMenu";
|
||||
import CpuTemperatureChart from "../Charts/CpuTemperatureChart";
|
||||
import GpuTemperatureChart from "../Charts/GpuTemperatureChart";
|
||||
import RamUsageChart from "../Charts/RamUsageChart";
|
||||
import "../Style/Dashboard.css";
|
||||
import ErrorIndicator from "./ErrorIndicator";
|
||||
|
||||
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 [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 (
|
||||
<div className="dashboard-container">
|
||||
<SidebarMenu onOpenTab={handleOpenTab} />
|
||||
|
||||
<div className="main-content">
|
||||
{/* Вкладки */}
|
||||
<div className="tabs">
|
||||
<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">
|
||||
{activeTab === "Главная" ? (
|
||||
<div>
|
||||
<h2>Общий мониторинг</h2>
|
||||
<ErrorIndicator />
|
||||
<CpuTemperatureChart />
|
||||
<GpuTemperatureChart />
|
||||
<RamUsageChart />
|
||||
</div>
|
||||
) : (
|
||||
tabContent[activeTab] || <p>Нет контента</p>
|
||||
)}
|
||||
</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,92 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import "../Style/SidebarMenu.css";
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: "Выбор сервиса",
|
||||
items: ["Сервис ВКС", "Сервис 2", "Сервис 3"],
|
||||
},
|
||||
{
|
||||
title: "Функциональные задачи",
|
||||
items: ["Контроль системы", "Система управления", "Проведение ВКС", "Резервное копирование", "Ретрансляция информации"],
|
||||
},
|
||||
{
|
||||
title: "Программное обеспечение",
|
||||
items: [
|
||||
{
|
||||
title: "ПО 1",
|
||||
items: ["компонент ПО1", "компонент ПО2"],
|
||||
},
|
||||
{
|
||||
title: "ПО 2",
|
||||
items: ["компонент ПО3"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Аппаратное обеспечение",
|
||||
items: [
|
||||
{
|
||||
title: "Оборудование 1",
|
||||
items: ["компонент Оборудование 1"],
|
||||
},
|
||||
{
|
||||
title: "Оборудование 2",
|
||||
items: ["компонент Оборудование 2"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Рекурсивный компонент для отображения меню
|
||||
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={typeof child === "string" ? { title: child } : child}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Основной компонент SidebarMenu
|
||||
function SidebarMenu({ onOpenTab }) {
|
||||
const handleSelectItem = (item) => {
|
||||
onOpenTab(item.title); // Передаем название вкладки в родительский компонент
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<h2 className="sidebar-title">Меню</h2>
|
||||
{menuItems.map((section, index) => (
|
||||
<MenuItem key={index} item={section} onSelectItem={handleSelectItem} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarMenu;
|
||||
|
|
@ -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,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;
|
||||
}
|
||||
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