rc #6
|
|
@ -41,7 +41,6 @@ pipeline {
|
||||||
always {
|
always {
|
||||||
script {
|
script {
|
||||||
echo "Cleaning up workspace..."
|
echo "Cleaning up workspace..."
|
||||||
sh "rm -rf ${env.WORKSPACE}/package/ || true"
|
|
||||||
sh "rm -rf ${env.WORKSPACE}/rc/ || true"
|
sh "rm -rf ${env.WORKSPACE}/rc/ || true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +55,7 @@ pipeline {
|
||||||
-u "${GITEA_USER}:${GITEA_PASS}" \
|
-u "${GITEA_USER}:${GITEA_PASS}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"do":"merge"}' \
|
-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!"
|
echo "PR ${prId} merged successfully into master!"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,15 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
|
"recharts": "^2.15.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"chart.js": "^4.0.0",
|
"chart.js": "^4.0.0",
|
||||||
|
"chartjs-chart-box-and-violin-plot": "^4.0.0",
|
||||||
"react-chartjs-2": "^5.0.0",
|
"react-chartjs-2": "^5.0.0",
|
||||||
"axios": "^1.7.9"
|
"axios": "^1.7.9",
|
||||||
|
"react-datepicker": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Dashboard from "./Components/Dashboard";
|
import Dashboard from "./Components/Layout/Dashboard";
|
||||||
import LoginModal from "./Components/LoginModal"; // Импортируем компонент авторизации
|
import LoginModal from "./Components/UI/LoginModal"; // Импортируем компонент авторизации
|
||||||
import "./Style/LoginModal.css"; // Импортируем стили
|
import "./Style/LoginModal.css"; // Импортируем стили
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<h2>{metricName} ({metricType})</h2>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="instance" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
{allQuantiles.map((quantile, index) => (
|
||||||
|
<Bar
|
||||||
|
key={quantile}
|
||||||
|
dataKey={quantile}
|
||||||
|
fill={colors[index % colors.length]}
|
||||||
|
name={`Quantile ${quantile}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BarChartComponent;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const CounterComponent = ({ value, metricName }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px' }}>
|
||||||
|
<h2>{metricName}</h2>
|
||||||
|
<p style={{ fontSize: '48px', fontWeight: 'bold', color: '#3e95cd' }}>{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CounterComponent;
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="custom-tooltip" style={{ padding: '10px' }}>
|
||||||
|
<p>{`Время: ${label}`}</p> {/* Время из label */}
|
||||||
|
{payload.map((entry, index) => (
|
||||||
|
<p key={index} style={{}}>
|
||||||
|
{`Значение: ${entry.value}`} {/* Имя и значение из payload */}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{description}</h2>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<LineChart data={data}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="time" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip content={<CustomTooltip />} /> {/* Подключаем кастомный Tooltip */}
|
||||||
|
<Legend />
|
||||||
|
{Object.keys(chartData).map((key, index) => (
|
||||||
|
<Line
|
||||||
|
key={key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={key}
|
||||||
|
stroke={colors[index % colors.length]}
|
||||||
|
name={key}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Brush
|
||||||
|
dataKey="time"
|
||||||
|
height={30}
|
||||||
|
stroke="#8884d8"
|
||||||
|
startIndex={0} // Начальный индекс для Brush
|
||||||
|
endIndex={data.length - 1} // Конечный индекс для Brush (весь диапазон)
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LineChartComponent;
|
||||||
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<h2>{metricName} ({metricType})</h2>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<ScatterChart>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="time" />
|
||||||
|
<YAxis dataKey="value" />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
{Object.keys(chartData).map((instance, index) => (
|
||||||
|
<Scatter
|
||||||
|
key={instance}
|
||||||
|
data={chartData[instance]}
|
||||||
|
name={instance}
|
||||||
|
fill={colors[index % colors.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScatterChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScatterChartComponent;
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { Line } from "react-chartjs-2";
|
|
||||||
import axios from "axios";
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
LinearScale,
|
|
||||||
CategoryScale,
|
|
||||||
} from "chart.js";
|
|
||||||
import ExpandableInfo from "../Components/ExpandableInfo"
|
|
||||||
|
|
||||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale);
|
|
||||||
|
|
||||||
const GpuTemperatureChart = () => {
|
|
||||||
const chartRef = useRef(null);
|
|
||||||
const [data, setData] = useState({
|
|
||||||
labels: Array(10).fill("").map((_, i) => i), // 20 точек по X
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Температура GPU (°C)",
|
|
||||||
data: [], // Начальные значения (например, 50°C)
|
|
||||||
borderColor: "blue",
|
|
||||||
borderWidth: 2,
|
|
||||||
fill: false,
|
|
||||||
cubicInterpolationMode: "monotone", // Сглаживание
|
|
||||||
tension: 0.4, // Делаем линию плавнее
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу
|
|
||||||
setData({
|
|
||||||
labels: response.data.labels,
|
|
||||||
datasets: [{ ...data.datasets[0], data: response.data.datasets[0].data }],
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Ошибка загрузки данных:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Пример данных для меню "Подробнее"
|
|
||||||
const details = [
|
|
||||||
{ label: "Использование", value: " 20%" },
|
|
||||||
{ label: "Оперативная память ГП", value: " 1,2/7,9 ГБ" },
|
|
||||||
{ label: "Общая память ГП", value: " 1,2/7,9 ГБ" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">График температуры ГП</h2>
|
|
||||||
<Line
|
|
||||||
ref={chartRef}
|
|
||||||
data={data}
|
|
||||||
options={{
|
|
||||||
animation: false, // Отключаем анимацию обновления (чтобы был плавный сдвиг)
|
|
||||||
scales: {
|
|
||||||
x: { display: true },
|
|
||||||
y: { min: 30, max: 80 }, // Ограничиваем Y (например, 30-80°C)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ExpandableInfo details={details} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GpuTemperatureChart;
|
|
||||||
|
|
@ -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 <p>Loading...</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="time-range">Выберите временной диапазон: </label>
|
||||||
|
<select id="time-range" value={selectedRange.value} onChange={handleRangeChange}>
|
||||||
|
{TIME_RANGES.map(range => (
|
||||||
|
<option key={range.value} value={range.value}>{range.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Или выберите другой диапазон: </label>
|
||||||
|
<div>
|
||||||
|
<label>Начальная дата: </label>
|
||||||
|
<DatePicker
|
||||||
|
selected={startDate}
|
||||||
|
onChange={(date) => setStartDate(date)}
|
||||||
|
showTimeSelect
|
||||||
|
timeFormat="HH:mm"
|
||||||
|
timeIntervals={15}
|
||||||
|
dateFormat="yyyy-MM-dd HH:mm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Конечная дата: </label>
|
||||||
|
<DatePicker
|
||||||
|
selected={endDate}
|
||||||
|
onChange={(date) => setEndDate(date)}
|
||||||
|
showTimeSelect
|
||||||
|
timeFormat="HH:mm"
|
||||||
|
timeIntervals={15}
|
||||||
|
dateFormat="yyyy-MM-dd HH:mm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleCustomRangeChange}>Использовать кастомный диапазон</button>
|
||||||
|
</div>
|
||||||
|
<LineChartComponent
|
||||||
|
chartData={chartData}
|
||||||
|
metricName={metricName}
|
||||||
|
metricType={metricType}
|
||||||
|
colors={COLORS}
|
||||||
|
description={metricDescription}
|
||||||
|
brushRange={brushRange}
|
||||||
|
onBrushChange={setBrushRange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrometheusChart;
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { Line } from "react-chartjs-2";
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
LinearScale,
|
|
||||||
CategoryScale,
|
|
||||||
} from "chart.js";
|
|
||||||
import ExpandableInfo from "../Components/ExpandableInfo"
|
|
||||||
|
|
||||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale);
|
|
||||||
|
|
||||||
const RamUsageChart = () => {
|
|
||||||
const chartRef = useRef(null);
|
|
||||||
const [data, setData] = useState({
|
|
||||||
labels: Array(10).fill("").map((_, i) => i), // 20 точек по X
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Загруженность RAM (%)",
|
|
||||||
data: Array(20).fill(50), // Начальные значения (например, 50%)
|
|
||||||
borderColor: "green",
|
|
||||||
borderWidth: 2,
|
|
||||||
fill: false,
|
|
||||||
cubicInterpolationMode: "monotone", // Сглаживание
|
|
||||||
tension: 0.4, // Делаем линию плавнее
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setData((prevData) => {
|
|
||||||
const newTemp = Math.floor(Math.random() * 20) + 40; // Генерируем новую температуру (50-600°C)
|
|
||||||
const newLabels = [...prevData.labels.slice(1), prevData.labels[prevData.labels.length - 1] + 1]; // Сдвигаем ось X
|
|
||||||
const newDataset = [...prevData.datasets[0].data.slice(1), newTemp]; // Сдвигаем данные влево
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels: newLabels,
|
|
||||||
datasets: [{ ...prevData.datasets[0], data: newDataset }],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 1000); // Обновление каждую секунду
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Пример данных для меню "Подробнее"
|
|
||||||
const details = [
|
|
||||||
{ label: "Используется", value: " 6,2 ГБ" },
|
|
||||||
{ label: "Доступно", value: " 9,5 ГБ" },
|
|
||||||
{ label: "Выделено", value: " 6,8/18,2 ГБ" },
|
|
||||||
{ label: "Скорость", value: " 3200 МГц" },
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">График загруженности ОЗУ</h2>
|
|
||||||
<Line
|
|
||||||
ref={chartRef}
|
|
||||||
data={data}
|
|
||||||
options={{
|
|
||||||
animation: false, // Отключаем анимацию обновления (чтобы был плавный сдвиг)
|
|
||||||
scales: {
|
|
||||||
x: { display: true },
|
|
||||||
y: { min: 0, max: 100 }, // Ограничиваем Y (например, 30-80°C)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ExpandableInfo details={details} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RamUsageChart;
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale,
|
|
||||||
} from 'chart.js';
|
|
||||||
import 'chartjs-adapter-date-fns'; // Импортируем адаптер дат
|
|
||||||
|
|
||||||
// Регистрируем компоненты Chart.js
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale // Регистрируем временную шкалу
|
|
||||||
);
|
|
||||||
|
|
||||||
const NetworkSpeedChart = () => {
|
|
||||||
const [chartData, setChartData] = useState({
|
|
||||||
labels: [],
|
|
||||||
datasets: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const chartRef = useRef(null); // Референс на график
|
|
||||||
|
|
||||||
// Функция для загрузки данных
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('http://192.168.2.33:3000/metrics?metric=zvks_abonents_total');
|
|
||||||
const newData = response.data;
|
|
||||||
|
|
||||||
console.log('New data from backend:', newData); // Проверяем новые данные
|
|
||||||
|
|
||||||
// Обновляем состояние, добавляя новые данные к существующим
|
|
||||||
setChartData((prevChartData) => {
|
|
||||||
// Группируем новые данные по устройству (device)
|
|
||||||
const newGroupedData = newData.reduce((acc, entry) => {
|
|
||||||
const device = entry.device;
|
|
||||||
if (!acc[device]) {
|
|
||||||
acc[device] = [];
|
|
||||||
}
|
|
||||||
acc[device].push(entry);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Создаем новый набор данных
|
|
||||||
const newDatasets = Object.keys(newGroupedData).map((device, index) => {
|
|
||||||
// Находим существующий dataset для этого устройства
|
|
||||||
const existingDataset = prevChartData.datasets.find((dataset) => dataset.label === `Device: ${device}`);
|
|
||||||
|
|
||||||
// Если dataset уже существует, добавляем новые данные к нему
|
|
||||||
if (existingDataset) {
|
|
||||||
return {
|
|
||||||
...existingDataset,
|
|
||||||
data: [
|
|
||||||
...existingDataset.data,
|
|
||||||
...newGroupedData[device].map((entry) => ({
|
|
||||||
x: new Date(entry.timestamp), // Временная метка
|
|
||||||
y: entry.value, // Значение
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если dataset не существует, создаем новый
|
|
||||||
return {
|
|
||||||
label: `Device: ${device}`,
|
|
||||||
data: newGroupedData[device].map((entry) => ({
|
|
||||||
x: new Date(entry.timestamp),
|
|
||||||
y: entry.value,
|
|
||||||
})),
|
|
||||||
borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`,
|
|
||||||
backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`,
|
|
||||||
tension: 0.2,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновляем labels (метки времени)
|
|
||||||
const newLabels = [
|
|
||||||
...prevChartData.labels,
|
|
||||||
...newData.map((entry) => new Date(entry.timestamp)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels: newLabels,
|
|
||||||
datasets: newDatasets,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при загрузке метрик:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Загружаем данные при монтировании компонента и обновляем каждые 5 секунд
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
const interval = setInterval(fetchData, 5000);
|
|
||||||
|
|
||||||
// Очищаем интервал и уничтожаем график при размонтировании компонента
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
if (chartRef.current) {
|
|
||||||
chartRef.current.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Опции графика
|
|
||||||
const options = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'top',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'node_network_receive_bytes_total',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time', // Используем временную шкалу
|
|
||||||
time: {
|
|
||||||
unit: 'second', // Единица времени
|
|
||||||
displayFormats: {
|
|
||||||
second: 'HH:mm:ss', // Формат отображения времени
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Time',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Данные',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
duration: 1000, // Длительность анимации
|
|
||||||
easing: 'linear', // Тип анимации
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '800px', height: '400px' }}>
|
|
||||||
<Line ref={chartRef} data={chartData} options={options} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkSpeedChart;
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale,
|
|
||||||
} from 'chart.js';
|
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale
|
|
||||||
);
|
|
||||||
|
|
||||||
const MAX_DATA_POINTS = 50;
|
|
||||||
|
|
||||||
const NetworkSpeedChart2 = () => {
|
|
||||||
const [chartData, setChartData] = useState({ labels: [], datasets: [] });
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('http://192.168.2.33:3000/metrics?metric=node_time_seconds');
|
|
||||||
const newData = response.data;
|
|
||||||
|
|
||||||
setChartData((prevChartData) => {
|
|
||||||
const newGroupedData = newData.reduce((acc, entry) => {
|
|
||||||
if (!acc[entry.device]) acc[entry.device] = [];
|
|
||||||
acc[entry.device].push({ x: new Date(entry.timestamp), y: entry.value });
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const newDatasets = Object.keys(newGroupedData).map((device, index) => {
|
|
||||||
const existingDataset = prevChartData.datasets.find((d) => d.label === `Device: ${device}`);
|
|
||||||
const updatedData = existingDataset ? [...existingDataset.data, ...newGroupedData[device]] : newGroupedData[device];
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: `Device: ${device}`,
|
|
||||||
data: updatedData.slice(-MAX_DATA_POINTS),
|
|
||||||
borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`,
|
|
||||||
backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`,
|
|
||||||
tension: 0.2,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { labels: newDatasets[0]?.data.map((d) => d.x) || [], datasets: newDatasets };
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при загрузке метрик:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
const interval = setInterval(fetchData, 5000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: { position: 'top' },
|
|
||||||
title: { display: true, text: 'node_time_seconds' },
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
time: { unit: 'second', displayFormats: { second: 'HH:mm:ss' } },
|
|
||||||
title: { display: true, text: 'Time' },
|
|
||||||
},
|
|
||||||
y: { title: { display: true, text: 'Value' } },
|
|
||||||
},
|
|
||||||
animation: { duration: 1000, easing: 'linear' },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '800px', height: '400px' }}>
|
|
||||||
<Line data={chartData} options={options} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkSpeedChart2;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale } from 'chart.js';
|
|
||||||
|
|
||||||
// Регистрация компонентов Chart.js
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale);
|
|
||||||
|
|
||||||
const SimpleGraph = () => {
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
// Загружаем данные из файла с использованием axios
|
|
||||||
const response = await axios.get('/data.json'); // Путь должен быть относительно папки public
|
|
||||||
const rawData = response.data;
|
|
||||||
|
|
||||||
// Проверяем, что данные действительно массив
|
|
||||||
if (Array.isArray(rawData)) {
|
|
||||||
const chartData = rawData.map(item => ({
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
value: item.value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setData(chartData);
|
|
||||||
} else {
|
|
||||||
throw new Error('Ошибка: Данные не являются массивом.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (data.length === 0) return <div>Loading...</div>;
|
|
||||||
|
|
||||||
// Настройки графика
|
|
||||||
const chartOptions = {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Simple Data Graph',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartData = {
|
|
||||||
labels: data.map(item => item.timestamp), // Массив меток для оси X
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: 'Value',
|
|
||||||
data: data.map(item => item.value), // Массив значений для оси Y
|
|
||||||
borderColor: 'rgb(75, 192, 192)',
|
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
||||||
fill: false,
|
|
||||||
tension: 0.1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Line data={chartData} options={chartOptions} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimpleGraph;
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import SidebarMenu from "./SidebarMenu";
|
|
||||||
import SystemStatusTable from "../Charts/SystemStatusTable";
|
|
||||||
import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware";
|
|
||||||
import TreeChart from "./TreeChart";
|
|
||||||
import "../Style/Dashboard.css";
|
|
||||||
import ErrorIndicator from "./ErrorIndicator";
|
|
||||||
import tabContentData from "./tabContent";
|
|
||||||
import menuData from "./menuData.json"; // Загружаем новое меню
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
|
||||||
const [tabs, setTabs] = useState([]);
|
|
||||||
const [activeTab, setActiveTab] = useState("Главная");
|
|
||||||
const [tabContent, setTabContent] = useState({});
|
|
||||||
const [treeData, setTreeData] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTabContent(tabContentData);
|
|
||||||
setTreeData(menuData); // Теперь menuData - объект, а не массив
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOpenTab = (id, title) => {
|
|
||||||
if (!tabs.includes(id)) {
|
|
||||||
setTabs([...tabs, id]);
|
|
||||||
}
|
|
||||||
setActiveTab(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseTab = (id) => {
|
|
||||||
const newTabs = tabs.filter((tab) => tab !== id);
|
|
||||||
setTabs(newTabs);
|
|
||||||
if (activeTab === id) {
|
|
||||||
setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabContent = () => {
|
|
||||||
if (activeTab === "Главная") {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Общий мониторинг</h2>
|
|
||||||
<ErrorIndicator />
|
|
||||||
<SystemStatusTable />
|
|
||||||
<SystemStatusTableSoftware />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (activeTab === "Визуализация") {
|
|
||||||
return <TreeChart data={treeData} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
|
|
||||||
} else {
|
|
||||||
const tabData = tabContent[activeTab];
|
|
||||||
return tabData ? tabData.content : <p>Нет данных</p>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="dashboard-container">
|
|
||||||
<SidebarMenu onOpenTab={handleOpenTab} />
|
|
||||||
|
|
||||||
<div className="main-content">
|
|
||||||
<div className="tabs">
|
|
||||||
<div
|
|
||||||
className={`tab ${activeTab === "Главная" ? "active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("Главная")}
|
|
||||||
>
|
|
||||||
Главная
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`tab ${activeTab === "Визуализация" ? "active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("Визуализация")}
|
|
||||||
>
|
|
||||||
Визуализация
|
|
||||||
</div>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<div
|
|
||||||
key={tab}
|
|
||||||
className={`tab ${activeTab === tab ? "active" : ""}`}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
<button
|
|
||||||
className="close-tab"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCloseTab(tab);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="content">
|
|
||||||
{renderTabContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<h2>Общий мониторинг</h2>
|
||||||
|
<ErrorIndicator />
|
||||||
|
<TreeTable data={treeData.items} /> {/* Используем актуальные данные */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (activeTab === "Визуализация") {
|
||||||
|
return <TreeChart data={treeData} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
|
||||||
|
} else {
|
||||||
|
const tabData = tabContent[activeTab];
|
||||||
|
return tabData ? tabData.content : <p>Нет данных</p>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard-container">
|
||||||
|
<div
|
||||||
|
className="sidebar"
|
||||||
|
ref={sidebarRef}
|
||||||
|
style={{ width: sidebarWidth }} // Динамическая ширина сайдбара
|
||||||
|
>
|
||||||
|
<SidebarMenu data={treeData} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} />
|
||||||
|
{/* Элемент для перетаскивания */}
|
||||||
|
<div
|
||||||
|
className="sidebar-resizer"
|
||||||
|
onMouseDown={startResizing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="main-content" style={{ marginLeft: sidebarWidth }}>
|
||||||
|
<Tabs
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabClick={(id) => setActiveTab(id)}
|
||||||
|
onCloseTab={handleCloseTab}
|
||||||
|
/>
|
||||||
|
<div className="content">
|
||||||
|
{renderTabContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="menu-item" style={{ width: sidebarWidth - 20 }}> {/* Динамическая ширина */}
|
||||||
|
<div
|
||||||
|
onClick={handleSingleClick} // Одинарный клик для разворачивания/сворачивания или открытия
|
||||||
|
className="menu-item-header"
|
||||||
|
>
|
||||||
|
{/* Круглый индикатор статуса */}
|
||||||
|
<div
|
||||||
|
className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`}
|
||||||
|
style={{ backgroundColor: statusColor }}
|
||||||
|
/>
|
||||||
|
<span>{item.title}</span>
|
||||||
|
|
||||||
|
{/* Иконка для открытия родителя */}
|
||||||
|
{hasChildren && (
|
||||||
|
<span
|
||||||
|
onClick={handleOpenParent}
|
||||||
|
className="open-parent-icon"
|
||||||
|
title="Открыть родителя"
|
||||||
|
>
|
||||||
|
📂
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Иконка для разворачивания/сворачивания */}
|
||||||
|
{hasChildren && <span>{isOpen ? "▲" : "▼"}</span>}
|
||||||
|
</div>
|
||||||
|
{isOpen && hasChildren && (
|
||||||
|
<div className="submenu">
|
||||||
|
{item.items.map((child, index) => (
|
||||||
|
<MenuItem key={index} item={child} onSelectItem={onSelectItem} sidebarWidth={sidebarWidth} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function SidebarMenu({ data, onOpenTab, sidebarWidth }) {
|
||||||
|
const handleSelectItem = (item) => {
|
||||||
|
onOpenTab(item.id, item.title);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sidebar">
|
||||||
|
<div className="sidebar-content" style={{ width: sidebarWidth }}> {/* Динамическая ширина */}
|
||||||
|
<h2 className="sidebar-title">Меню</h2>
|
||||||
|
<MenuItem item={data} onSelectItem={handleSelectItem} sidebarWidth={sidebarWidth} />
|
||||||
|
</div>
|
||||||
|
<div className="sidebar-footer" style={{ width: sidebarWidth }}> {/* Динамическая ширина */}
|
||||||
|
<h2 className="help">Помощь</h2>
|
||||||
|
<h2 className="settings">Настройка</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarMenu;
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
const Login = ({ onLogin, onClose }) => {
|
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (username === "admin" && password === "admin") {
|
|
||||||
onLogin(); // Успешная авторизация
|
|
||||||
onClose(); // Закрыть модальное окно
|
|
||||||
} else {
|
|
||||||
setError("Неверный логин или пароль");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="modal-overlay">
|
|
||||||
<div className="modal">
|
|
||||||
<h2>Авторизация</h2>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<label>Логин:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Пароль:</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{error && <p className="error">{error}</p>}
|
|
||||||
<button type="submit">Войти</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import "../Style/SidebarMenu.css";
|
|
||||||
import menuData from "./menuData.json";
|
|
||||||
|
|
||||||
const MenuItem = ({ item, onSelectItem }) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
if (hasChildren) {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
} else {
|
|
||||||
onSelectItem(item);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="menu-item">
|
|
||||||
<div onClick={handleClick} className="menu-item-header">
|
|
||||||
<span>{item.title}</span>
|
|
||||||
{hasChildren && <span>{isOpen ? "▲" : "▼"}</span>}
|
|
||||||
</div>
|
|
||||||
{isOpen && hasChildren && (
|
|
||||||
<div className="submenu">
|
|
||||||
{item.items.map((child, index) => (
|
|
||||||
<MenuItem key={index} item={child} onSelectItem={onSelectItem} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function SidebarMenu({ onOpenTab }) {
|
|
||||||
const handleSelectItem = (item) => {
|
|
||||||
onOpenTab(item.id, item.title); // Передаем id и title
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sidebar">
|
|
||||||
<h2 className="sidebar-title">Меню</h2>
|
|
||||||
<MenuItem item={menuData} onSelectItem={handleSelectItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SidebarMenu;
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
const TreeChart = ({ data, onNodeClick }) => {
|
|
||||||
const chartRef = useRef();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// Очищаем старый граф перед отрисовкой
|
|
||||||
d3.select(chartRef.current).selectAll("*").remove();
|
|
||||||
|
|
||||||
const width = 928;
|
|
||||||
const height = 600;
|
|
||||||
|
|
||||||
const root = d3.hierarchy(data, (d) => d.items);
|
|
||||||
const links = root.links();
|
|
||||||
const nodes = root.descendants();
|
|
||||||
|
|
||||||
const simulation = d3
|
|
||||||
.forceSimulation(nodes)
|
|
||||||
.force("link", d3.forceLink(links).id((d) => d.data.title).distance(80).strength(1)) // Увеличил дистанцию
|
|
||||||
.force("charge", d3.forceManyBody().strength(-500)) // Увеличил отталкивание узлов
|
|
||||||
.force("x", d3.forceX())
|
|
||||||
.force("y", d3.forceY());
|
|
||||||
|
|
||||||
const svg = d3
|
|
||||||
.select(chartRef.current)
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", height)
|
|
||||||
.attr("viewBox", [-width / 2, -height / 2, width, height])
|
|
||||||
.attr("style", "max-width: 100%; height: auto;");
|
|
||||||
|
|
||||||
const link = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("stroke", "#999")
|
|
||||||
.attr("stroke-opacity", 0.6)
|
|
||||||
.selectAll("line")
|
|
||||||
.data(links)
|
|
||||||
.join("line");
|
|
||||||
|
|
||||||
const node = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("stroke", "#000")
|
|
||||||
.attr("stroke-width", 1.5)
|
|
||||||
.selectAll("circle")
|
|
||||||
.data(nodes)
|
|
||||||
.join("circle")
|
|
||||||
.attr("fill", (d) => (d.children ? "#555" : "#000"))
|
|
||||||
.attr("stroke", "#fff")
|
|
||||||
.attr("r", 7) // Немного увеличил размер узлов для удобства клика
|
|
||||||
.call(drag(simulation));
|
|
||||||
|
|
||||||
// Добавляем текстовые подписи
|
|
||||||
const text = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("fill", "#000")
|
|
||||||
.attr("font-family", "Arial")
|
|
||||||
.attr("font-size", 12)
|
|
||||||
.attr("pointer-events", "none") // Отключаем обработку событий текста
|
|
||||||
.selectAll("text")
|
|
||||||
.data(nodes)
|
|
||||||
.join("text")
|
|
||||||
.text((d) => d.data.title)
|
|
||||||
.attr("dx", 12) // Отодвигаем текст дальше от узла
|
|
||||||
.attr("dy", 4) // Немного поднимаем текст
|
|
||||||
|
|
||||||
node.append("title").text((d) => d.data.title);
|
|
||||||
|
|
||||||
node.on("click", (event, d) => {
|
|
||||||
if (onNodeClick) {
|
|
||||||
onNodeClick(d.data.id, d.data.title); // Передаем id и title
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
simulation.on("tick", () => {
|
|
||||||
link
|
|
||||||
.attr("x1", (d) => d.source.x)
|
|
||||||
.attr("y1", (d) => d.source.y)
|
|
||||||
.attr("x2", (d) => d.target.x)
|
|
||||||
.attr("y2", (d) => d.target.y);
|
|
||||||
|
|
||||||
node
|
|
||||||
.attr("cx", (d) => d.x)
|
|
||||||
.attr("cy", (d) => d.y);
|
|
||||||
|
|
||||||
text
|
|
||||||
.attr("x", (d) => d.x + 12) // Смещаем текст правее узла
|
|
||||||
.attr("y", (d) => d.y + 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
simulation.stop();
|
|
||||||
};
|
|
||||||
}, [data, onNodeClick]);
|
|
||||||
|
|
||||||
const drag = (simulation) => {
|
|
||||||
function dragstarted(event, d) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragged(event, d) {
|
|
||||||
d.fx = event.x;
|
|
||||||
d.fy = event.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragended(event, d) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <svg ref={chartRef}></svg>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TreeChart;
|
|
||||||
|
|
@ -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 <svg ref={chartRef} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TreeChart;
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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": "ПО"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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": "ПО"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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 = (
|
||||||
|
<div>
|
||||||
|
<h2>{node.title}</h2>
|
||||||
|
<p>Контент для {node.title}.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Если у узла есть потомки, добавляем графики для всех потомков
|
||||||
|
if (node.items && node.items.length > 0) {
|
||||||
|
const childIds = getAllChildIds(node); // Получаем все id потомков
|
||||||
|
const charts = childIds.map((id) => {
|
||||||
|
const metricName = getMetricName(id);
|
||||||
|
return (
|
||||||
|
<div key={id}>
|
||||||
|
<h3>{node.title} - {id}</h3>
|
||||||
|
<Suspense fallback={<div>Загрузка графика...</div>}>
|
||||||
|
<PrometheusChart metricName={metricName} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
<h2>{node.title}</h2>
|
||||||
|
<p>Контент для {node.title}.</p>
|
||||||
|
{charts}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Если у узла нет потомков, добавляем график для него
|
||||||
|
|
||||||
|
const metricName = getMetricName(node.id);
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
<h2>{node.title}</h2>
|
||||||
|
<p>Контент для {node.title}.</p>
|
||||||
|
<Suspense fallback={<div>Загрузка графика...</div>}>
|
||||||
|
<PrometheusChart metricName={metricName} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем контент для текущего 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; // Экспортируем только функцию
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import React from "react";
|
||||||
|
import PrometheusChart from '../../Charts/PrometheusChart';
|
||||||
|
|
||||||
|
const tabContent = {
|
||||||
|
// Сервис ВКС
|
||||||
|
service1: { title: "Сервис ВКС", content: <div><h2>Сервис ВКС</h2></div> },
|
||||||
|
|
||||||
|
// Функциональные задачи
|
||||||
|
system_control: { title: "Контроль системы", content: <div><h2>Контроль системы</h2><p>Описание контроля.</p></div> },
|
||||||
|
system_management: { title: "Система управления", content: <div><h2>Система управления</h2><p>Описание системы управления.</p></div> },
|
||||||
|
conference: { title: "Проведение ВКС", content: <div><h2>Проведение ВКС</h2><p>Информация о проведении ВКС.</p></div> },
|
||||||
|
backup: { title: "Резервное копирование", content: <div><h2>Резервное копирование</h2><p>Процесс резервного копирования.</p></div> },
|
||||||
|
relay_info: { title: "Ретрансляция информации", content: <div><h2>Ретрансляция информации</h2><p>Детали ретрансляции.</p></div> },
|
||||||
|
|
||||||
|
// Медиа сервер 1
|
||||||
|
media_system_software_1: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора медиа сервера.</p></div> },
|
||||||
|
media_system_software_2: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти медиа сервера.</p></div> },
|
||||||
|
media_system_software_3: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска медиа сервера.</p></div> },
|
||||||
|
media_system_software_4: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров медиа сервера.</p></div> },
|
||||||
|
media_software_1: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><PrometheusChart /></div> },
|
||||||
|
media_software_2: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_3: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_4: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
|
||||||
|
// Медиа сервер 2
|
||||||
|
media_system_software_1_2: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора медиа сервера.</p></div> },
|
||||||
|
media_system_software_2_2: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти медиа сервера.</p></div> },
|
||||||
|
media_system_software_3_2: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска медиа сервера.</p></div> },
|
||||||
|
media_system_software_4_2: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров медиа сервера.</p></div> },
|
||||||
|
media_software_1_2: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><PrometheusChart /></div> },
|
||||||
|
media_software_2_2: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_3_2: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_4_2: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
|
||||||
|
// Медиа сервер 3
|
||||||
|
media_system_software_1_3: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора медиа сервера.</p></div> },
|
||||||
|
media_system_software_2_3: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти медиа сервера.</p></div> },
|
||||||
|
media_system_software_3_3: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска медиа сервера.</p></div> },
|
||||||
|
media_system_software_4_3: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров медиа сервера.</p></div> },
|
||||||
|
media_software_1_3: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><PrometheusChart /></div> },
|
||||||
|
media_software_2_3: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_3_3: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_4_3: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
|
||||||
|
// Медиа сервер 4
|
||||||
|
media_system_software_1_4: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора медиа сервера.</p></div> },
|
||||||
|
media_system_software_2_4: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти медиа сервера.</p></div> },
|
||||||
|
media_system_software_3_4: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска медиа сервера.</p></div> },
|
||||||
|
media_system_software_4_4: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров медиа сервера.</p></div> },
|
||||||
|
media_software_1_4: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><PrometheusChart /></div> },
|
||||||
|
media_software_2_4: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_3_4: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_4_4: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
|
||||||
|
// Медиа сервер 5
|
||||||
|
media_system_software_1_5: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора медиа сервера.</p></div> },
|
||||||
|
media_system_software_2_5: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти медиа сервера.</p></div> },
|
||||||
|
media_system_software_3_5: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска медиа сервера.</p></div> },
|
||||||
|
media_system_software_4_5: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров медиа сервера.</p></div> },
|
||||||
|
media_software_1_5: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><PrometheusChart /></div> },
|
||||||
|
media_software_2_5: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_3_5: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
media_software_4_5: { title: "ПО", content: <div><h2>Программное обеспечение медиа сервера</h2><p>Описание ПО медиа сервера.</p></div> },
|
||||||
|
|
||||||
|
// Сервер резервного копирования
|
||||||
|
copy_system_software_1: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора сервера резервного копирования.</p></div> },
|
||||||
|
copy_system_software_2: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти сервера резервного копирования.</p></div> },
|
||||||
|
copy_system_software_3: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска сервера резервного копирования.</p></div> },
|
||||||
|
copy_system_software_4: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров сервера резервного копирования.</p></div> },
|
||||||
|
copy_software_1: { title: "ПО", content: <div><h2>Программное обеспечение сервера резервного копирования</h2><p>Описание ПО сервера резервного копирования.</p></div> },
|
||||||
|
copy_software_2: { title: "ПО", content: <div><h2>Программное обеспечение сервера резервного копирования</h2><p>Описание ПО сервера резервного копирования.</p></div> },
|
||||||
|
copy_software_3: { title: "ПО", content: <div><h2>Программное обеспечение сервера резервного копирования</h2><p>Описание ПО сервера резервного копирования.</p></div> },
|
||||||
|
copy_software_4: { title: "ПО", content: <div><h2>Программное обеспечение сервера резервного копирования</h2><p>Описание ПО сервера резервного копирования.</p></div> },
|
||||||
|
|
||||||
|
// Сервер системы управления
|
||||||
|
control_system_software_1: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора сервера системы управления.</p></div> },
|
||||||
|
control_system_software_2: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти сервера системы управления.</p></div> },
|
||||||
|
control_system_software_3: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска сервера системы управления.</p></div> },
|
||||||
|
control_system_software_4: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров сервера системы управления.</p></div> },
|
||||||
|
control_software_1: { title: "ПО", content: <div><h2>Программное обеспечение сервера системы управления</h2><p>Описание ПО сервера системы управления.</p></div> },
|
||||||
|
control_software_2: { title: "ПО", content: <div><h2>Программное обеспечение сервера системы управления</h2><p>Описание ПО сервера системы управления.</p></div> },
|
||||||
|
control_software_3: { title: "ПО", content: <div><h2>Программное обеспечение сервера системы управления</h2><p>Описание ПО сервера системы управления.</p></div> },
|
||||||
|
control_software_4: { title: "ПО", content: <div><h2>Программное обеспечение сервера системы управления</h2><p>Описание ПО сервера системы управления.</p></div> },
|
||||||
|
|
||||||
|
// Сервер сбора и ретрансляции информации
|
||||||
|
system_software_1: { title: "Центральный процессор", content: <div><h2>Центральный процессор</h2><p>Описание центрального процессора сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
system_software_2: { title: "Оперативная память", content: <div><h2>Оперативная память</h2><p>Описание оперативной памяти сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
system_software_3: { title: "Жесткий диск", content: <div><h2>Жесткий диск</h2><p>Описание жесткого диска сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
system_software_4: { title: "Сетевые адаптеры", content: <div><h2>Сетевые адаптеры</h2><p>Описание сетевых адаптеров сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
software_1: { title: "ПО", content: <div><h2>Программное обеспечение сервера сбора и ретрансляции информации</h2><p>Описание ПО сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
software_2: { title: "ПО", content: <div><h2>Программное обеспечение сервера сбора и ретрансляции информации</h2><p>Описание ПО сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
software_3: { title: "ПО", content: <div><h2>Программное обеспечение сервера сбора и ретрансляции информации</h2><p>Описание ПО сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
software_4: { title: "ПО", content: <div><h2>Программное обеспечение сервера сбора и ретрансляции информации</h2><p>Описание ПО сервера сбора и ретрансляции информации.</p></div> },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tabContent;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import criticalIcon from "../assets/images/critical.png"; // Красный треугольник
|
import criticalIcon from "../../assets/images/critical.png"; // Красный треугольник
|
||||||
import warningIcon from "../assets/images/warning.png"; // Желтый треугольник
|
import warningIcon from "../../assets/images/warning.png"; // Желтый треугольник
|
||||||
import "../Style/ErrorIndicator.css"; // Подключаем стили
|
import "../../Style/ErrorIndicator.css"; // Подключаем стили
|
||||||
|
|
||||||
const ErrorIndicator = ({ criticalCount, warningCount }) => {
|
const ErrorIndicator = ({ criticalCount, warningCount }) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -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 (
|
||||||
|
<Modal onClose={onClose}>
|
||||||
|
<h2>Авторизация</h2>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<label>Логин:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Пароль:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
<button type="submit">Войти</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginModal;
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Modal = ({ children, onClose }) => {
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
{children}
|
||||||
|
<button onClick={onClose}>Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="tabs">
|
||||||
|
{/* Всегда отображаемые вкладки */}
|
||||||
|
<div
|
||||||
|
className={`tab ${activeTab === "Главная" ? "active" : ""}`}
|
||||||
|
onClick={() => onTabClick("Главная")}
|
||||||
|
onMouseDown={(e) => handleMouseDown(e, "Главная")} // Добавляем обработчик для СКМ
|
||||||
|
>
|
||||||
|
<span>Главная</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`tab ${activeTab === "Визуализация" ? "active" : ""}`}
|
||||||
|
onClick={() => onTabClick("Визуализация")}
|
||||||
|
onMouseDown={(e) => handleMouseDown(e, "Визуализация")} // Добавляем обработчик для СКМ
|
||||||
|
>
|
||||||
|
<span>Визуализация</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Динамически добавляемые вкладки */}
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
className={`tab ${activeTab === tab.id ? "active" : ""}`}
|
||||||
|
onClick={() => onTabClick(tab.id)}
|
||||||
|
onMouseDown={(e) => handleMouseDown(e, tab.id)} // Добавляем обработчик для СКМ
|
||||||
|
>
|
||||||
|
<span>{tab.title}</span>
|
||||||
|
<button
|
||||||
|
className="close-tab"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCloseTab(tab.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="table-container">
|
||||||
|
<table className="tree-table">
|
||||||
|
<thead>
|
||||||
|
{/* Первый уровень: Заголовки "Медиа сервер" */}
|
||||||
|
<tr>
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<th key={index} colSpan="2" className="tree-table-header" style={{ backgroundColor: getStatusColor(item.status) }}>
|
||||||
|
{item.title}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
{/* Второй уровень: "АО" и "ПО" */}
|
||||||
|
<tr>
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<td className="tree-table-subheader" style={{ backgroundColor: getStatusColor(item.items[0]?.status) }}>
|
||||||
|
АО
|
||||||
|
</td>
|
||||||
|
<td className="tree-table-subheader" style={{ backgroundColor: getStatusColor(item.items[1]?.status) }}>
|
||||||
|
ПО
|
||||||
|
</td>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{/* Третий уровень: Вложенные элементы "АО" и "ПО" */}
|
||||||
|
{renderRows(filteredData)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для отображения строк с вложенными элементами
|
||||||
|
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(
|
||||||
|
<tr key={i} className="tree-table-row">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<td className="tree-table-cell" style={{ backgroundColor: getStatusColor(item.items[0]?.items[i]?.status) }}>
|
||||||
|
{item.items[0]?.items[i]?.title || ""}
|
||||||
|
</td>
|
||||||
|
<td className="tree-table-cell" style={{ backgroundColor: getStatusColor(item.items[1]?.items[i]?.status) }}>
|
||||||
|
{item.items[1]?.items[i]?.title || ""}
|
||||||
|
</td>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
export default TreeTable;
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
"title": "Сервис ВКС",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "Функциональные задачи",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "system_control",
|
|
||||||
"title": "Контроль системы"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "system_management",
|
|
||||||
"title": "Система управления"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "conference",
|
|
||||||
"title": "Проведение ВКС"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "backup",
|
|
||||||
"title": "Резервное копирование"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "relay_info",
|
|
||||||
"title": "Ретрансляция информации"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Аппаратное ПО",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "hardware_software_1",
|
|
||||||
"title": "ПО1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_2",
|
|
||||||
"title": "ПО2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_3",
|
|
||||||
"title": "ПО3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_4",
|
|
||||||
"title": "ПО4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "hardware_software_5",
|
|
||||||
"title": "ПО5"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import NetworkSpeedChart2 from '../Charts/TestCharts2';
|
|
||||||
|
|
||||||
const tabContent = {
|
|
||||||
service1: { title: "Сервис ВКС", content: <div><h2>Сервис ВКС</h2></div> },
|
|
||||||
system_control: { title: "Контроль системы", content: <div><h2>Контроль системы</h2><p>Описание контроля.</p></div> },
|
|
||||||
system_management: { title: "Система управления", content: <div><h2>Система управления</h2><p>Описание системы управления.</p></div> },
|
|
||||||
conference: { title: "Проведение ВКС", content: <div><h2>Проведение ВКС</h2><p>Информация о проведении ВКС.</p></div> },
|
|
||||||
backup: { title: "Резервное копирование", content: <div><h2>Резервное копирование</h2><p>Процесс резервного копирования.</p></div> },
|
|
||||||
relay_info: { title: "Ретрансляция информации", content: <div><h2>Ретрансляция информации</h2><p>Детали ретрансляции.</p></div> },
|
|
||||||
hardware_software_1: { title: "График скорости сети", content: <div><h2>График скорости сети</h2><NetworkSpeedChart2 /></div> },
|
|
||||||
hardware_software_2: { title: "ПО2", content: <div><h2>ПО2</h2></div> },
|
|
||||||
hardware_software_3: { title: "ПО3", content: <div><h2>ПО3</h2></div> },
|
|
||||||
hardware_software_4: { title: "ПО4", content: <div><h2>ПО4</h2></div> },
|
|
||||||
hardware_software_5: { title: "ПО5", content: <div><h2>ПО5</h2></div> },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tabContent;
|
|
||||||
|
|
@ -1,62 +1,56 @@
|
||||||
|
/* Основной контейнер */
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: calc(100vw - 20px); /* Учитываем отступ */
|
||||||
overflow: hidden;
|
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 {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 400px;
|
|
||||||
max-width: calc(100vw - 250px);
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
margin-left: 50px;
|
||||||
overflow-y: auto;
|
transition: margin-left 0.2s ease;
|
||||||
/* Добавляем вертикальную прокрутку */
|
overflow: 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Контент */
|
/* Контент */
|
||||||
|
|
@ -64,22 +58,12 @@
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 10px;
|
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; /* Включаем скролл, если нужно */
|
||||||
.default-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Заголовки */
|
||||||
h2 {
|
h2 {
|
||||||
color: #444;
|
color: #444;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-item {
|
.error-item {
|
||||||
|
|
@ -33,4 +34,5 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
background: white;
|
background: rgb(255, 255, 255);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
.modal label {
|
.modal label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal input {
|
.modal input {
|
||||||
|
|
@ -38,7 +39,8 @@
|
||||||
|
|
||||||
.modal button {
|
.modal button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: #007bff;
|
margin-bottom: 5px;
|
||||||
|
background: #08294b;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,124 @@
|
||||||
/* Боковое меню */
|
/* Боковое меню */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 250px;
|
|
||||||
background-color: #333;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-right: 1px solid #444;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
/* Занимает всю высоту экрана */
|
background-color: #3d74c7;
|
||||||
overflow-y: auto;
|
color: white;
|
||||||
/* Прокрутка внутри меню, если контент не помещается */
|
position: fixed;
|
||||||
position: sticky;
|
left: 0;
|
||||||
/* Фиксируем меню */
|
|
||||||
top: 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 {
|
.sidebar-title {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
/* Добавляем отступы */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Элементы меню */
|
||||||
.menu-item {
|
.menu-item {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
width: 100%;
|
||||||
|
/* Ширина на всю ширину сайдбара */
|
||||||
h2 {
|
|
||||||
color: white
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item-header {
|
.menu-item-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
/* Выравниваем элементы по центру */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #444;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item-header:hover {
|
.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 {
|
.submenu {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-container {
|
/* Футер сайдбара */
|
||||||
margin-top: 20px;
|
.sidebar-footer {
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #444;
|
background-color: #3d74c7;
|
||||||
border: 1px solid #333;
|
text-align: center;
|
||||||
border-radius: 5px;
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
margin-bottom: 5px;
|
/* Разделительная линия */
|
||||||
cursor: pointer;
|
flex-shrink: 0;
|
||||||
transition: background-color 0.3s ease;
|
/* Запрещаем сжатие */
|
||||||
|
width: 100%;
|
||||||
|
/* Ширина на всю ширину сайдбара */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.help,
|
||||||
background-color: #222;
|
.settings {
|
||||||
|
color: white;
|
||||||
|
margin: 5px 0;
|
||||||
|
/* Отступы между элементами */
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* Убираем горизонтальную прокрутку */
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
/* Цвет фона при наведении */
|
||||||
|
}
|
||||||
|
|
@ -71,4 +71,28 @@ button:focus-visible {
|
||||||
button {
|
button {
|
||||||
background-color: #f9f9f9;
|
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; /* Чуть темнее при наведении */
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import react from '@vitejs/plugin-react'
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host: true
|
host: true,
|
||||||
|
allowedHosts: ['dev.msf.enode']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue