Compare commits
8 Commits
ffc9f82e5a
...
a3484553c3
| Author | SHA1 | Date |
|---|---|---|
|
|
a3484553c3 | |
|
|
637b559fe8 | |
|
|
46a8bbb35d | |
|
|
01f0be9cdd | |
|
|
e9fbc9bdc4 | |
|
|
2e283bf9d7 | |
|
|
42aa7e2f52 | |
|
|
402f38b12e |
|
|
@ -1 +1,27 @@
|
||||||
|
# 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
|
node_modules
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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"]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
Запуск проекта
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 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
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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"]
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!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
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Число участников конференции",
|
||||||
|
"value": 50,
|
||||||
|
"status": "normal",
|
||||||
|
"details": "Число участников конференции в пределах нормы."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Подключение пользователей",
|
||||||
|
"value": 75,
|
||||||
|
"status": "warning",
|
||||||
|
"details": "Пользователи имеют проблемы с подключением."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Максимальное число комнат",
|
||||||
|
"value": 100,
|
||||||
|
"status": "critical",
|
||||||
|
"details": "Количество комнат превысило норму."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"data": [50, 52, 55, 53, 60, 58, 65, 62, 66, 72],
|
||||||
|
"data2": [20,56,74,45,21,20,56,74,45,21]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 729 B |
|
|
@ -0,0 +1,23 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
#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,30 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* Боковое меню */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
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.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,74 @@
|
||||||
|
: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,10 @@
|
||||||
|
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>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!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,34 +1,31 @@
|
||||||
{
|
{
|
||||||
"name": "trust_module",
|
"name": "trust-module",
|
||||||
"version": "0.1.0",
|
"private": true,
|
||||||
"devDependencies": {
|
"version": "0.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"type": "module",
|
||||||
"postcss": "^8.5.1",
|
"scripts": {
|
||||||
"postcss-cli": "^11.0.0",
|
"dev": "vite --port 5173",
|
||||||
"tailwindcss": "^4.0.1"
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cra-template": "^1.2.0",
|
"react": "^18.3.1",
|
||||||
"react": "^19.0.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-dom": "^19.0.0",
|
"chart.js": "^4.0.0",
|
||||||
"react-scripts": "^5.0.1"
|
"react-chartjs-2": "^5.0.0",
|
||||||
|
"axios": "^1.7.9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"devDependencies": {
|
||||||
"start": "react-scripts start",
|
"@eslint/js": "^9.17.0",
|
||||||
"build": "react-scripts build",
|
"@types/react": "^18.3.18",
|
||||||
"test": "react-scripts test",
|
"@types/react-dom": "^18.3.5",
|
||||||
"eject": "react-scripts eject"
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
},
|
"eslint": "^9.17.0",
|
||||||
"browserslist": {
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"production": [
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
">0.2%",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
"not dead",
|
"globals": "^15.14.0",
|
||||||
"not op_mini all"
|
"vite": "^6.0.5"
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"data": [50, 52, 55, 53, 60, 58, 65, 62, 66, 72],
|
||||||
|
"data2": [20,56,74,45,21,20,56,74,45,21]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 729 B |
|
|
@ -0,0 +1,45 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
13
src/App.js
13
src/App.js
|
|
@ -1,13 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* Боковое меню */
|
||||||
|
.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.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,51 +0,0 @@
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
: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,6 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
|
||||||
root.render(<App />);
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
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>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
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