diff --git a/Jenkinsfile b/Jenkinsfile index 93053b4..83ae4af 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -41,7 +41,6 @@ pipeline { always { script { echo "Cleaning up workspace..." - sh "rm -rf ${env.WORKSPACE}/package/ || true" sh "rm -rf ${env.WORKSPACE}/rc/ || true" } } @@ -56,7 +55,7 @@ pipeline { -u "${GITEA_USER}:${GITEA_PASS}" \ -H "Content-Type: application/json" \ -d '{"do":"merge"}' \ - http://git.entcor/api/v1/repos/DmitriyA/trust-module-frontend/pulls/${prId}/merge + http://git.entcor/api/v1/repos/deployer3000/trust-module-frontend/pulls/${prId}/merge """ echo "PR ${prId} merged successfully into master!" } diff --git a/package.json b/package.json index a4e7e89..9d0c2c7 100755 --- a/package.json +++ b/package.json @@ -11,12 +11,15 @@ }, "dependencies": { "chartjs-adapter-date-fns": "^3.0.0", + "recharts": "^2.15.1", "d3": "^7.9.0", "react": "^18.3.1", "react-dom": "^18.3.1", "chart.js": "^4.0.0", + "chartjs-chart-box-and-violin-plot": "^4.0.0", "react-chartjs-2": "^5.0.0", - "axios": "^1.7.9" + "axios": "^1.7.9", + "react-datepicker": "^8.1.0" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/App.jsx b/src/App.jsx index 21396db..f127d25 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import Dashboard from "./Components/Dashboard"; -import LoginModal from "./Components/LoginModal"; // Импортируем компонент авторизации +import Dashboard from "./Components/Layout/Dashboard"; +import LoginModal from "./Components/UI/LoginModal"; // Импортируем компонент авторизации import "./Style/LoginModal.css"; // Импортируем стили function App() { diff --git a/src/Charts/Components/BarChartComponent.jsx b/src/Charts/Components/BarChartComponent.jsx new file mode 100644 index 0000000..b0d425c --- /dev/null +++ b/src/Charts/Components/BarChartComponent.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { BarChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Bar, ResponsiveContainer } from 'recharts'; + +const BarChartComponent = ({ chartData, metricName, metricType, colors }) => { + // Преобразуем данные для отображения + const data = Object.keys(chartData).map(instance => { + const instanceData = chartData[instance].reduce((acc, point) => { + if (point.value !== null) { + acc[point.quantile] = point.value; + } + return acc; + }, {}); + return { instance, ...instanceData }; + }); + + // Получаем все уникальные квантили + const allQuantiles = [...new Set( + Object.values(chartData).flat().map(point => point.quantile) + )]; + + return ( +
+

{metricName} ({metricType})

+ + + + + + + + {allQuantiles.map((quantile, index) => ( + + ))} + + +
+ ); +}; + +export default BarChartComponent; \ No newline at end of file diff --git a/src/Charts/Components/CounterComponent.jsx b/src/Charts/Components/CounterComponent.jsx new file mode 100644 index 0000000..209076a --- /dev/null +++ b/src/Charts/Components/CounterComponent.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const CounterComponent = ({ value, metricName }) => { + return ( +
+

{metricName}

+

{value}

+
+ ); +}; + +export default CounterComponent; \ No newline at end of file diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx new file mode 100644 index 0000000..37d76da --- /dev/null +++ b/src/Charts/Components/LineChartComponent.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line, ResponsiveContainer, Brush } from 'recharts'; + +const LineChartComponent = ({ chartData, metricName, metricType, colors, description }) => { + // Создаем массив уникальных временных меток + const allTimes = Object.values(chartData) + .flat() + .map(point => point.time) + .filter((time, index, self) => self.indexOf(time) === index); + + // Формируем данные для графика + const data = allTimes.map(time => { + const point = { time }; + Object.keys(chartData).forEach(key => { + const instanceData = chartData[key].find(p => p.time === time); + point[key] = instanceData ? instanceData.value : null; + }); + return point; + }); + + // Кастомный Tooltip для отображения значения + const CustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{`Время: ${label}`}

{/* Время из label */} + {payload.map((entry, index) => ( +

+ {`Значение: ${entry.value}`} {/* Имя и значение из payload */} +

+ ))} +
+ ); + } + + return null; + }; + + return ( +
+

{description}

+ + + + + + } /> {/* Подключаем кастомный Tooltip */} + + {Object.keys(chartData).map((key, index) => ( + + ))} + + + +
+ ); +}; + +export default LineChartComponent; \ No newline at end of file diff --git a/src/Charts/Components/ScatterChartComponent.jsx b/src/Charts/Components/ScatterChartComponent.jsx new file mode 100644 index 0000000..15d5e90 --- /dev/null +++ b/src/Charts/Components/ScatterChartComponent.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { ScatterChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Scatter, ResponsiveContainer } from 'recharts'; + +const ScatterChartComponent = ({ chartData, metricName, metricType, colors }) => { + return ( +
+

{metricName} ({metricType})

+ + + + + + + + {Object.keys(chartData).map((instance, index) => ( + + ))} + + +
+ ); +}; + +export default ScatterChartComponent; \ No newline at end of file diff --git a/src/Charts/GpuTemperatureChart.jsx b/src/Charts/GpuTemperatureChart.jsx deleted file mode 100644 index a8a71d0..0000000 --- a/src/Charts/GpuTemperatureChart.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Line } from "react-chartjs-2"; -import axios from "axios"; -import { - Chart as ChartJS, - LineElement, - PointElement, - LinearScale, - CategoryScale, -} from "chart.js"; -import ExpandableInfo from "../Components/ExpandableInfo" - -ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale); - -const GpuTemperatureChart = () => { - const chartRef = useRef(null); - const [data, setData] = useState({ - labels: Array(10).fill("").map((_, i) => i), // 20 точек по X - datasets: [ - { - label: "Температура GPU (°C)", - data: [], // Начальные значения (например, 50°C) - borderColor: "blue", - borderWidth: 2, - fill: false, - cubicInterpolationMode: "monotone", // Сглаживание - tension: 0.4, // Делаем линию плавнее - }, - ], - }); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу - setData({ - labels: response.data.labels, - datasets: [{ ...data.datasets[0], data: response.data.datasets[0].data }], - }); - } catch (error) { - console.error("Ошибка загрузки данных:", error); - } - }; - - fetchData(); - const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд - - return () => clearInterval(interval); - }, []); - - // Пример данных для меню "Подробнее" - const details = [ - { label: "Использование", value: " 20%" }, - { label: "Оперативная память ГП", value: " 1,2/7,9 ГБ" }, - { label: "Общая память ГП", value: " 1,2/7,9 ГБ" }, - ]; - - return ( -
-

График температуры ГП

- - -
- ); -}; - -export default GpuTemperatureChart; \ No newline at end of file diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx new file mode 100644 index 0000000..d91f9cc --- /dev/null +++ b/src/Charts/PrometheusChart.jsx @@ -0,0 +1,197 @@ +import React, { useEffect, useState, useRef } from 'react'; +import axios from 'axios'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import LineChartComponent from './Components/LineChartComponent'; + +const MAX_POINTS = 20; // Ограничение точек на графике +const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; // Фиксированные цвета для линий + +// Список временных диапазонов и интервалов обновления +const TIME_RANGES = [ + { label: '1 минута', value: 60, interval: 3000 }, + { label: '5 минут', value: 300, interval: 15000 }, + { label: '30 минут', value: 1800, interval: 90000 }, + { label: '1 час', value: 3600, interval: 180000 }, + { label: '3 часа', value: 10800, interval: 540000 }, + { label: '6 часов', value: 21600, interval: 1080000 }, + { label: '12 часов', value: 43200, interval: 2160000 }, + { label: '24 часа', value: 86400, interval: 4320000 }, + { label: '2 дня', value: 172800, interval: 8640000 }, + { label: '7 дней', value: 604800, interval: 30240000 }, + { label: '30 дней', value: 2592000, interval: 129600000 }, + { label: '90 дней', value: 7776000, interval: 388800000 }, + { label: '6 месяцев', value: 15552000, interval: 777600000 }, + { label: '9 месяцев', value: 23328000, interval: 1166400000 }, + { label: '1 год', value: 31536000, interval: 1576800000 }, +]; + +const PrometheusChart = ({ metricName }) => { + const [chartData, setChartData] = useState({}); + const [metricType, setMetricType] = useState(''); + const [metricDescription, setMetricDescription] = useState(''); + const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); // По умолчанию 1 минута + const [startDate, setStartDate] = useState(new Date()); // Начальная дата для кастомного диапазона + const [endDate, setEndDate] = useState(new Date()); // Конечная дата для кастомного диапазона + const [useCustomRange, setUseCustomRange] = useState(false); // Флаг для выбора кастомного диапазона + const [brushRange, setBrushRange] = useState({ startIndex: 0, endIndex: 0 }); // Состояние Brush + const intervalRef = useRef(null); + + const fetchData = async () => { + try { + let start, end; + + if (useCustomRange) { + // Используем кастомный диапазон + start = Math.floor(startDate.getTime() / 1000); + end = Math.floor(endDate.getTime() / 1000); + } else { + // Используем предустановленный диапазон + end = Math.floor(Date.now() / 1000); + start = end - selectedRange.value; + } + + // Динамический шаг (чем больше диапазон, тем больше шаг) + let step; + const range = end - start; + if (range <= 3600) step = 5; // 1 час и меньше → 5 сек + else if (range <= 21600) step = 30; // 1-6 часов → 30 сек + else if (range <= 86400) step = 120; // 6-24 часа → 2 минуты + else step = 300; // > 24 часов → 5 минут + + console.log(`Запрашиваем данные с шагом ${step} сек`); + + const response = await axios.get('http://192.168.2.39:3000/metrics', { + params: { metric: metricName, start, end, step }, + }); + + const result = response.data; + let metrics = Array.isArray(result) ? result : result.data || []; + + if (!Array.isArray(metrics) || metrics.length === 0) { + console.warn('No metrics data available, filling with empty values.'); + metrics = []; + } + + // 1. Генерация временных точек с учетом диапазона + const timePoints = []; + for (let t = start; t <= end; t += step) { + const date = new Date(t * 1000); + const formattedTime = range > 86400 + ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) + : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + + timePoints.push(formattedTime); + } + + // 2. Обработка данных + const updatedData = {}; + metrics.forEach(m => { + const date = new Date(m.timestamp); + const formattedTime = range > 86400 + ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) + : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + + const key = `${m.instance}-${m.device || m.scrape_job}`; + if (!updatedData[key]) updatedData[key] = {}; + updatedData[key][formattedTime] = m.value; + }); + + // 3. Заполнение пропусков + const chartData = {}; + Object.keys(updatedData).forEach(key => { + chartData[key] = timePoints.map(time => ({ + time, + value: updatedData[key][time] ?? null, + })); + }); + + setChartData(chartData); + + // Устанавливаем Brush на весь диапазон + setBrushRange({ + startIndex: 0, + endIndex: timePoints.length - 1, + }); + } catch (error) { + console.error('Ошибка при загрузке метрик:', error); + } + }; + + useEffect(() => { + fetchData(); // Первоначальная загрузка данных + + intervalRef.current = setInterval(() => { + fetchData(); + }, selectedRange.interval); // Обновляем с выбранным интервалом + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); // Очищаем интервал при размонтировании + } + }; + }, [metricName, selectedRange, useCustomRange, startDate, endDate]); // Зависимость от metricName, selectedRange, useCustomRange, startDate и endDate + + const handleRangeChange = (event) => { + const selectedValue = event.target.value; + const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10)); + setSelectedRange(range); + setUseCustomRange(false); // Переключаемся на предустановленный диапазон + }; + + const handleCustomRangeChange = () => { + setUseCustomRange(true); // Переключаемся на кастомный диапазон + }; + + if (!Object.keys(chartData).length) return

Loading...

; + + return ( +
+
+ + +
+
+ +
+ + setStartDate(date)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={15} + dateFormat="yyyy-MM-dd HH:mm" + /> +
+
+ + setEndDate(date)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={15} + dateFormat="yyyy-MM-dd HH:mm" + /> +
+ +
+ +
+ ); +}; + +export default PrometheusChart; \ No newline at end of file diff --git a/src/Charts/RamUsageChart.jsx b/src/Charts/RamUsageChart.jsx deleted file mode 100644 index 26f5555..0000000 --- a/src/Charts/RamUsageChart.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Line } from "react-chartjs-2"; -import { - Chart as ChartJS, - LineElement, - PointElement, - LinearScale, - CategoryScale, -} from "chart.js"; -import ExpandableInfo from "../Components/ExpandableInfo" - -ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale); - -const RamUsageChart = () => { - const chartRef = useRef(null); - const [data, setData] = useState({ - labels: Array(10).fill("").map((_, i) => i), // 20 точек по X - datasets: [ - { - label: "Загруженность RAM (%)", - data: Array(20).fill(50), // Начальные значения (например, 50%) - borderColor: "green", - borderWidth: 2, - fill: false, - cubicInterpolationMode: "monotone", // Сглаживание - tension: 0.4, // Делаем линию плавнее - }, - ], - }); - - useEffect(() => { - const interval = setInterval(() => { - setData((prevData) => { - const newTemp = Math.floor(Math.random() * 20) + 40; // Генерируем новую температуру (50-600°C) - const newLabels = [...prevData.labels.slice(1), prevData.labels[prevData.labels.length - 1] + 1]; // Сдвигаем ось X - const newDataset = [...prevData.datasets[0].data.slice(1), newTemp]; // Сдвигаем данные влево - - return { - labels: newLabels, - datasets: [{ ...prevData.datasets[0], data: newDataset }], - }; - }); - }, 1000); // Обновление каждую секунду - - return () => clearInterval(interval); - }, []); - - // Пример данных для меню "Подробнее" - const details = [ - { label: "Используется", value: " 6,2 ГБ" }, - { label: "Доступно", value: " 9,5 ГБ" }, - { label: "Выделено", value: " 6,8/18,2 ГБ" }, - { label: "Скорость", value: " 3200 МГц" }, - - ]; - - return ( -
-

График загруженности ОЗУ

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

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

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

Нет данных

; - } - }; - - return ( -
- - -
-
-
setActiveTab("Главная")} - > - Главная -
-
setActiveTab("Визуализация")} - > - Визуализация -
- {tabs.map((tab) => ( -
setActiveTab(tab)} - > - {tab} - -
- ))} -
- -
- {renderTabContent()} -
-
-
- ); -}; - -export default Dashboard; \ No newline at end of file diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx new file mode 100644 index 0000000..ce9c706 --- /dev/null +++ b/src/Components/Layout/Dashboard.jsx @@ -0,0 +1,139 @@ +import React, { useState, useEffect, useRef } from "react"; +import SidebarMenu from "./SidebarMenu"; +import TreeChart from "../TreeChart/TreeChart"; +import "../../Style/Dashboard.css"; +import ErrorIndicator from "../UI/ErrorIndicator"; +import Tabs from "../UI/Tabs"; +import menuData from "../TreeChart/menuData.json"; // Импортируем JSON-данные +import TreeTable from "../UI/TreeTable"; +import { updateStatuses } from "../TreeChart/dataUtils"; // Функция обновления статусов +import generateTabContent from "../TreeChart/tabContent"; // Импортируем функцию generateTabContent + +const Dashboard = () => { + const [tabs, setTabs] = useState([]); + const [activeTab, setActiveTab] = useState("Главная"); + const [tabContent, setTabContent] = useState({}); // Состояние для контента вкладок + const [treeData, setTreeData] = useState(menuData); // Загружаем меню в state + const [sidebarWidth, setSidebarWidth] = useState(250); // Начальная ширина сайдбара + const [isResizing, setIsResizing] = useState(false); // Состояние перетаскивания + const sidebarRef = useRef(null); // Референс на сайдбар + + // Генерация контента для вкладок на основе menuData + useEffect(() => { + const generatedTabContent = generateTabContent(menuData); + setTabContent(generatedTabContent); + }, []); + + // Обновление treeData каждые 30 секунд + useEffect(() => { + const interval = setInterval(() => { + setTreeData((prevData) => { + const updatedData = JSON.parse(JSON.stringify(prevData)); // Клонируем данные + updateStatuses(updatedData); // Обновляем статусы + return updatedData; + }); + }, 30000); + + return () => clearInterval(interval); + }, []); + + // Обработчик начала перетаскивания + const startResizing = (e) => { + e.preventDefault(); + setIsResizing(true); + }; + + // Обработчик движения мыши + const resize = (e) => { + if (isResizing) { + const newWidth = e.clientX; // Новая ширина сайдбара + if (newWidth > 100 && newWidth < 400) { // Ограничиваем минимальную и максимальную ширину + setSidebarWidth(newWidth); + } + } + }; + + // Обработчик окончания перетаскивания + const stopResizing = () => { + setIsResizing(false); + }; + + // Добавляем обработчики событий + useEffect(() => { + const handleMouseMove = (e) => resize(e); + const handleMouseUp = () => stopResizing(); + + if (isResizing) { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + } + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing]); + + const handleOpenTab = (id, title) => { + if (!tabs.some((tab) => tab.id === id)) { + setTabs([...tabs, { id, title }]); + } + setActiveTab(id); + }; + + const handleCloseTab = (id) => { + const newTabs = tabs.filter((tab) => tab.id !== id); + setTabs(newTabs); + if (activeTab === id) { + setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1].id : "Главная"); + } + }; + + const renderTabContent = () => { + if (activeTab === "Главная") { + return ( +
+

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

+ + {/* Используем актуальные данные */} +
+ ); + } else if (activeTab === "Визуализация") { + return handleOpenTab(id, title)} />; + } else { + const tabData = tabContent[activeTab]; + return tabData ? tabData.content :

Нет данных

; + } + }; + + return ( +
+
+ + {/* Элемент для перетаскивания */} +
+
+ +
+ setActiveTab(id)} + onCloseTab={handleCloseTab} + /> +
+ {renderTabContent()} +
+
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx new file mode 100644 index 0000000..5661716 --- /dev/null +++ b/src/Components/Layout/SidebarMenu.jsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import "../../Style/SidebarMenu.css"; +import { getStatusColor } from "../TreeChart/dataUtils"; // Импортируем только нужную функцию + +const MenuItem = ({ item, onSelectItem, sidebarWidth }) => { + const [isOpen, setIsOpen] = useState(false); + const hasChildren = Array.isArray(item.items) && item.items.length > 0; + const statusColor = getStatusColor(item.status); + + // Обработчик одинарного клика (разворачивание/сворачивание или открытие элемента) + const handleSingleClick = () => { + if (hasChildren) { + setIsOpen(!isOpen); // Разворачиваем/сворачиваем дочерние элементы + } else { + onSelectItem(item); // Если нет потомков, открываем элемент как вкладку + } + }; + + // Обработчик клика для открытия родителя + const handleOpenParent = (e) => { + e.stopPropagation(); // Останавливаем всплытие события, чтобы не сработал handleSingleClick + onSelectItem(item); // Открываем родителя + }; + + return ( +
{/* Динамическая ширина */} +
+ {/* Круглый индикатор статуса */} +
+ {item.title} + + {/* Иконка для открытия родителя */} + {hasChildren && ( + + 📂 + + )} + + {/* Иконка для разворачивания/сворачивания */} + {hasChildren && {isOpen ? "▲" : "▼"}} +
+ {isOpen && hasChildren && ( +
+ {item.items.map((child, index) => ( + + ))} +
+ )} +
+ ); +}; + +function SidebarMenu({ data, onOpenTab, sidebarWidth }) { + const handleSelectItem = (item) => { + onOpenTab(item.id, item.title); + }; + + return ( +
+
{/* Динамическая ширина */} +

Меню

+ +
+
{/* Динамическая ширина */} +

Помощь

+

Настройка

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

Авторизация

-
-
- - 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 deleted file mode 100644 index 2c5adb9..0000000 --- a/src/Components/SidebarMenu.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useState } from "react"; -import "../Style/SidebarMenu.css"; -import menuData from "./menuData.json"; - -const MenuItem = ({ item, onSelectItem }) => { - const [isOpen, setIsOpen] = useState(false); - const hasChildren = Array.isArray(item.items) && item.items.length > 0; - - const handleClick = () => { - if (hasChildren) { - setIsOpen(!isOpen); - } else { - onSelectItem(item); - } - }; - - return ( -
-
- {item.title} - {hasChildren && {isOpen ? "▲" : "▼"}} -
- {isOpen && hasChildren && ( -
- {item.items.map((child, index) => ( - - ))} -
- )} -
- ); -}; - -function SidebarMenu({ onOpenTab }) { - const handleSelectItem = (item) => { - onOpenTab(item.id, item.title); // Передаем id и title - }; - - return ( -
-

Меню

- -
- ); -} - -export default SidebarMenu; diff --git a/src/Components/TreeChart.jsx b/src/Components/TreeChart.jsx deleted file mode 100644 index c008388..0000000 --- a/src/Components/TreeChart.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useRef, useEffect } from "react"; -import * as d3 from "d3"; - -const TreeChart = ({ data, onNodeClick }) => { - const chartRef = useRef(); - - useEffect(() => { - if (!data) return; - - // Очищаем старый граф перед отрисовкой - d3.select(chartRef.current).selectAll("*").remove(); - - const width = 928; - const height = 600; - - const root = d3.hierarchy(data, (d) => d.items); - const links = root.links(); - const nodes = root.descendants(); - - const simulation = d3 - .forceSimulation(nodes) - .force("link", d3.forceLink(links).id((d) => d.data.title).distance(80).strength(1)) // Увеличил дистанцию - .force("charge", d3.forceManyBody().strength(-500)) // Увеличил отталкивание узлов - .force("x", d3.forceX()) - .force("y", d3.forceY()); - - const svg = d3 - .select(chartRef.current) - .attr("width", width) - .attr("height", height) - .attr("viewBox", [-width / 2, -height / 2, width, height]) - .attr("style", "max-width: 100%; height: auto;"); - - const link = svg - .append("g") - .attr("stroke", "#999") - .attr("stroke-opacity", 0.6) - .selectAll("line") - .data(links) - .join("line"); - - const node = svg - .append("g") - .attr("stroke", "#000") - .attr("stroke-width", 1.5) - .selectAll("circle") - .data(nodes) - .join("circle") - .attr("fill", (d) => (d.children ? "#555" : "#000")) - .attr("stroke", "#fff") - .attr("r", 7) // Немного увеличил размер узлов для удобства клика - .call(drag(simulation)); - - // Добавляем текстовые подписи - const text = svg - .append("g") - .attr("fill", "#000") - .attr("font-family", "Arial") - .attr("font-size", 12) - .attr("pointer-events", "none") // Отключаем обработку событий текста - .selectAll("text") - .data(nodes) - .join("text") - .text((d) => d.data.title) - .attr("dx", 12) // Отодвигаем текст дальше от узла - .attr("dy", 4) // Немного поднимаем текст - - node.append("title").text((d) => d.data.title); - - node.on("click", (event, d) => { - if (onNodeClick) { - onNodeClick(d.data.id, d.data.title); // Передаем id и title - } - }); - - simulation.on("tick", () => { - link - .attr("x1", (d) => d.source.x) - .attr("y1", (d) => d.source.y) - .attr("x2", (d) => d.target.x) - .attr("y2", (d) => d.target.y); - - node - .attr("cx", (d) => d.x) - .attr("cy", (d) => d.y); - - text - .attr("x", (d) => d.x + 12) // Смещаем текст правее узла - .attr("y", (d) => d.y + 4); - }); - - return () => { - simulation.stop(); - }; - }, [data, onNodeClick]); - - const drag = (simulation) => { - function dragstarted(event, d) { - if (!event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - } - - function dragged(event, d) { - d.fx = event.x; - d.fy = event.y; - } - - function dragended(event, d) { - if (!event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - } - - return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); - }; - - return ; -}; - -export default TreeChart; diff --git a/src/Components/TreeChart/TreeChart.jsx b/src/Components/TreeChart/TreeChart.jsx new file mode 100644 index 0000000..37705e8 --- /dev/null +++ b/src/Components/TreeChart/TreeChart.jsx @@ -0,0 +1,160 @@ +import React, { useRef, useEffect, useMemo } from "react"; +import * as d3 from "d3"; +import { getStatusColor } from "./dataUtils"; + +const TreeChart = ({ data, onNodeClick }) => { + const chartRef = useRef(); + const simulationRef = useRef(null); + const nodePositions = useRef(new Map()); + + const { root, nodes, links } = useMemo(() => { + if (!data || !data.items) return { root: null, nodes: [], links: [] }; + + const root = d3.hierarchy(data, (d) => d.items); + const nodes = root.descendants(); + const links = root.links(); + + // Применяем сохраненные позиции к узлам + nodes.forEach((node) => { + const prev = nodePositions.current.get(node.data.id); + if (prev) { + node.x = prev.x; + node.y = prev.y; + node.fx = prev.fx ?? null; // Если фиксированные координаты были, сохраняем + node.fy = prev.fy ?? null; + } else { + // Если узел новый, задаем ему позицию рядом с родителем + const parent = node.parent; + node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000; + node.y = parent ? parent.y + Math.random() * 50 - 25 : Math.random() * 1000; + } + nodePositions.current.set(node.data.id, { x: node.x, y: node.y, fx: node.fx, fy: node.fy }); + }); + + return { root, nodes, links }; + }, [data]); + + useEffect(() => { + if (!chartRef.current) return; + + const svg = d3.select(chartRef.current) + .attr("width", 2000) + .attr("height", 1000) + .attr("viewBox", [-500, -500, 1000, 1000]) + .attr("style", "max-width: 100%; height: auto;"); + + svg.append("g").attr("class", "links"); + svg.append("g").attr("class", "nodes"); + svg.append("g").attr("class", "labels"); + + // Инициализация симуляции + simulationRef.current = d3.forceSimulation() + .force("link", d3.forceLink().id((d) => d.data.id).distance(80).strength(1)) + .force("charge", d3.forceManyBody().strength(-200)) + .force("center", d3.forceCenter(0, 0)) + .force("collision", d3.forceCollide().radius(20)) + .force("x", d3.forceX(0).strength(0.05)) // Ограничиваем разлет по X + .force("y", d3.forceY(0).strength(0.05)) // Ограничиваем разлет по Y + .force("radial", d3.forceRadial(200, 0, 0).strength(0.02)) // Держим узлы ближе к центру + .alphaDecay(0.02) // Замедляем затухание + .alphaTarget(0.1); + + // Запускаем симуляцию на 15 секунд, затем отключаем + setTimeout(() => { + simulationRef.current.stop(); // Останавливаем симуляцию + nodes.forEach((node) => { + node.fx = node.x; // Фиксируем текущие позиции узлов + node.fy = node.y; + }); + }, 15000); // 15 секунд + + }, []); + + useEffect(() => { + if (!root || !chartRef.current) return; + + const svg = d3.select(chartRef.current); + const linkGroup = svg.select(".links"); + const nodeGroup = svg.select(".nodes"); + const labelGroup = svg.select(".labels"); + + // Обновляем связи + const link = linkGroup + .selectAll("line") + .data(links, (d) => `${d.source.data.id}-${d.target.data.id}`) + .join("line") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6); + + // Обновляем узлы + const node = nodeGroup + .selectAll("circle") + .data(nodes, (d) => d.data.id) + .join("circle") + .attr("fill", (d) => getStatusColor(d.data.status)) + .attr("stroke", "#fff") + .attr("r", 7) + .call(drag()); + + node.on("click", (event, d) => { + if (onNodeClick) { + onNodeClick(d.data.id, d.data.title); + } + }); + + // Обновляем текстовые метки + const text = labelGroup + .selectAll("text") + .data(nodes, (d) => d.data.id) + .join("text") + .text((d) => d.data.title) + .attr("dx", 12) + .attr("dy", 4); + + // Обновляем симуляцию + simulationRef.current.nodes(nodes); + simulationRef.current.force("link").links(links); + simulationRef.current.alphaTarget(0.1).restart(); + + simulationRef.current.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); + }); + + }, [root, links, nodes, onNodeClick]); + + const drag = () => { + function dragstarted(event, d) { + if (!event.active) simulationRef.current.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) simulationRef.current.alphaTarget(0); + nodePositions.current.set(d.data.id, { x: d.x, y: d.y, fx: d.fx, fy: d.fy }); + } + + return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); + }; + + return ; +}; + +export default TreeChart; \ No newline at end of file diff --git a/src/Components/TreeChart/dataUtils.jsx b/src/Components/TreeChart/dataUtils.jsx new file mode 100644 index 0000000..fa67130 --- /dev/null +++ b/src/Components/TreeChart/dataUtils.jsx @@ -0,0 +1,53 @@ +// Функция для генерации случайных статусов +const getRandomStatus = () => { + const statuses = [ + ...Array(90).fill("green"), // 63/70 chance (примерно 90%) + ...Array(6).fill("yellow"), // 1/70 chance (примерно 1.43%) + ...Array(3).fill("orange"), // 1/70 chance (примерно 1.43%) + ...Array(1).fill("red"), // 1/70 chance (примерно 1.43%) + ]; + return statuses[Math.floor(Math.random() * statuses.length)]; +}; + +// Функция для обновления статусов в дереве +const updateStatuses = (data) => { + if (!data.items || data.items.length === 0) { + // Если это элемент нижнего уровня, генерируем случайный статус + data.status = getRandomStatus(); + return data.status; + } + + // Рекурсивно обновляем статусы для всех дочерних элементов + let childStatuses = data.items.map((child) => updateStatuses(child)); + + // Определяем статус текущего элемента на основе статусов дочерних элементов + if (childStatuses.includes("red")) { + data.status = "red"; + } else if (childStatuses.includes("orange")) { + data.status = "orange"; + } else if (childStatuses.includes("yellow")) { + data.status = "yellow"; + } else { + data.status = "green"; + } + + return data.status; +}; + +// Функция для получения цвета по статусу +const getStatusColor = (status) => { + switch (status) { + case "green": + return "#4CAF50"; // Зеленый + case "yellow": + return "#FFEB3B"; // Желтый + case "orange": + return "#FF9800"; // Оранжевый + case "red": + return "#F44336"; // Красный + default: + return "#4CAF50"; // Синий (или любой другой стандартный цвет) + } +}; + +export { getRandomStatus, updateStatuses, getStatusColor }; \ No newline at end of file diff --git a/src/Components/TreeChart/menuData.json b/src/Components/TreeChart/menuData.json new file mode 100644 index 0000000..4cdcaff --- /dev/null +++ b/src/Components/TreeChart/menuData.json @@ -0,0 +1,735 @@ +{ + "title": "Сервер ЗВКС", + "id": "1", + "items": [ + { + "title": "Функциональные задачи", + "items": [ + { + "id": "system_control", + "title": "Контроль системы" + }, + { + "id": "system_management", + "title": "Система управления" + }, + { + "id": "conference", + "title": "Проведение ВКС" + }, + { + "id": "backup", + "title": "Резервное копирование" + }, + { + "id": "relay_info", + "title": "Ретрансляция информации" + } + ] + }, + { + "id": "18", + "title": "Graviton S2082I (device$18)", + "items": [ + { + "id": "4", + "title": "OS Linux (module$4) АО", + "items": [ + { + "id": "188", + "title": "Наименование" + }, + { + "id": "189", + "title": "Время работы" + }, + { + "id": "190", + "title": "Загрузка процессора за 1 минуту" + }, + { + "id": "191", + "title": "Загрузка процессора за 5 минут" + }, + { + "id": "192", + "title": "Загрузка процессора за 15 минут" + }, + { + "id": "197", + "title": "Общий объем SWAP-файла" + }, + { + "id": "198", + "title": "Используемый объем SWAP-файла" + }, + { + "id": "199", + "title": "Общий объем физической оперативной памяти" + }, + { + "id": "200", + "title": "Доступный объем физической оперативной памяти" + }, + { + "id": "201", + "title": "Свободный объем физической и виртуальной оперативной памяти" + }, + { + "id": "202", + "title": "Буферизованный объем оперативной памяти" + }, + { + "id": "203", + "title": "Кэшированый объем оперативной памяти" + }, + { + "id": "274", + "title": "Используемый объем SWAP-файла" + }, + { + "id": "275", + "title": "Время затраченное процессором на процессы с пониженным приоритетом" + }, + { + "id": "276", + "title": "Время затраченное процессором на процессы ядра ОС" + }, + { + "id": "277", + "title": "Время простоя процессора" + }, + { + "id": "278", + "title": "Общая емкость жестких дисков" + }, + { + "id": "279", + "title": "Доступная емкость жестких дисков" + } + ] + }, + { + "id": "5", + "title": "Vinteo (module$5) ПО", + "items": [ + { + "id": "31", + "title": "Общее количество участников" + }, + { + "id": "32", + "title": "Ожидание соединения" + }, + { + "id": "33", + "title": "Зарегистрированные абоненты" + }, + { + "id": "34", + "title": "Количество пользоватей HLS" + }, + { + "id": "35", + "title": "Общее количество P2P комнат" + }, + { + "id": "36", + "title": "Общее количество конференций" + }, + { + "id": "37", + "title": "Общее количество активных конференций" + }, + { + "id": "38", + "title": "Статус записи" + }, + { + "id": "39", + "title": "Общее количество сохранённых записей" + } + ] + }, + { + "id": "261", + "title": "Сетевой адаптер №1 (port$261) Eth_1", + "items": [ + { + "id": "206", + "title": "Наименование порта Eth_1" + }, + { + "id": "207", + "title": "Скорость порта Eth_1" + }, + { + "id": "208", + "title": "Физический адрес порта Eth_1" + }, + { + "id": "209", + "title": "Административное состояние порта Eth_1" + }, + { + "id": "210", + "title": "Оперативное состояние порта Eth_1" + }, + { + "id": "211", + "title": "Общее количество отправленных октетов Eth_1" + }, + { + "id": "212", + "title": "Количество входящих Multicast пакетов Eth_1" + }, + { + "id": "213", + "title": "Количество иcходящих Multiicast пакетов Eth_1" + }, + { + "id": "214", + "title": "Количество входящих Broadcast пакетов Eth_1" + }, + { + "id": "215", + "title": "Количество иcходящих Broadcast пакетов Eth_1" + }, + { + "id": "216", + "title": "Количество входящих Unicast пакетов Eth_1" + }, + { + "id": "217", + "title": "Количество иcходящих Unicast пакетов Eth_1" + }, + { + "id": "218", + "title": "Количество входящих пакетов помеченные как отброшенные Eth_1" + }, + { + "id": "219", + "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_1" + }, + { + "id": "220", + "title": "Количество входящих пакетов с ошибкой Eth_1" + }, + { + "id": "221", + "title": "Количество исходящих пакетов с ошибкой Eth_1" + }, + { + "id": "222", + "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_1" + } + ] + }, + { + "id": "262", + "title": "Сетевой адаптер №2 (port$262) Eth_2", + "items": [ + { + "id": "223", + "title": "Наименование порта Eth_2" + }, + { + "id": "224", + "title": "Скорость порта Eth_2" + }, + { + "id": "225", + "title": "Физический адрес порта Eth_2" + }, + { + "id": "226", + "title": "Административное состояние порта Eth_2" + }, + { + "id": "227", + "title": "Оперативное состояние порта Eth_2" + }, + { + "id": "228", + "title": "Общее количество отправленных октетов Eth_2" + }, + { + "id": "229", + "title": "Количество входящих Multicast пакетов Eth_2" + }, + { + "id": "230", + "title": "Количество иcходящих Multiicast пакетов Eth_2" + }, + { + "id": "231", + "title": "Количество входящих Broadcast пакетов Eth_2" + }, + { + "id": "232", + "title": "Количество иcходящих Broadcast пакетов Eth_2" + }, + { + "id": "233", + "title": "Количество входящих Unicast пакетов Eth_2" + }, + { + "id": "234", + "title": "Количество иcходящих Unicast пакетов Eth_2" + }, + { + "id": "235", + "title": "Количество входящих пакетов помеченные как отброшенные Eth_2" + }, + { + "id": "236", + "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_2" + }, + { + "id": "237", + "title": "Количество входящих пакетов с ошибкой Eth_2" + }, + { + "id": "238", + "title": "Количество исходящих пакетов с ошибкой Eth_2" + }, + { + "id": "239", + "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_2" + } + ] + }, + { + "id": "263", + "title": "Сетевой адаптер №3 (port$263) Eth_3", + "items": [ + { + "id": "240", + "title": "Наименование порта Eth_3" + }, + { + "id": "241", + "title": "Скорость порта Eth_3" + }, + { + "id": "242", + "title": "Физический адрес порта Eth_3" + }, + { + "id": "243", + "title": "Административное состояние порта Eth_3" + }, + { + "id": "244", + "title": "Оперативное состояние порта Eth_3" + }, + { + "id": "245", + "title": "Общее количество отправленных октетов Eth_3" + }, + { + "id": "246", + "title": "Количество входящих Multicast пакетов Eth_3" + }, + { + "id": "247", + "title": "Количество иcходящих Multiicast пакетов Eth_3" + }, + { + "id": "248", + "title": "Количество входящих Broadcast пакетов Eth_3" + }, + { + "id": "249", + "title": "Количество иcходящих Broadcast пакетов Eth_3" + }, + { + "id": "250", + "title": "Количество входящих Unicast пакетов Eth_3" + }, + { + "id": "251", + "title": "Количество иcходящих Unicast пакетов Eth_3" + }, + { + "id": "252", + "title": "Количество входящих пакетов помеченные как отброшенные Eth_3" + }, + { + "id": "253", + "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_3" + }, + { + "id": "254", + "title": "Количество входящих пакетов с ошибкой Eth_3" + }, + { + "id": "255", + "title": "Количество исходящих пакетов с ошибкой Eth_3" + }, + { + "id": "256", + "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_3" + } + ] + }, + { + "id": "264", + "title": "Сетевой адаптер №4 (port$264) Eth_4", + "items": [ + { + "id": "257", + "title": "Наименование порта Eth_4" + }, + { + "id": "258", + "title": "Скорость порта Eth_4" + }, + { + "id": "259", + "title": "Физический адрес порта Eth_4" + }, + { + "id": "260", + "title": "Административное состояние порта Eth_4" + }, + { + "id": "261", + "title": "Оперативное состояние порта Eth_4" + }, + { + "id": "262", + "title": "Общее количество отправленных октетов Eth_4" + }, + { + "id": "263", + "title": "Количество входящих Multicast пакетов Eth_4" + }, + { + "id": "264", + "title": "Количество иcходящих Multiicast пакетов Eth_4" + }, + { + "id": "265", + "title": "Количество входящих Broadcast пакетов Eth_4" + }, + { + "id": "266", + "title": "Количество иcходящих Broadcast пакетов Eth_4" + }, + { + "id": "267", + "title": "Количество входящих Unicast пакетов Eth_4" + }, + { + "id": "268", + "title": "Количество иcходящих Unicast пакетов Eth_4" + }, + { + "id": "269", + "title": "Количество входящих пакетов помеченные как отброшенные Eth_4" + }, + { + "id": "270", + "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_4" + }, + { + "id": "271", + "title": "Количество входящих пакетов с ошибкой Eth_4" + }, + { + "id": "272", + "title": "Количество исходящих пакетов с ошибкой Eth_4" + }, + { + "id": "273", + "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_4" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "items": [ + { + "title": "Аппаратное обеспечение", + "items": [ + { + "id": "media_system_software_1_2", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_2", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_2", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_2", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "items": [ + { + "id": "media_software_1_2", + "title": "ПО" + }, + { + "id": "media_software_2_2", + "title": "ПО" + }, + { + "id": "media_software_3_2", + "title": "ПО" + }, + { + "id": "media_software_4_2", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "items": [ + { + "title": "Аппаратное обеспечение", + "items": [ + { + "id": "media_system_software_1_3", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_3", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_3", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_3", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "items": [ + { + "id": "media_software_1_3", + "title": "ПО" + }, + { + "id": "media_software_2_3", + "title": "ПО" + }, + { + "id": "media_software_3_3", + "title": "ПО" + }, + { + "id": "media_software_4_3", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "items": [ + { + "title": "Аппаратное обеспечение", + "items": [ + { + "id": "media_system_software_1_4", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_4", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_4", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_4", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "items": [ + { + "id": "media_software_1_4", + "title": "ПО" + }, + { + "id": "media_software_2_4", + "title": "ПО" + }, + { + "id": "media_software_3_4", + "title": "ПО" + }, + { + "id": "media_software_4_4", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "items": [ + { + "title": "Аппаратное обеспечение", + "items": [ + { + "id": "media_system_software_1_5", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_5", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_5", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_5", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "items": [ + { + "id": "media_software_1_5", + "title": "ПО" + }, + { + "id": "media_software_2_5", + "title": "ПО" + }, + { + "id": "media_software_3_5", + "title": "ПО" + }, + { + "id": "media_software_4_5", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Сервер систем", + "items": [ + { + "title": "Аппаратное обеспечение", + "items": [ + { + "id": "copy_system_software_1", + "title": "Центральный процессор" + }, + { + "id": "copy_system_software_2", + "title": "Оперативная память" + }, + { + "id": "copy_system_software_3", + "title": "Жесткий диск" + }, + { + "id": "copy_system_software_4", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "items": [ + { + "id": "copy_software_1", + "title": "ПО" + }, + { + "id": "copy_software_2", + "title": "ПО" + }, + { + "id": "copy_software_3", + "title": "ПО" + }, + { + "id": "copy_software_4", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Сервер систем", + "items": [ + { + "title": "Аппаратное обеспечение", + "items": [ + { + "id": "control_system_software_1", + "title": "Центральный процессор" + }, + { + "id": "control_system_software_2", + "title": "Оперативная память" + }, + { + "id": "control_system_software_3", + "title": "Жесткий диск" + }, + { + "id": "control_system_software_4", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "items": [ + { + "id": "control_software_1", + "title": "ПО" + }, + { + "id": "control_software_2", + "title": "ПО" + }, + { + "id": "control_software_3", + "title": "ПО" + }, + { + "id": "control_software_4", + "title": "ПО" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Components/TreeChart/menuData2.json b/src/Components/TreeChart/menuData2.json new file mode 100644 index 0000000..645a8ef --- /dev/null +++ b/src/Components/TreeChart/menuData2.json @@ -0,0 +1,382 @@ +{ + "title": "Сервис ВКС", + "id":"service_VKS", + "items": [ + { + "title": "Функциональные задачи", + "id":"functions", + "items": [ + { + "id": "system_control", + "title": "Контроль системы" + }, + { + "id": "system_management", + "title": "Система управления" + }, + { + "id": "conference", + "title": "Проведение ВКС" + }, + { + "id": "backup", + "title": "Резервное копирование" + }, + { + "id": "relay_info", + "title": "Ретрансляция информации" + } + ] + }, + { + "title": "Медиа сервер", + "id":"media_server_1", + "items": [ + { + "title": "Аппаратное обеспечение", + "id":"system_software_1", + "items": [ + { + "id": "media_system_software_1", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "id":"software_1", + "items": [ + { + "id": "media_software_1", + "title": "ПО" + }, + { + "id": "media_software_2", + "title": "ПО" + }, + { + "id": "media_software_3", + "title": "ПО" + }, + { + "id": "media_software_4", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "id":"media_server_2", + "items": [ + { + "title": "Аппаратное обеспечение", + "id":"system_software_2", + "items": [ + { + "id": "media_system_software_1_2", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_2", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_2", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_2", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "id":"software_2", + "items": [ + { + "id": "media_software_1_2", + "title": "ПО" + }, + { + "id": "media_software_2_2", + "title": "ПО" + }, + { + "id": "media_software_3_2", + "title": "ПО" + }, + { + "id": "media_software_4_2", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "id":"media_server_3", + "items": [ + { + "title": "Аппаратное обеспечение", + "id":"system_software_3", + "items": [ + { + "id": "media_system_software_1_3", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_3", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_3", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_3", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "id":"software_3", + "items": [ + { + "id": "media_software_1_3", + "title": "ПО" + }, + { + "id": "media_software_2_3", + "title": "ПО" + }, + { + "id": "media_software_3_3", + "title": "ПО" + }, + { + "id": "media_software_4_3", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "id":"media_server_4", + "items": [ + { + "title": "Аппаратное обеспечение", + "id":"system_software_4", + "items": [ + { + "id": "media_system_software_1_4", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_4", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_4", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_4", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "id":"software_4", + "items": [ + { + "id": "media_software_1_4", + "title": "ПО" + }, + { + "id": "media_software_2_4", + "title": "ПО" + }, + { + "id": "media_software_3_4", + "title": "ПО" + }, + { + "id": "media_software_4_4", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Медиа сервер", + "id":"media_server_5", + "items": [ + { + "title": "Аппаратное обеспечение", + "id":"system_software_5", + "items": [ + { + "id": "media_system_software_1_5", + "title": "Центральный процессор" + }, + { + "id": "media_system_software_2_5", + "title": "Оперативная память" + }, + { + "id": "media_system_software_3_5", + "title": "Жесткий диск" + }, + { + "id": "media_system_software_4_5", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "id":"software_5", + "items": [ + { + "id": "media_software_1_5", + "title": "ПО" + }, + { + "id": "media_software_2_5", + "title": "ПО" + }, + { + "id": "media_software_3_5", + "title": "ПО" + }, + { + "id": "media_software_4_5", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Сервер систем", + "id":"system_server_1", + "items": [ + { + "title": "Аппаратное обеспечение", + "id":"system_software_6", + "items": [ + { + "id": "copy_system_software_1", + "title": "Центральный процессор" + }, + { + "id": "copy_system_software_2", + "title": "Оперативная память" + }, + { + "id": "copy_system_software_3", + "title": "Жесткий диск" + }, + { + "id": "copy_system_software_4", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "id":"software_6", + "items": [ + { + "id": "copy_software_1", + "title": "ПО" + }, + { + "id": "copy_software_2", + "title": "ПО" + }, + { + "id": "copy_software_3", + "title": "ПО" + }, + { + "id": "copy_software_4", + "title": "ПО" + } + ] + } + ] + }, + { + "title": "Сервер систем", + "id":"system_server_2", + "items": [ + { + "title": "Аппаратное обеспечение", + "id":"system_software_7", + "items": [ + { + "id": "control_system_software_1", + "title": "Центральный процессор" + }, + { + "id": "control_system_software_2", + "title": "Оперативная память" + }, + { + "id": "control_system_software_3", + "title": "Жесткий диск" + }, + { + "id": "control_system_software_4", + "title": "Сетевые адаптеры" + } + ] + }, + { + "title": "Программное обеспечение", + "id":"software_7", + "items": [ + { + "id": "control_software_1", + "title": "ПО" + }, + { + "id": "control_software_2", + "title": "ПО" + }, + { + "id": "control_software_3", + "title": "ПО" + }, + { + "id": "control_software_4", + "title": "ПО" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx new file mode 100644 index 0000000..9c55909 --- /dev/null +++ b/src/Components/TreeChart/tabContent.jsx @@ -0,0 +1,107 @@ +import React, { lazy, Suspense } from "react"; + +const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart')); + +// Функция для генерации названия метрики на основе id +const getMetricName = (id) => { + return `zvks_apiforsnmp_measure_${id}`; +}; + +//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!! + +// Функция для рекурсивного сбора всех id потомков +const getAllChildIds = (node) => { + let ids = []; + if (node.id) { + ids.push(node.id); // Добавляем id текущего узла + } + if (node.items && node.items.length > 0) { + node.items.forEach((child) => { + ids = ids.concat(getAllChildIds(child)); // Рекурсивно собираем id потомков + }); + } + return ids; +}; + +const tabContent = (data) => { + const tabContent = {}; + + + const generateContent = (nodes) => { + nodes.forEach((node) => { + + + // Если у узла есть вложенные элементы, рекурсивно обрабатываем их + if (node.items && node.items.length > 0) { + + generateContent(node.items); + } + + // Если у узла есть id, добавляем его в tabContent + if (node.id) { + + + let content = ( +
+

{node.title}

+

Контент для {node.title}.

+
+ ); + + // Если у узла есть потомки, добавляем графики для всех потомков + if (node.items && node.items.length > 0) { + const childIds = getAllChildIds(node); // Получаем все id потомков + const charts = childIds.map((id) => { + const metricName = getMetricName(id); + return ( +
+

{node.title} - {id}

+ Загрузка графика...
}> + + +
+ ); + }); + + content = ( +
+

{node.title}

+

Контент для {node.title}.

+ {charts} +
+ ); + } else { + // Если у узла нет потомков, добавляем график для него + + const metricName = getMetricName(node.id); + content = ( +
+

{node.title}

+

Контент для {node.title}.

+ Загрузка графика...
}> + + +
+ ); + } + + // Сохраняем контент для текущего id + tabContent[node.id] = { + title: node.title, + content: content, + }; + } + }); + }; + + // Начинаем обработку с корневого уровня + if (data.items && data.items.length > 0) { + generateContent(data.items); + } else { + console.warn("Данные отсутствуют или массив items пуст"); + } + + return tabContent; +}; + +export default tabContent; // Экспортируем только функцию \ No newline at end of file diff --git a/src/Components/TreeChart/tabContent2.jsx b/src/Components/TreeChart/tabContent2.jsx new file mode 100644 index 0000000..47dc890 --- /dev/null +++ b/src/Components/TreeChart/tabContent2.jsx @@ -0,0 +1,96 @@ +import React from "react"; +import PrometheusChart from '../../Charts/PrometheusChart'; + +const tabContent = { + // Сервис ВКС + service1: { title: "Сервис ВКС", content:

Сервис ВКС

}, + + // Функциональные задачи + system_control: { title: "Контроль системы", content:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}, + + // Медиа сервер 1 + media_system_software_1: { title: "Центральный процессор", content:

Центральный процессор

Описание центрального процессора медиа сервера.

}, + media_system_software_2: { title: "Оперативная память", content:

Оперативная память

Описание оперативной памяти медиа сервера.

}, + media_system_software_3: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска медиа сервера.

}, + media_system_software_4: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

Описание сетевых адаптеров медиа сервера.

}, + media_software_1: { title: "ПО", content:

Программное обеспечение медиа сервера

}, + media_software_2: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_3: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_4: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + + // Медиа сервер 2 + media_system_software_1_2: { title: "Центральный процессор", content:

Центральный процессор

Описание центрального процессора медиа сервера.

}, + media_system_software_2_2: { title: "Оперативная память", content:

Оперативная память

Описание оперативной памяти медиа сервера.

}, + media_system_software_3_2: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска медиа сервера.

}, + media_system_software_4_2: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

Описание сетевых адаптеров медиа сервера.

}, + media_software_1_2: { title: "ПО", content:

Программное обеспечение медиа сервера

}, + media_software_2_2: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_3_2: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_4_2: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + + // Медиа сервер 3 + media_system_software_1_3: { title: "Центральный процессор", content:

Центральный процессор

Описание центрального процессора медиа сервера.

}, + media_system_software_2_3: { title: "Оперативная память", content:

Оперативная память

Описание оперативной памяти медиа сервера.

}, + media_system_software_3_3: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска медиа сервера.

}, + media_system_software_4_3: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

Описание сетевых адаптеров медиа сервера.

}, + media_software_1_3: { title: "ПО", content:

Программное обеспечение медиа сервера

}, + media_software_2_3: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_3_3: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_4_3: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + + // Медиа сервер 4 + media_system_software_1_4: { title: "Центральный процессор", content:

Центральный процессор

Описание центрального процессора медиа сервера.

}, + media_system_software_2_4: { title: "Оперативная память", content:

Оперативная память

Описание оперативной памяти медиа сервера.

}, + media_system_software_3_4: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска медиа сервера.

}, + media_system_software_4_4: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

Описание сетевых адаптеров медиа сервера.

}, + media_software_1_4: { title: "ПО", content:

Программное обеспечение медиа сервера

}, + media_software_2_4: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_3_4: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_4_4: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + + // Медиа сервер 5 + media_system_software_1_5: { title: "Центральный процессор", content:

Центральный процессор

Описание центрального процессора медиа сервера.

}, + media_system_software_2_5: { title: "Оперативная память", content:

Оперативная память

Описание оперативной памяти медиа сервера.

}, + media_system_software_3_5: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска медиа сервера.

}, + media_system_software_4_5: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

Описание сетевых адаптеров медиа сервера.

}, + media_software_1_5: { title: "ПО", content:

Программное обеспечение медиа сервера

}, + media_software_2_5: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_3_5: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + media_software_4_5: { title: "ПО", content:

Программное обеспечение медиа сервера

Описание ПО медиа сервера.

}, + + // Сервер резервного копирования + copy_system_software_1: { title: "Центральный процессор", content:

Центральный процессор

Описание центрального процессора сервера резервного копирования.

}, + copy_system_software_2: { title: "Оперативная память", content:

Оперативная память

Описание оперативной памяти сервера резервного копирования.

}, + copy_system_software_3: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска сервера резервного копирования.

}, + copy_system_software_4: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

Описание сетевых адаптеров сервера резервного копирования.

}, + copy_software_1: { title: "ПО", content:

Программное обеспечение сервера резервного копирования

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

}, + copy_software_2: { title: "ПО", content:

Программное обеспечение сервера резервного копирования

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

}, + copy_software_3: { title: "ПО", content:

Программное обеспечение сервера резервного копирования

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

}, + copy_software_4: { title: "ПО", content:

Программное обеспечение сервера резервного копирования

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

}, + + // Сервер системы управления + control_system_software_1: { title: "Центральный процессор", content:

Центральный процессор

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

}, + control_system_software_2: { title: "Оперативная память", content:

Оперативная память

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

}, + control_system_software_3: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска сервера системы управления.

}, + control_system_software_4: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

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

}, + control_software_1: { title: "ПО", content:

Программное обеспечение сервера системы управления

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

}, + control_software_2: { title: "ПО", content:

Программное обеспечение сервера системы управления

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

}, + control_software_3: { title: "ПО", content:

Программное обеспечение сервера системы управления

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

}, + control_software_4: { title: "ПО", content:

Программное обеспечение сервера системы управления

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

}, + + // Сервер сбора и ретрансляции информации + system_software_1: { title: "Центральный процессор", content:

Центральный процессор

Описание центрального процессора сервера сбора и ретрансляции информации.

}, + system_software_2: { title: "Оперативная память", content:

Оперативная память

Описание оперативной памяти сервера сбора и ретрансляции информации.

}, + system_software_3: { title: "Жесткий диск", content:

Жесткий диск

Описание жесткого диска сервера сбора и ретрансляции информации.

}, + system_software_4: { title: "Сетевые адаптеры", content:

Сетевые адаптеры

Описание сетевых адаптеров сервера сбора и ретрансляции информации.

}, + software_1: { title: "ПО", content:

Программное обеспечение сервера сбора и ретрансляции информации

Описание ПО сервера сбора и ретрансляции информации.

}, + software_2: { title: "ПО", content:

Программное обеспечение сервера сбора и ретрансляции информации

Описание ПО сервера сбора и ретрансляции информации.

}, + software_3: { title: "ПО", content:

Программное обеспечение сервера сбора и ретрансляции информации

Описание ПО сервера сбора и ретрансляции информации.

}, + software_4: { title: "ПО", content:

Программное обеспечение сервера сбора и ретрансляции информации

Описание ПО сервера сбора и ретрансляции информации.

}, +}; + +export default tabContent; \ No newline at end of file diff --git a/src/Components/ErrorIndicator.jsx b/src/Components/UI/ErrorIndicator.jsx similarity index 70% rename from src/Components/ErrorIndicator.jsx rename to src/Components/UI/ErrorIndicator.jsx index 7d71840..7215211 100644 --- a/src/Components/ErrorIndicator.jsx +++ b/src/Components/UI/ErrorIndicator.jsx @@ -1,7 +1,7 @@ import React from "react"; -import criticalIcon from "../assets/images/critical.png"; // Красный треугольник -import warningIcon from "../assets/images/warning.png"; // Желтый треугольник -import "../Style/ErrorIndicator.css"; // Подключаем стили +import criticalIcon from "../../assets/images/critical.png"; // Красный треугольник +import warningIcon from "../../assets/images/warning.png"; // Желтый треугольник +import "../../Style/ErrorIndicator.css"; // Подключаем стили const ErrorIndicator = ({ criticalCount, warningCount }) => { return ( diff --git a/src/Components/ExpandableInfo.jsx b/src/Components/UI/ExpandableInfo.jsx similarity index 100% rename from src/Components/ExpandableInfo.jsx rename to src/Components/UI/ExpandableInfo.jsx diff --git a/src/Components/UI/LoginModal.jsx b/src/Components/UI/LoginModal.jsx new file mode 100644 index 0000000..c4d5f09 --- /dev/null +++ b/src/Components/UI/LoginModal.jsx @@ -0,0 +1,49 @@ +import React, { useState } from "react"; +import Modal from "./Modal"; +import "../../Style/LoginModal.css"; + +const LoginModal = ({ 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 LoginModal; \ No newline at end of file diff --git a/src/Components/UI/Modal.jsx b/src/Components/UI/Modal.jsx new file mode 100644 index 0000000..3718035 --- /dev/null +++ b/src/Components/UI/Modal.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +const Modal = ({ children, onClose }) => { + return ( +
+
+ {children} + +
+
+ ); +}; + +export default Modal; \ No newline at end of file diff --git a/src/Components/UI/Tabs.jsx b/src/Components/UI/Tabs.jsx new file mode 100644 index 0000000..78ec099 --- /dev/null +++ b/src/Components/UI/Tabs.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import "../../Style/common.css"; // Общие стили для табов + +const Tabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => { + const handleMouseDown = (e, id) => { + // Проверяем, была ли нажата средняя кнопка мыши (button === 1) + if (e.button === 1) { + e.preventDefault(); // Предотвращаем стандартное поведение (например, прокрутку) + onCloseTab(id); // Закрываем вкладку + } + }; + + + return ( +
+ {/* Всегда отображаемые вкладки */} +
onTabClick("Главная")} + onMouseDown={(e) => handleMouseDown(e, "Главная")} // Добавляем обработчик для СКМ + > + Главная +
+
onTabClick("Визуализация")} + onMouseDown={(e) => handleMouseDown(e, "Визуализация")} // Добавляем обработчик для СКМ + > + Визуализация +
+ + {/* Динамически добавляемые вкладки */} + {tabs.map((tab) => ( +
onTabClick(tab.id)} + onMouseDown={(e) => handleMouseDown(e, tab.id)} // Добавляем обработчик для СКМ + > + {tab.title} + +
+ ))} +
+ ); +}; + +export default Tabs; \ No newline at end of file diff --git a/src/Components/UI/TreeTable.jsx b/src/Components/UI/TreeTable.jsx new file mode 100644 index 0000000..28b2105 --- /dev/null +++ b/src/Components/UI/TreeTable.jsx @@ -0,0 +1,76 @@ +import React from "react"; +import "../../Style/TreeTable.css"; +import { getStatusColor } from "../TreeChart/dataUtils"; // Импортируем функцию + +const TreeTable = ({ data }) => { + // Фильтруем данные, чтобы убрать "Функциональные задачи" + const filteredData = data.filter((item) => item.title !== "Функциональные задачи"); + + return ( +
+ + + {/* Первый уровень: Заголовки "Медиа сервер" */} + + {filteredData.map((item, index) => ( + + ))} + + {/* Второй уровень: "АО" и "ПО" */} + + {filteredData.map((item, index) => ( + + + + + ))} + + + + {/* Третий уровень: Вложенные элементы "АО" и "ПО" */} + {renderRows(filteredData)} + +
+ {item.title} +
+ АО + + ПО +
+
+ ); +}; + +// Функция для отображения строк с вложенными элементами +const renderRows = (data) => { + const rows = []; + + // Находим максимальное количество элементов среди всех "АО" и "ПО" + const maxItems = Math.max( + ...data.flatMap((item) => [ + item.items[0]?.items?.length || 0, // АО + item.items[1]?.items?.length || 0 // ПО + ]) + ); + + // Генерируем строки + for (let i = 0; i < maxItems; i++) { + rows.push( + + {data.map((item, index) => ( + + + {item.items[0]?.items[i]?.title || ""} + + + {item.items[1]?.items[i]?.title || ""} + + + ))} + + ); + } + + return rows; +}; +export default TreeTable; \ No newline at end of file diff --git a/src/Components/menuData.json b/src/Components/menuData.json deleted file mode 100644 index daf3ef1..0000000 --- a/src/Components/menuData.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "title": "Сервис ВКС", - "items": [ - { - "title": "Функциональные задачи", - "items": [ - { - "id": "system_control", - "title": "Контроль системы" - }, - { - "id": "system_management", - "title": "Система управления" - }, - { - "id": "conference", - "title": "Проведение ВКС" - }, - { - "id": "backup", - "title": "Резервное копирование" - }, - { - "id": "relay_info", - "title": "Ретрансляция информации" - } - ] - }, - { - "title": "Аппаратное ПО", - "items": [ - { - "id": "hardware_software_1", - "title": "ПО1" - }, - { - "id": "hardware_software_2", - "title": "ПО2" - }, - { - "id": "hardware_software_3", - "title": "ПО3" - }, - { - "id": "hardware_software_4", - "title": "ПО4" - }, - { - "id": "hardware_software_5", - "title": "ПО5" - } - ] - } - ] -} diff --git a/src/Components/tabContent.jsx b/src/Components/tabContent.jsx deleted file mode 100644 index 9f7aa40..0000000 --- a/src/Components/tabContent.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import NetworkSpeedChart2 from '../Charts/TestCharts2'; - -const tabContent = { - 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/Dashboard.css b/src/Style/Dashboard.css index 8a83bd3..537a8ca 100644 --- a/src/Style/Dashboard.css +++ b/src/Style/Dashboard.css @@ -1,62 +1,56 @@ +/* Основной контейнер */ .dashboard-container { display: flex; height: 100vh; - width: 100vw; + width: calc(100vw - 20px); /* Учитываем отступ */ overflow: hidden; - /* Запрещаем появление скролла */ + margin-left: 20px; } +/* Сайдбар */ +.sidebar { + height: 100vh; + background-color: #3d74c7; + color: white; + position: fixed; + left: 0; + top: 0; + z-index: 999; + overflow: hidden; + transition: width 0.2s ease; + /* Плавное изменение ширины */ + display: flex; + flex-direction: column; +} + +/* Элемент для перетаскивания */ +.sidebar-resizer { + width: 5px; + /* Ширина элемента перетаскивания */ + height: 100%; + background-color: rgba(255, 255, 255, 0.1); + position: absolute; + right: 0; + top: 0; + cursor: ew-resize; + /* Курсор "изменить размер" */ + transition: background-color 0.2s ease; + z-index: 1000; + /* Убедимся, что элемент поверх других */ +} + +.sidebar-resizer:hover { + background-color: rgba(255, 255, 255, 0.3); + /* Эффект при наведении */ +} + +/* Основной контент */ .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; + margin-left: 50px; + transition: margin-left 0.2s ease; + overflow: auto; /* Позволяет прокручивать контент, если он не влезает */ } /* Контент */ @@ -64,22 +58,12 @@ 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); + box-shadow: 0 2px 5px rgba(29, 1, 1, 0.521); + max-width: 100%; /* Гарантируем, что контент не выйдет за границы */ + overflow: auto; /* Включаем скролл, если нужно */ } +/* Заголовки */ h2 { color: #444; } diff --git a/src/Style/DatePicker.css b/src/Style/DatePicker.css new file mode 100644 index 0000000..3b564d1 --- /dev/null +++ b/src/Style/DatePicker.css @@ -0,0 +1,129 @@ +/* DatePicker.css */ + +.react-datepicker-wrapper { + width: auto; + display: inline-block; +} + +.react-datepicker__input-container input { + width: 200px; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + color: #333; + background-color: #fff; + transition: border-color 0.2s ease; +} + +.react-datepicker__input-container input:focus { + border-color: #0078d4; + outline: none; +} + +.react-datepicker { + font-family: 'Segoe UI', sans-serif; + border: 1px solid #e0e0e0; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + background-color: #fff; + /* Непрозрачный фон */ + z-index: 1000; + /* Календарь поверх других элементов */ +} + +.react-datepicker-popper { + z-index: 1000; + /* Календарь поверх других элементов */ + pointer-events: auto; + /* Разрешить взаимодействие только с календарем */ +} + +.react-datepicker__header { + background-color: #f8f8f8; + /* Непрозрачный фон заголовка */ + border-bottom: 1px solid #e0e0e0; + border-radius: 8px 8px 0 0; + padding: 12px; +} + +.react-datepicker__current-month { + font-size: 14px; + font-weight: 600; + color: #333; +} + +.react-datepicker__navigation { + top: 12px; + border: 0.45rem solid transparent; +} + +.react-datepicker__navigation--previous { + left: 12px; + border-right-color: #666; +} + +.react-datepicker__navigation--next { + right: 12px; + border-left-color: #666; +} + +.react-datepicker__day-names { + display: flex; + justify-content: space-between; + padding: 0 8px; + margin-top: 8px; +} + +.react-datepicker__day-name { + width: 28px; + line-height: 28px; + text-align: center; + font-size: 12px; + color: #666; +} + +.react-datepicker__month { + background-color: #fff; + /* Непрозрачный фон месяца */ + margin: 0; + padding: 8px; +} + +.react-datepicker__week { + display: flex; + justify-content: space-between; +} + +.react-datepicker__day { + width: 28px; + line-height: 28px; + text-align: center; + font-size: 12px; + color: #333; + cursor: pointer; + border-radius: 50%; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.react-datepicker__day:hover { + background-color: #f0f0f0; +} + +.react-datepicker__day--selected { + background-color: #0078d4; + color: #fff; +} + +.react-datepicker__day--selected:hover { + background-color: #005bb5; +} + +.react-datepicker__day--outside-month { + color: #ccc; +} + +.react-datepicker__day--disabled { + color: #ccc; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/Style/ErrorIndicator.css b/src/Style/ErrorIndicator.css index efce90a..115f992 100644 --- a/src/Style/ErrorIndicator.css +++ b/src/Style/ErrorIndicator.css @@ -2,6 +2,7 @@ display: flex; align-items: center; gap: 15px; + padding-bottom: 20px; } .error-item { @@ -33,4 +34,5 @@ align-items: center; gap: 15px; justify-content: center; + } \ No newline at end of file diff --git a/src/Style/LoginModal.css b/src/Style/LoginModal.css index 0d6d28c..ae99fe0 100644 --- a/src/Style/LoginModal.css +++ b/src/Style/LoginModal.css @@ -11,7 +11,7 @@ } .modal { - background: white; + background: rgb(255, 255, 255); padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); @@ -26,6 +26,7 @@ .modal label { display: block; margin-bottom: 5px; + color: black; } .modal input { @@ -38,7 +39,8 @@ .modal button { padding: 10px 20px; - background: #007bff; + margin-bottom: 5px; + background: #08294b; color: white; border: none; border-radius: 4px; diff --git a/src/Style/SidebarMenu.css b/src/Style/SidebarMenu.css index 2956424..9fcb8d6 100644 --- a/src/Style/SidebarMenu.css +++ b/src/Style/SidebarMenu.css @@ -1,70 +1,124 @@ /* Боковое меню */ .sidebar { - width: 250px; - background-color: #333; - padding: 20px; - box-sizing: border-box; - border-right: 1px solid #444; height: 100vh; - /* Занимает всю высоту экрана */ - overflow-y: auto; - /* Прокрутка внутри меню, если контент не помещается */ - position: sticky; - /* Фиксируем меню */ + background-color: #3d74c7; + color: white; + position: fixed; + left: 0; top: 0; - /* Прилипаем к верху */ + z-index: 999; + overflow: hidden; + transition: width 0.2s ease; + /* Плавное изменение ширины */ + display: flex; + flex-direction: column; } +/* Контейнер для основного контента меню */ +.sidebar-content { + flex: 1; + overflow-y: auto; + /* Вертикальная прокрутка */ + overflow-x: hidden; + /* Убираем горизонтальную прокрутку */ + padding-bottom: 20px; + /* Отступ для "Помощи" и "Настроек" */ +} + +/* Заголовок меню */ .sidebar-title { margin-bottom: 20px; font-size: 18px; font-weight: bold; color: white; + padding: 10px; + /* Добавляем отступы */ } +/* Элементы меню */ .menu-item { margin-bottom: 10px; color: white; -} - -h2 { - color: white + width: 100%; + /* Ширина на всю ширину сайдбара */ } .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; + background-color: rgba(255, 255, 255, 0.1); + /* Легкий эффект при наведении */ } +/* Круглый индикатор статуса */ +.status-indicator { + width: 10px; + height: 10px; + border-radius: 50%; + /* Делаем круглым */ + margin-right: 10px; + /* Отступ от текста */ + flex-shrink: 0; + /* Запрещаем сжатие */ +} + +/* Анимация мигания для красного индикатора */ +@keyframes blink { + 0% { + opacity: 1; + } + + /* Полная видимость */ + 50% { + opacity: 0.3; + } + + /* Полупрозрачность */ + 100% { + opacity: 1; + } + + /* Полная видимость */ +} + +.status-indicator.blinking { + animation: blink 1s infinite; + /* Бесконечная анимация с интервалом 1 секунда */ +} + +/* Подменю */ .submenu { margin-left: 20px; margin-top: 10px; } -.tabs-container { - margin-top: 20px; -} - -.tab { +/* Футер сайдбара */ +.sidebar-footer { padding: 10px; - background-color: #444; - border: 1px solid #333; - border-radius: 5px; - margin-bottom: 5px; - cursor: pointer; - transition: background-color 0.3s ease; + background-color: #3d74c7; + text-align: center; + border-top: 1px solid rgba(255, 255, 255, 0.1); + /* Разделительная линия */ + flex-shrink: 0; + /* Запрещаем сжатие */ + width: 100%; + /* Ширина на всю ширину сайдбара */ } -.tab:hover { - background-color: #222; +.help, +.settings { + color: white; + margin: 5px 0; + /* Отступы между элементами */ + overflow-x: hidden; + /* Убираем горизонтальную прокрутку */ + text-align: left; } \ No newline at end of file diff --git a/src/Style/TreeTable.css b/src/Style/TreeTable.css new file mode 100644 index 0000000..d2093b1 --- /dev/null +++ b/src/Style/TreeTable.css @@ -0,0 +1,58 @@ +/* Контейнер для таблицы с прокруткой */ +.table-container { + width: 100%; + /* Занимает всю доступную ширину */ + overflow-x: auto; + /* Горизонтальная прокрутка при необходимости */ + margin: 0 auto; + /* Центрирование контейнера */ +} + +/* Стили для таблицы */ +.tree-table { + width: auto; + /* Автоматическая ширина, чтобы таблица могла расширяться */ + min-width: 95%; + /* Минимальная ширина таблицы */ + border-collapse: collapse; + margin: 0 auto; + /* Центрирование таблицы */ +} + +/* Заголовки таблицы (первый уровень) */ +.tree-table th { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + white-space: nowrap; + /* Запрет на перенос текста */ + font-weight: bold; + /* Жирный шрифт для заголовков */ +} + +/* Подзаголовки (второй уровень: "АО" и "ПО") */ +.tree-table-subheader { + font-weight: 500; + /* Жирный шрифт для подзаголовков */ +} + +/* Ячейки таблицы */ +.tree-table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + white-space: nowrap; + /* Запрет на перенос текста */ + font-weight: normal; + /* Обычный шрифт для ячеек */ +} + +/* Цвет фона для заголовков */ +.tree-table-header { + background-color: #f4f4f4; +} + +/* Чередование цвета строк */ +.tree-table-row:nth-child(even) { + background-color: #f9f9f9; +} \ No newline at end of file diff --git a/src/Style/common.css b/src/Style/common.css new file mode 100644 index 0000000..d170447 --- /dev/null +++ b/src/Style/common.css @@ -0,0 +1,72 @@ +/* src/Style/common.css */ + +/* Контейнер для вкладок */ +.tabs { + display: flex; + gap: 5px; + /* Расстояние между вкладками */ + padding: 5px; + background-color: #3d74c7; + /* Цвет фона */ + border-bottom: 2px solid #195fc9; + /* Линия под вкладками */ + overflow-x: auto; + /* Прокрутка, если вкладок много */ + border-radius: 5px; + /* Скругление углов */ + white-space: nowrap; + /* Запрет переноса текста */ +} + +/* Стили для отдельной вкладки */ +.tab { + display: flex; + align-items: center; + background-color: #3d74c7; + /* Цвет фона вкладки */ + color: white; + /* Цвет текста */ + padding: 5px 15px; + /* Отступы внутри вкладки */ + border-radius: 5px 5px 0 0; + /* Скругление углов */ + cursor: pointer; + /* Курсор при наведении */ + flex-shrink: 0; + /* Запрет сжатия */ + transition: background-color 0.3s ease; + /* Плавное изменение цвета */ +} + +/* Активная вкладка */ +.tab.active { + background-color: #195fc9; + /* Цвет фона активной вкладки */ +} + +/* Кнопка закрытия вкладки */ +.close-tab { + background: none; + border: none; + color: white; + /* Цвет крестика */ + cursor: pointer; + font-size: 16px; + margin-left: 10px; + /* Отступ от текста */ + padding: 0; + transition: color 0.3s ease; + /* Плавное изменение цвета */ +} + +/* Эффект при наведении на кнопку закрытия */ +.close-tab:hover { + color: #ff6b6b; + /* Цвет крестика при наведении */ +} + +/* Эффект при наведении на вкладку */ +.tab:hover { + background-color: #195fc9; + /* Цвет фона при наведении */ +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 5a6ad3e..6c94c43 100755 --- a/src/index.css +++ b/src/index.css @@ -71,4 +71,28 @@ button:focus-visible { button { background-color: #f9f9f9; } +} + +/* Глобальный стиль для WebKit-браузеров (Chrome, Edge, Safari) */ +::-webkit-scrollbar { + width: 10px; /* Толщина вертикального скролла */ + height: 10px; /* Толщина горизонтального скролла */ +} + +/* Фон скроллбара */ +::-webkit-scrollbar-track { + background: #f1f1f1; /* Цвет фона */ + border-radius: 10px; /* Скругление углов */ +} + +/* Ползунок */ +::-webkit-scrollbar-thumb { + background: #3d74c7; /* Основной цвет */ + border-radius: 10px; /* Скругляем края */ + border: 1px solid #1c36c9; /* Белая обводка */ +} + +/* Эффект при наведении */ +::-webkit-scrollbar-thumb:hover { + background: #2b5aa5; /* Чуть темнее при наведении */ } \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 6bb98ac..b034150 100755 --- a/vite.config.js +++ b/vite.config.js @@ -5,6 +5,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { - host: true + host: true, + allowedHosts: ['dev.msf.enode'] } })