From f8eab83bb475cd4bd102e526ba2cb3fecac4bbc6 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 18 Feb 2025 02:29:11 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=20=D1=84=D0=BE=D1=80=D0=BC=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=B5=D0=BD=D1=8E=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20json=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D1=8B,=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BC=D0=B5=D0=BD=D1=8E=20?= =?UTF-8?q?=D0=B2=20=D0=B2=D0=B8=D0=B4=D0=B5=20=D0=B3=D1=80=D0=B0=D1=84?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/App.jsx | 31 ++++----- src/Components/Dashboard.jsx | 50 ++++++++------ src/Components/SidebarMenu.jsx | 45 ++---------- src/Components/TreeChart.jsx | 121 +++++++++++++++++++++++++++++++++ src/Components/menuData.json | 69 +++++++++++++++++++ src/Components/tabContent.jsx | 14 ++++ 7 files changed, 256 insertions(+), 76 deletions(-) create mode 100644 src/Components/TreeChart.jsx create mode 100644 src/Components/menuData.json create mode 100644 src/Components/tabContent.jsx diff --git a/package.json b/package.json index 72c68e5..a4e7e89 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "chartjs-adapter-date-fns": "^3.0.0", + "d3": "^7.9.0", "react": "^18.3.1", "react-dom": "^18.3.1", "chart.js": "^4.0.0", diff --git a/src/App.jsx b/src/App.jsx index 304270b..1dc1979 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,26 +5,25 @@ import NetworkSpeedChart2 from './Charts/TestCharts2' import NetworkSpeedChart3 from './Charts/TestCharts3' function App() { - /*return ( + return (
-

График

- -
- ); */ - - return ( -
-

Dashboard

- -
-

Примеры импорта данных

- - - -
); + /* + return ( +
+

Dashboard

+ +
+

Примеры импорта данных

+ + + +
+
+ ); + */ } export default App; \ No newline at end of file diff --git a/src/Components/Dashboard.jsx b/src/Components/Dashboard.jsx index 51465f8..919c03e 100644 --- a/src/Components/Dashboard.jsx +++ b/src/Components/Dashboard.jsx @@ -1,24 +1,23 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import SidebarMenu from "./SidebarMenu"; import SystemStatusTable from "../Charts/SystemStatusTable"; import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware"; +import TreeChart from "./TreeChart"; // Подключаем граф import "../Style/Dashboard.css"; import ErrorIndicator from "./ErrorIndicator"; - -const tabContent = { - "Сервис ВКС":

Сервис 1

, - "Сервис 2":

Сервис 2

, - "Сервис 3":

Сервис 3

, - "Контроль системы":

Контроль системы

Описание контроля.

, - "Система управления":

Система управления

Описание системы управления.

, - "Проведение ВКС":

Проведение ВКС

Информация о проведении ВКС.

, - "Резервное копирование":

Резервное копирование

Процесс резервного копирования.

, - "Ретрансляция информации":

Ретрансляция информации

Детали ретрансляции.

, -}; +import tabContentData from "./tabContent"; +import menuData from "./menuData.json"; // Загружаем меню const Dashboard = () => { - const [tabs, setTabs] = useState([]); // Открытые вкладки - const [activeTab, setActiveTab] = useState("Главная"); // Текущая активная вкладка + const [tabs, setTabs] = useState([]); + const [activeTab, setActiveTab] = useState("Главная"); + const [tabContent, setTabContent] = useState({}); + const [treeData, setTreeData] = useState(null); + + useEffect(() => { + setTabContent(tabContentData); + setTreeData({ title: "Меню", items: menuData }); // Передаём данные в граф + }, []); const handleOpenTab = (tabName) => { if (!tabs.includes(tabName)) { @@ -28,7 +27,7 @@ const Dashboard = () => { }; const handleCloseTab = (tabName) => { - const newTabs = tabs.filter(tab => tab !== tabName); + const newTabs = tabs.filter((tab) => tab !== tabName); setTabs(newTabs); if (activeTab === tabName) { setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная"); @@ -40,7 +39,6 @@ const Dashboard = () => {
- {/* Вкладки */}
{ > Главная
- {tabs.map(tab => ( +
setActiveTab("Визуализация")} + > + Визуализация +
+ {tabs.map((tab) => (
{ {tab} @@ -65,7 +72,6 @@ const Dashboard = () => { ))}
- {/* Контент */}
{activeTab === "Главная" ? (
@@ -74,8 +80,10 @@ const Dashboard = () => {
+ ) : activeTab === "Визуализация" ? ( + handleOpenTab(node.title)} /> ) : ( - tabContent[activeTab] ||

Нет контента

+
Нет данных

" }} /> )}
@@ -83,4 +91,4 @@ const Dashboard = () => { ); }; -export default Dashboard; \ No newline at end of file +export default Dashboard; diff --git a/src/Components/SidebarMenu.jsx b/src/Components/SidebarMenu.jsx index a908741..92c0fd8 100644 --- a/src/Components/SidebarMenu.jsx +++ b/src/Components/SidebarMenu.jsx @@ -1,42 +1,8 @@ import React, { useState } from "react"; import "../Style/SidebarMenu.css"; +import menuData from "./menuData.json"; + -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 }) => { @@ -63,10 +29,11 @@ const MenuItem = ({ item, onSelectItem }) => { {item.items.map((child, index) => ( ))} +
)}
@@ -76,14 +43,14 @@ const MenuItem = ({ item, onSelectItem }) => { // Основной компонент SidebarMenu function SidebarMenu({ onOpenTab }) { const handleSelectItem = (item) => { - onOpenTab(item.title); // Передаем название вкладки в родительский компонент + onOpenTab(item.id, item.title); // Передаем и ID, и название }; return (

Уровень доверия:

Меню

- {menuItems.map((section, index) => ( + {menuData.map((section, index) => ( // Используем menuData вместо menuItems ))}
diff --git a/src/Components/TreeChart.jsx b/src/Components/TreeChart.jsx new file mode 100644 index 0000000..425319c --- /dev/null +++ b/src/Components/TreeChart.jsx @@ -0,0 +1,121 @@ +import React, { useRef, useEffect } from "react"; +import * as d3 from "d3"; + +const TreeChart = ({ data, onNodeClick }) => { + const chartRef = useRef(); + + useEffect(() => { + if (!data) return; + + // Очищаем старый граф перед отрисовкой + d3.select(chartRef.current).selectAll("*").remove(); + + const width = 928; + const height = 600; + + const root = d3.hierarchy(data, (d) => d.items); + const links = root.links(); + const nodes = root.descendants(); + + const simulation = d3 + .forceSimulation(nodes) + .force("link", d3.forceLink(links).id((d) => d.data.title).distance(80).strength(1)) // Увеличил дистанцию + .force("charge", d3.forceManyBody().strength(-500)) // Увеличил отталкивание узлов + .force("x", d3.forceX()) + .force("y", d3.forceY()); + + const svg = d3 + .select(chartRef.current) + .attr("width", width) + .attr("height", height) + .attr("viewBox", [-width / 2, -height / 2, width, height]) + .attr("style", "max-width: 100%; height: auto;"); + + const link = svg + .append("g") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6) + .selectAll("line") + .data(links) + .join("line"); + + const node = svg + .append("g") + .attr("stroke", "#000") + .attr("stroke-width", 1.5) + .selectAll("circle") + .data(nodes) + .join("circle") + .attr("fill", (d) => (d.children ? "#555" : "#000")) + .attr("stroke", "#fff") + .attr("r", 7) // Немного увеличил размер узлов для удобства клика + .call(drag(simulation)); + + // Добавляем текстовые подписи + const text = svg + .append("g") + .attr("fill", "#000") + .attr("font-family", "Arial") + .attr("font-size", 12) + .attr("pointer-events", "none") // Отключаем обработку событий текста + .selectAll("text") + .data(nodes) + .join("text") + .text((d) => d.data.title) + .attr("dx", 12) // Отодвигаем текст дальше от узла + .attr("dy", 4) // Немного поднимаем текст + + node.append("title").text((d) => d.data.title); + + node.on("click", (event, d) => { + if (onNodeClick) { + onNodeClick(d.data); + } + }); + + simulation.on("tick", () => { + link + .attr("x1", (d) => d.source.x) + .attr("y1", (d) => d.source.y) + .attr("x2", (d) => d.target.x) + .attr("y2", (d) => d.target.y); + + node + .attr("cx", (d) => d.x) + .attr("cy", (d) => d.y); + + text + .attr("x", (d) => d.x + 12) // Смещаем текст правее узла + .attr("y", (d) => d.y + 4); + }); + + return () => { + simulation.stop(); + }; + }, [data, onNodeClick]); + + const drag = (simulation) => { + function dragstarted(event, d) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + function dragended(event, d) { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); + }; + + return ; +}; + +export default TreeChart; diff --git a/src/Components/menuData.json b/src/Components/menuData.json new file mode 100644 index 0000000..bd7153e --- /dev/null +++ b/src/Components/menuData.json @@ -0,0 +1,69 @@ +[ + { + "title": "Выбор сервиса", + "items": [ + { + "id": "service1", + "title": "Сервис ВКС" + }, + { + "id": "service2", + "title": "Сервис 2" + }, + { + "id": "service3", + "title": "Сервис 3" + } + ] + }, + { + "title": "Функциональные задачи", + "items": [ + { + "id": "system_control", + "title": "Контроль системы" + }, + { + "id": "system_management", + "title": "Система управления" + }, + { + "id": "conference", + "title": "Проведение ВКС" + }, + { + "id": "backup", + "title": "Резервное копирование" + }, + { + "id": "relay_info", + "title": "Ретрансляция информации" + } + ] + }, + { + "title": "Аппаратное ПО", + "items": [ + { + "id": "hardware_software_1", + "title": "ПО1" + }, + { + "id": "hardware_software_2", + "title": "ПО2" + }, + { + "id": "hardware_software_3", + "title": "ПО3" + }, + { + "id": "hardware_software_4", + "title": "ПО4" + }, + { + "id": "hardware_software_5", + "title": "ПО5" + } + ] + } +] \ No newline at end of file diff --git a/src/Components/tabContent.jsx b/src/Components/tabContent.jsx new file mode 100644 index 0000000..8557a58 --- /dev/null +++ b/src/Components/tabContent.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +const tabContent = { + service1:

Сервис ВКС

, + service2:

Сервис 2

, + service3:

Сервис 3

, + system_control:

Контроль системы

Описание контроля.

, + system_management:

Система управления

Описание системы управления.

, + conference:

Проведение ВКС

Информация о проведении ВКС.

, + backup:

Резервное копирование

Процесс резервного копирования.

, + relay_info:

Ретрансляция информации

Детали ретрансляции.

, +}; + +export default tabContent; \ No newline at end of file From 517d3893bec0a15fd7ac8f8227c529907a6f9c8e Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Thu, 20 Feb 2025 12:01:48 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BE=D0=BA=D0=BD=D0=BE=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B7=D1=83=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=8E=20=D0=B2=20=D0=B2=D0=B8=D0=B4=D0=B5=20?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/data.json | 31 +++-- src/App.jsx | 35 +++--- src/Charts/TestCharts2.jsx | 112 ++++-------------- src/Charts/TestCharts3.jsx | 193 ++++++++------------------------ src/Components/Dashboard.jsx | 53 +++++---- src/Components/LoginModal.jsx | 49 ++++++++ src/Components/SidebarMenu.jsx | 27 ++--- src/Components/TreeChart.jsx | 2 +- src/Components/menuData.json | 124 +++++++++----------- src/Components/tabContent.jsx | 20 ++-- src/Style/LoginModal.css | 55 +++++++++ src/Style/SystemStatusTable.css | 106 +++++++++--------- 12 files changed, 371 insertions(+), 436 deletions(-) create mode 100644 src/Components/LoginModal.jsx create mode 100644 src/Style/LoginModal.css diff --git a/public/data.json b/public/data.json index 65e363b..e0b1e37 100644 --- a/public/data.json +++ b/public/data.json @@ -1,9 +1,22 @@ -{ - "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] - } - ] -} \ No newline at end of file +[ + { + "timestamp": "2025-02-18 12:00", + "value": 10 + }, + { + "timestamp": "2025-02-18 12:05", + "value": 12 + }, + { + "timestamp": "2025-02-18 12:10", + "value": 15 + }, + { + "timestamp": "2025-02-18 12:15", + "value": 13 + }, + { + "timestamp": "2025-02-18 12:20", + "value": 17 + } +] \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 1dc1979..21396db 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,29 +1,26 @@ -import React from "react"; +import React, { useState } from "react"; import Dashboard from "./Components/Dashboard"; -import NetworkSpeedChart from './Charts/TestCharts'; -import NetworkSpeedChart2 from './Charts/TestCharts2' -import NetworkSpeedChart3 from './Charts/TestCharts3' +import LoginModal from "./Components/LoginModal"; // Импортируем компонент авторизации +import "./Style/LoginModal.css"; // Импортируем стили function App() { + const [isAuthenticated, setIsAuthenticated] = useState(false); // Состояние авторизации + const [showLoginModal, setShowLoginModal] = useState(true); // Показывать ли модальное окно + + const handleLogin = () => { + setIsAuthenticated(true); // Устанавливаем авторизацию + setShowLoginModal(false); // Скрываем модальное окно + }; + return (
- + {!isAuthenticated && showLoginModal && ( + setShowLoginModal(false)} /> + )} + + {isAuthenticated && }
); - /* - return ( -
-

Dashboard

- -
-

Примеры импорта данных

- - - -
-
- ); - */ } export default App; \ No newline at end of file diff --git a/src/Charts/TestCharts2.jsx b/src/Charts/TestCharts2.jsx index 66dc35e..931e22b 100644 --- a/src/Charts/TestCharts2.jsx +++ b/src/Charts/TestCharts2.jsx @@ -12,9 +12,8 @@ import { Legend, TimeScale, } from 'chart.js'; -import 'chartjs-adapter-date-fns'; // Импортируем адаптер дат +import 'chartjs-adapter-date-fns'; -// Регистрируем компоненты Chart.js ChartJS.register( CategoryScale, LinearScale, @@ -23,143 +22,74 @@ ChartJS.register( Title, Tooltip, Legend, - TimeScale // Регистрируем временную шкалу + TimeScale ); +const MAX_DATA_POINTS = 50; + const NetworkSpeedChart2 = () => { - const [chartData, setChartData] = useState({ - labels: [], - datasets: [], - }); + 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); + if (!acc[entry.device]) acc[entry.device] = []; + acc[entry.device].push({ x: new Date(entry.timestamp), y: entry.value }); return acc; }, {}); - // Создаем новый набор данных const newDatasets = Object.keys(newGroupedData).map((device, index) => { - // Находим существующий dataset для этого устройства - const existingDataset = prevChartData.datasets.find((dataset) => dataset.label === `Device: ${device}`); + const existingDataset = prevChartData.datasets.find((d) => d.label === `Device: ${device}`); + const updatedData = existingDataset ? [...existingDataset.data, ...newGroupedData[device]] : newGroupedData[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, - })), + data: updatedData.slice(-MAX_DATA_POINTS), borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`, backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`, tension: 0.2, }; }); - // Обновляем labels (метки времени) - const newLabels = [ - ...prevChartData.labels, - ...newData.map((entry) => new Date(entry.timestamp)), - ]; - - return { - labels: newLabels, - datasets: newDatasets, - }; + return { labels: newDatasets[0]?.data.map((d) => d.x) || [], datasets: newDatasets }; }); } catch (error) { console.error('Ошибка при загрузке метрик:', error); } }; - // Загружаем данные при монтировании компонента и обновляем каждые 5 секунд useEffect(() => { fetchData(); const interval = setInterval(fetchData, 5000); - - // Очищаем интервал и уничтожаем график при размонтировании компонента - return () => { - clearInterval(interval); - if (chartRef.current) { - chartRef.current.destroy(); - } - }; + return () => clearInterval(interval); }, []); - // Опции графика const options = { responsive: true, plugins: { - legend: { - position: 'top', - }, - title: { - display: true, - text: 'node_time_seconds', - }, + 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: 'Данные', - }, + type: 'time', + time: { unit: 'second', displayFormats: { second: 'HH:mm:ss' } }, + title: { display: true, text: 'Time' }, }, + y: { title: { display: true, text: 'Value' } }, }, - animation: { - duration: 1000, // Длительность анимации - easing: 'linear', // Тип анимации - }, + animation: { duration: 1000, easing: 'linear' }, }; return (
- +
); }; -export default NetworkSpeedChart2; \ No newline at end of file +export default NetworkSpeedChart2; diff --git a/src/Charts/TestCharts3.jsx b/src/Charts/TestCharts3.jsx index 9b66a5e..cb8a36f 100644 --- a/src/Charts/TestCharts3.jsx +++ b/src/Charts/TestCharts3.jsx @@ -1,165 +1,68 @@ -import React, { useEffect, useState, useRef } from 'react'; -import axios from 'axios'; +import React, { useEffect, useState } from 'react'; 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'; // Импортируем адаптер дат +import axios from 'axios'; +import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale } from 'chart.js'; -// Регистрируем компоненты Chart.js -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - TimeScale // Регистрируем временную шкалу -); +// Регистрация компонентов Chart.js +ChartJS.register(Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale); -const NetworkSpeedChart3 = () => { - const [chartData, setChartData] = useState({ - labels: [], - datasets: [], - }); +const SimpleGraph = () => { + const [data, setData] = useState([]); - 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); + const fetchData = async () => { + try { + // Загружаем данные из файла с использованием axios + const response = await axios.get('/data.json'); // Путь должен быть относительно папки public + const rawData = response.data; - // Очищаем интервал и уничтожаем график при размонтировании компонента - return () => { - clearInterval(interval); - if (chartRef.current) { - chartRef.current.destroy(); + // Проверяем, что данные действительно массив + if (Array.isArray(rawData)) { + const chartData = rawData.map(item => ({ + timestamp: item.timestamp, + value: item.value, + })); + + setData(chartData); + } else { + throw new Error('Ошибка: Данные не являются массивом.'); + } + } catch (error) { + console.error('Error fetching data:', error); } }; + + fetchData(); }, []); - // Опции графика - const options = { + if (data.length === 0) return
Loading...
; + + // Настройки графика + const chartOptions = { responsive: true, plugins: { - legend: { - position: 'top', - }, title: { display: true, - text: 'node_memory_MemAvailable_bytes', + text: 'Simple Data Graph', }, }, - 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 ( -
- -
- ); + const chartData = { + labels: data.map(item => item.timestamp), // Массив меток для оси X + datasets: [ + { + label: 'Value', + data: data.map(item => item.value), // Массив значений для оси Y + borderColor: 'rgb(75, 192, 192)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + fill: false, + tension: 0.1, + }, + ], + }; + + return ; }; -export default NetworkSpeedChart3; \ No newline at end of file +export default SimpleGraph; diff --git a/src/Components/Dashboard.jsx b/src/Components/Dashboard.jsx index 919c03e..a1c9a34 100644 --- a/src/Components/Dashboard.jsx +++ b/src/Components/Dashboard.jsx @@ -2,11 +2,11 @@ import React, { useState, useEffect } from "react"; import SidebarMenu from "./SidebarMenu"; import SystemStatusTable from "../Charts/SystemStatusTable"; import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware"; -import TreeChart from "./TreeChart"; // Подключаем граф +import TreeChart from "./TreeChart"; import "../Style/Dashboard.css"; import ErrorIndicator from "./ErrorIndicator"; import tabContentData from "./tabContent"; -import menuData from "./menuData.json"; // Загружаем меню +import menuData from "./menuData.json"; // Загружаем новое меню const Dashboard = () => { const [tabs, setTabs] = useState([]); @@ -16,24 +16,42 @@ const Dashboard = () => { useEffect(() => { setTabContent(tabContentData); - setTreeData({ title: "Меню", items: menuData }); // Передаём данные в граф + setTreeData(menuData); // Теперь menuData - объект, а не массив }, []); - const handleOpenTab = (tabName) => { - if (!tabs.includes(tabName)) { - setTabs([...tabs, tabName]); + const handleOpenTab = (id, title) => { + if (!tabs.includes(id)) { + setTabs([...tabs, id]); } - setActiveTab(tabName); + setActiveTab(id); }; - const handleCloseTab = (tabName) => { - const newTabs = tabs.filter((tab) => tab !== tabName); + const handleCloseTab = (id) => { + const newTabs = tabs.filter((tab) => tab !== id); setTabs(newTabs); - if (activeTab === tabName) { + if (activeTab === id) { setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная"); } }; + const renderTabContent = () => { + if (activeTab === "Главная") { + return ( +
+

Общий мониторинг

+ + + +
+ ); + } else if (activeTab === "Визуализация") { + return handleOpenTab(id, title)} />; + } else { + const tabData = tabContent[activeTab]; + return tabData ? tabData.content :

Нет данных

; + } + }; + return (
@@ -73,22 +91,11 @@ const Dashboard = () => {
- {activeTab === "Главная" ? ( -
-

Общий мониторинг

- - - -
- ) : activeTab === "Визуализация" ? ( - handleOpenTab(node.title)} /> - ) : ( -
Нет данных

" }} /> - )} + {renderTabContent()}
); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/src/Components/LoginModal.jsx b/src/Components/LoginModal.jsx new file mode 100644 index 0000000..895df58 --- /dev/null +++ b/src/Components/LoginModal.jsx @@ -0,0 +1,49 @@ +import React, { useState } from "react"; + +const Login = ({ onLogin, onClose }) => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + if (username === "admin" && password === "admin") { + onLogin(); // Успешная авторизация + onClose(); // Закрыть модальное окно + } else { + setError("Неверный логин или пароль"); + } + }; + + return ( +
+
+

Авторизация

+
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ {error &&

{error}

} + +
+
+
+ ); +}; + +export default Login; \ No newline at end of file diff --git a/src/Components/SidebarMenu.jsx b/src/Components/SidebarMenu.jsx index 92c0fd8..2c5adb9 100644 --- a/src/Components/SidebarMenu.jsx +++ b/src/Components/SidebarMenu.jsx @@ -2,19 +2,15 @@ import React, { useState } from "react"; import "../Style/SidebarMenu.css"; import menuData from "./menuData.json"; - - -// Рекурсивный компонент для отображения меню const MenuItem = ({ item, onSelectItem }) => { const [isOpen, setIsOpen] = useState(false); - const hasChildren = Array.isArray(item.items) && item.items.length > 0; const handleClick = () => { if (hasChildren) { - setIsOpen(!isOpen); // Раскрываем/сворачиваем подменю + setIsOpen(!isOpen); } else { - onSelectItem(item); // Выбираем конечный элемент + onSelectItem(item); } }; @@ -27,34 +23,25 @@ const MenuItem = ({ item, onSelectItem }) => { {isOpen && hasChildren && (
{item.items.map((child, index) => ( - + ))} -
)} ); }; -// Основной компонент SidebarMenu function SidebarMenu({ onOpenTab }) { const handleSelectItem = (item) => { - onOpenTab(item.id, item.title); // Передаем и ID, и название - }; + onOpenTab(item.id, item.title); // Передаем id и title + }; return (
-

Уровень доверия:

Меню

- {menuData.map((section, index) => ( // Используем menuData вместо menuItems - - ))} +
); } -export default SidebarMenu; \ No newline at end of file +export default SidebarMenu; diff --git a/src/Components/TreeChart.jsx b/src/Components/TreeChart.jsx index 425319c..c008388 100644 --- a/src/Components/TreeChart.jsx +++ b/src/Components/TreeChart.jsx @@ -69,7 +69,7 @@ const TreeChart = ({ data, onNodeClick }) => { node.on("click", (event, d) => { if (onNodeClick) { - onNodeClick(d.data); + onNodeClick(d.data.id, d.data.title); // Передаем id и title } }); diff --git a/src/Components/menuData.json b/src/Components/menuData.json index bd7153e..daf3ef1 100644 --- a/src/Components/menuData.json +++ b/src/Components/menuData.json @@ -1,69 +1,55 @@ -[ - { - "title": "Выбор сервиса", - "items": [ - { - "id": "service1", - "title": "Сервис ВКС" - }, - { - "id": "service2", - "title": "Сервис 2" - }, - { - "id": "service3", - "title": "Сервис 3" - } - ] - }, - { - "title": "Функциональные задачи", - "items": [ - { - "id": "system_control", - "title": "Контроль системы" - }, - { - "id": "system_management", - "title": "Система управления" - }, - { - "id": "conference", - "title": "Проведение ВКС" - }, - { - "id": "backup", - "title": "Резервное копирование" - }, - { - "id": "relay_info", - "title": "Ретрансляция информации" - } - ] - }, - { - "title": "Аппаратное ПО", - "items": [ - { - "id": "hardware_software_1", - "title": "ПО1" - }, - { - "id": "hardware_software_2", - "title": "ПО2" - }, - { - "id": "hardware_software_3", - "title": "ПО3" - }, - { - "id": "hardware_software_4", - "title": "ПО4" - }, - { - "id": "hardware_software_5", - "title": "ПО5" - } - ] - } -] \ No newline at end of file +{ + "title": "Сервис ВКС", + "items": [ + { + "title": "Функциональные задачи", + "items": [ + { + "id": "system_control", + "title": "Контроль системы" + }, + { + "id": "system_management", + "title": "Система управления" + }, + { + "id": "conference", + "title": "Проведение ВКС" + }, + { + "id": "backup", + "title": "Резервное копирование" + }, + { + "id": "relay_info", + "title": "Ретрансляция информации" + } + ] + }, + { + "title": "Аппаратное ПО", + "items": [ + { + "id": "hardware_software_1", + "title": "ПО1" + }, + { + "id": "hardware_software_2", + "title": "ПО2" + }, + { + "id": "hardware_software_3", + "title": "ПО3" + }, + { + "id": "hardware_software_4", + "title": "ПО4" + }, + { + "id": "hardware_software_5", + "title": "ПО5" + } + ] + } + ] +} diff --git a/src/Components/tabContent.jsx b/src/Components/tabContent.jsx index 8557a58..9f7aa40 100644 --- a/src/Components/tabContent.jsx +++ b/src/Components/tabContent.jsx @@ -1,14 +1,18 @@ import React from "react"; +import NetworkSpeedChart2 from '../Charts/TestCharts2'; const tabContent = { - service1:

Сервис ВКС

, - service2:

Сервис 2

, - service3:

Сервис 3

, - system_control:

Контроль системы

Описание контроля.

, - system_management:

Система управления

Описание системы управления.

, - conference:

Проведение ВКС

Информация о проведении ВКС.

, - backup:

Резервное копирование

Процесс резервного копирования.

, - relay_info:

Ретрансляция информации

Детали ретрансляции.

, + service1: { title: "Сервис ВКС", content:

Сервис ВКС

}, + system_control: { title: "Контроль системы", content:

Контроль системы

Описание контроля.

}, + system_management: { title: "Система управления", content:

Система управления

Описание системы управления.

}, + conference: { title: "Проведение ВКС", content:

Проведение ВКС

Информация о проведении ВКС.

}, + backup: { title: "Резервное копирование", content:

Резервное копирование

Процесс резервного копирования.

}, + relay_info: { title: "Ретрансляция информации", content:

Ретрансляция информации

Детали ретрансляции.

}, + hardware_software_1: { title: "График скорости сети", content:

График скорости сети

}, + hardware_software_2: { title: "ПО2", content:

ПО2

}, + hardware_software_3: { title: "ПО3", content:

ПО3

}, + hardware_software_4: { title: "ПО4", content:

ПО4

}, + hardware_software_5: { title: "ПО5", content:

ПО5

}, }; export default tabContent; \ No newline at end of file diff --git a/src/Style/LoginModal.css b/src/Style/LoginModal.css new file mode 100644 index 0000000..0d6d28c --- /dev/null +++ b/src/Style/LoginModal.css @@ -0,0 +1,55 @@ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; +} + +.modal { + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + max-width: 400px; + width: 100%; +} + +.modal h2 { + margin-bottom: 20px; +} + +.modal label { + display: block; + margin-bottom: 5px; +} + +.modal input { + width: 100%; + padding: 8px; + margin-bottom: 10px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.modal button { + padding: 10px 20px; + background: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.modal button:hover { + background: #0056b3; +} + +.error { + color: red; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/src/Style/SystemStatusTable.css b/src/Style/SystemStatusTable.css index bd14d6a..5711e1e 100644 --- a/src/Style/SystemStatusTable.css +++ b/src/Style/SystemStatusTable.css @@ -1,53 +1,57 @@ 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; - } + width: 100%; + table-layout: fixed; + /* Фиксированная ширина столбцов */ +} - caption { - position: relative; - margin-right: 100% ; - } \ No newline at end of file +th, +td { + width: 25%; + /* Равномерное распределение ширины для 4 столбцов */ + padding: 10px; + text-align: left; + border-bottom: 1px solid #ddd; + color: #333; +} + +.status { + padding: 5px 10px; + border-radius: 5px; + color: white; +} + +.status.normal { + background-color: green; +} + +.status.warning { + background-color: orange; +} + +.status.critical { + background-color: red; +} + +.details { + padding: 10px; + background-color: #f9f9f9; + border-radius: 5px; +} + +button { + background-color: #007bff; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +caption { + position: relative; + margin-right: 100%; +} \ No newline at end of file