Compare commits

..

21 Commits

Author SHA1 Message Date
deployer3000 dd0aa2d706 Merge pull request 'rc' (#37) from rc into main 2025-04-10 15:06:45 +03:00
YurijO bcdbd0f0fc Merge pull request 'redesign' (#36) from redesign into rc
test-org/trust-module-frontend/pipeline/pr-main Build succeeded
Reviewed-on: http://192.168.2.61/deployer3000/trust-module-frontend/pulls/36
Reviewed-by: Vladislav Drozdov <ya2@ya.ru>
Reviewed-by: YurijO <ya@ya.ru>
2025-04-10 14:57:52 +03:00
DmitriyA 08eaa274b2 Merge pull request 'Converted time to constants' (#35) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://192.168.2.61/deployer3000/trust-module-frontend/pulls/35
2025-04-10 12:36:47 +03:00
DmitriyA 22c5fcf02c Converted time to constants
test-org/trust-module-frontend/pipeline/pr-redesign Build queued... Details
2025-04-10 05:33:15 -04:00
yuobrezkov 2c9813fbb9 auto versioning added 2025-04-10 11:03:22 +03:00
DmitriyA c1b0f10e62 Merge pull request 'added an environment variable' (#33) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/33
2025-04-09 17:19:13 +03:00
DmitriyA 858f6e2c4c added an environment variable 2025-04-09 10:17:45 -04:00
DmitriyA 4eed2f364c Merge pull request 'added the api prefix' (#31) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/31
2025-04-09 17:03:45 +03:00
DmitriyA 5b25876056 added the api prefix
test-org/trust-module-frontend/pipeline/pr-redesign Build queued... Details
2025-04-09 10:00:44 -04:00
DmitriyA 9ee15160e5 Merge pull request 'optimized chart loading' (#30) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/30
2025-04-09 16:17:23 +03:00
DmitriyA e56b82bb66 optimized chart loading
test-org/trust-module-frontend/pipeline/pr-redesign Build queued... Details
2025-04-09 09:15:25 -04:00
DmitriyA 46484efdea Merge pull request 'debugging' (#29) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/29
2025-04-09 14:57:13 +03:00
DmitriyA b6b3b36f5a fixed data interpolation and range allocation
test-org/trust-module-frontend/pipeline/pr-redesign This commit looks good Details
2025-04-09 07:47:51 -04:00
DmitriyA 46da90fbb6 Fixed the date and time display 2025-04-08 01:42:47 -04:00
DmitriyA 64401cadbc fixed data transmission via a web socket and left data transmission via http requests for historical data 2025-04-07 10:27:08 -04:00
DmitriyA a24b89220c fixed bugs
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-04 15:31:20 -04:00
DmitriyA 32ece2f0ff refactoring, fixed bugs with the web socket
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-02 19:50:08 -04:00
DmitriyA 4405c693aa Established a connection to the back using a web socket
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-01 11:50:00 -04:00
DmitriyA 6a4640ba93 Deleted the trash files, added a button to minimize the side menu, and adjusted the styles
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-03-28 07:24:28 -04:00
DmitriyA bd96278895 redesign and fix graphics
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-03-27 10:09:58 -04:00
DmitriyA ed2e03e202 adjusting the chart legend 2025-03-26 06:53:41 -04:00
38 changed files with 1730 additions and 2169 deletions

40
Jenkinsfile vendored
View File

@ -30,7 +30,23 @@ pipeline {
stage ('Initialize variables') {
steps {
script {
env.IMAGE_TAG = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim()
def hasTags = sh(script: "git tag -l | wc -l", returnStdout: true).trim().toInteger() > 0
echo "${hasTags}"
def lastVersion = "0.0.0"
if (hasTags) {
lastVersion = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim()
}
echo "Last version: ${lastVersion}"
def (major, minor, patch) = lastVersion.tokenize('.')
def newVersion = "${major}.${minor}.${patch.toInteger() + 1}"
echo "New version: ${newVersion}"
env.IMAGE_TAG = newVersion
env.NEW_VERSION = newVersion
}
}
}
@ -71,17 +87,29 @@ pipeline {
echo "Attempting to merge PR ${env.CHANGE_ID} into master..."
withCredentials([usernamePassword(credentialsId: 'gitea_creds', usernameVariable: 'GITEA_USER', passwordVariable: 'GITEA_PASS')]) {
def prId = env.CHANGE_ID
sh """
curl -X POST \
-u "${GITEA_USER}:${GITEA_PASS}" \
-H "Content-Type: application/json" \
-d '{"do":"merge"}' \
http://git.entcor/api/v1/repos/deployer3000/trust-module-frontend/pulls/${prId}/merge
http://git.entcor/api/v1/repos/deployer3000/${env.IMAGE_NAME}/pulls/${prId}/merge
"""
echo "PR ${prId} merged successfully into master!"
def context = "test-org/trust-module-frontend/pipeline/pr-${env.CHANGE_TARGET}"
def commitHash = sh(script: "git rev-parse HEAD~1", returnStdout: true).trim()
notify(context, GITEA_USER, GITEA_PASS, env.GITEA_REPOSITORY_URL, "trust-module-frontend", commitHash, "success")
def commitHash = sh(script: "git rev-parse HEAD~1", returnStdout: true).trim() // необходим для корректного отображения статусов
echo "PR ${prId} merged successfully into main!"
sleep(time: 15, unit: 'SECONDS')
sh "git checkout main && git pull origin main"
sh """
curl -v -X POST -u "${GITEA_USER}:${GITEA_PASS}" \
-H "Content-Type: application/json" \
-d '{"tag_name": "${env.NEW_VERSION}", "name": "Release ${env.NEW_VERSION}", "target_commitish": "main"}' \
"${env.GITEA_REPOSITORY_URL}deployer3000/${env.IMAGE_NAME}/releases"
"""
echo "New release succeeded!"
def context = "test-org/${env.IMAGE_NAME}/pipeline/pr-${env.CHANGE_TARGET}"
notify(context, GITEA_USER, GITEA_PASS, env.GITEA_REPOSITORY_URL, env.IMAGE_NAME, commitHash, "success")
}
}
}

View File

@ -26,7 +26,8 @@
"@mui/icons-material": "^6.4.8",
"reactflow": "^11.11.4",
"vite-plugin-svgr": "^4.3.0",
"react-scripts": "^5.0.1"
"react-scripts": "^5.0.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",

View File

@ -0,0 +1,20 @@
import React from 'react';
export const ConnectionStatusIndicator = ({ connectionStatus }) => {
return (
<div style={{
position: 'absolute',
top: '10px',
right: '10px',
padding: '5px 10px',
borderRadius: '4px',
backgroundColor: connectionStatus === 'connected' ? '#4CAF50' :
connectionStatus === 'error' ? '#F44336' : '#FFC107',
color: 'white',
fontSize: '12px'
}}>
{connectionStatus === 'connected' ? 'Online' :
connectionStatus === 'error' ? 'Connection Error' : 'Offline'}
</div>
);
};

View File

@ -0,0 +1,17 @@
import React from 'react';
export const CurrentRangeDisplay = ({ useCustomRange, startDate, endDate, selectedRange }) => {
return (
<div style={{
margin: '10px 0',
padding: '8px 12px',
backgroundColor: '#f0f7ff',
borderRadius: '4px',
borderLeft: '3px solid #4a6baf'
}}>
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
</div>
);
};

View File

@ -1,103 +1,229 @@
import React, { useState } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from 'recharts';
import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
import { HOUR, DAY } from './constants';
const TIME_FORMATS = {
LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов
MEDIUM: 'HH:mm', // Для диапазона > 1 часа
SHORT: 'HH:mm:ss' // Для коротких диапазонов
};
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null);
const LineChartComponent = ({
chartData,
metricName,
colors,
onRangeSelect,
filteredData
}) => {
const [selectionArea, setSelectionArea] = useState(null);
const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null);
const allTimes = Object.values(chartData)
const allTimestamps = Object.values(chartData)
.flat()
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index);
.map(point => point.timestamp)
.filter((timestamp, index, self) => self.indexOf(timestamp) === index)
.sort((a, b) => a - b);
const data = allTimestamps.map(timestamp => {
const point = { timestamp };
const firstPoint = Object.values(chartData)
.flat()
.find(p => p.timestamp === timestamp);
if (firstPoint) {
point.time = firstPoint.time;
point.fullTime = firstPoint.fullTime;
}
const data = allTimes.map(time => {
const point = { time };
Object.keys(chartData).forEach(key => {
const instanceData = chartData[key].find(p => p.time === time);
const instanceData = chartData[key].find(p => p.timestamp === timestamp);
point[key] = instanceData ? instanceData.value : null;
});
return point;
});
const displayData = filteredData || data;
const handleClick = (e) => {
if (!e || !e.activeLabel) return;
const instanceKeys = displayData.length
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
: [];
const clickedTime = e.activeLabel;
// Функция для определения оптимального формата времени в зависимости от диапазона
const getTimeFormat = () => {
if (!data.length) return TIME_FORMATS.SHORT;
if (!selectionStart) {
setSelectionStart(clickedTime);
} else if (!selectionEnd) {
setSelectionEnd(clickedTime);
const range = data[data.length - 1].timestamp - data[0].timestamp;
const startIndex = data.findIndex(point => point.time === selectionStart);
const endIndex = data.findIndex(point => point.time === clickedTime);
onRangeSelect({ startIndex, endIndex });
setSelectionStart(null);
setSelectionEnd(null);
}
if (range > DAY) return TIME_FORMATS.LONG;
if (range > HOUR) return TIME_FORMATS.MEDIUM;
return TIME_FORMATS.SHORT;
};
useEffect(() => {
const handleSelectStart = (e) => {
if (isSelecting) {
e.preventDefault();
}
};
document.addEventListener('selectstart', handleSelectStart);
return () => document.removeEventListener('selectstart', handleSelectStart);
}, [isSelecting]);
const handleMouseDown = (e) => {
if (!e) return;
// Получаем индекс точки по координатам
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
setIsSelecting(true);
setSelectionArea({
start: data[activeIndex].timestamp,
end: null,
startIndex: activeIndex,
endIndex: null
});
};
const handleMouseMove = (e) => {
if (!isSelecting || !selectionArea?.start || !e) return;
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
setSelectionArea(prev => ({
...prev,
end: data[activeIndex].timestamp,
endIndex: activeIndex
}));
};
const handleMouseUp = () => {
if (!isSelecting || !selectionArea?.start || !selectionArea?.end) {
setIsSelecting(false);
setSelectionArea(null);
return;
}
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
// Нормализуем индексы к диапазону [0, 1] для родительского компонента
const normalizedStart = startIndex / (data.length - 1);
const normalizedEnd = endIndex / (data.length - 1);
onRangeSelect({
startIndex: normalizedStart,
endIndex: normalizedEnd
});
setIsSelecting(false);
setSelectionArea(null);
};
// Упрощенный Tooltip без указания instance
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
const currentPoint = data.find(point => point.timestamp === label);
return (
<div className="custom-tooltip" style={{
<div style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc',
borderRadius: '4px'
borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`Время: ${label}`}</p>
<p>{`Значение: ${payload[0].value}`}</p>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>
{currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')}
</p>
{payload.map((item, index) => (
<p key={index} style={{ color: item.color }}>
{`Значение: ${item.value}`}
</p>
))}
</div>
);
}
return null;
};
if (!data.length) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
}
return (
<div>
<ResponsiveContainer width="100%" height={400}>
<div style={{ position: 'relative', height: '400px' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={displayData}
onClick={handleClick}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
ref={chartRef}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="time"
tick={{ fill: '#666' }}
tickMargin={10}
dataKey="timestamp"
height={75}
tick={{ fontSize: 12, angle: -45, textAnchor: 'end' }}
interval={Math.max(1, Math.floor(data.length / 10))}
tickFormatter={(timestamp) => {
const date = new Date(timestamp);
const format = getTimeFormat();
if (format === 'dd.MM HH:mm') {
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} else if (format === 'HH:mm') {
return date.toLocaleString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
} else {
return date.toLocaleString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
}}
/>
<YAxis
tick={{ fill: '#666' }}
tickMargin={10}
/>
<Tooltip
content={<CustomTooltip />}
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
/>
{/* Убрали <Legend /> чтобы скрыть имена instance */}
{Object.keys(chartData).map((key, index) => (
<YAxis tick={{ fontSize: 12 }} />
<Tooltip content={<CustomTooltip />} />
{instanceKeys.map((instance, index) => (
<Line
key={key}
key={instance}
type="monotone"
dataKey={key}
dataKey={instance}
name={instance}
stroke={colors[index % colors.length]}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
// Убрали name чтобы не отображалось в tooltip
/>
))}
{selectionArea?.start && selectionArea?.end && (
<ReferenceArea
x1={selectionArea.start}
x2={selectionArea.end}
strokeOpacity={0.3}
fill="#4a6baf"
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default LineChartComponent;
export default React.memo(LineChartComponent);

View File

@ -0,0 +1,151 @@
import React from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { TIME_RANGES } from './constants';
export const TimeRangeSelector = ({
selectedRange,
handleRangeChange,
startDate,
setStartDate,
endDate,
setEndDate,
useCustomRange,
handleCustomRangeChange,
handleResetZoom
}) => {
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '15px',
alignItems: 'center',
marginBottom: '15px'
}}>
{/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px', display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
<div style={{ flex: '1' }}>
<label htmlFor="time-range" style={{
display: 'block',
marginBottom: '5px',
fontWeight: '500',
color: '#555'
}}>Стандартные диапазоны:</label>
<select
id="time-range"
value={selectedRange.value}
onChange={handleRangeChange}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd',
color: "#333",
backgroundColor: '#f9f9f9'
}}
>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
<button
onClick={handleResetZoom}
style={{
padding: '8px 16px',
backgroundColor: '#f0f0f0',
color: '#333',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s',
height: '36px',
whiteSpace: 'nowrap'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#e0e0e0'}
onMouseOut={(e) => e.target.style.backgroundColor = '#f0f0f0'}
>
Сбросить
</button>
</div>
{/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}>
<div style={{
marginBottom: '10px',
fontWeight: '500',
color: '#555'
}}>
Или укажите свой диапазон:
</div>
<div style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap'
}}>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Начальная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Конечная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<button
onClick={handleCustomRangeChange}
style={{
padding: '8px 16px',
backgroundColor: '#4a6baf',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
flex: '0 0 auto',
alignSelf: 'flex-end'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
>
Применить
</button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,35 @@
export 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 },
];
export const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850'];
export const MAX_POINTS = 20;
// Для работы с временными интервалами (setTimeout и т.д.)
export const MS = 1;
export const SECOND_MS = 1000 * MS;
export const MINUTE_MS = 60 * SECOND_MS;
export const HOUR_MS = 60 * MINUTE_MS;
export const DAY_MS = 24 * HOUR_MS;
// Для работы с Unix-временем и API (Prometheus и т.д.)
export const SECOND = 1;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;

View File

@ -1,27 +0,0 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
const SystemStatusChart = ({ data }) => {
// Обрезаем массив, оставляя только последние 20 точек
const trimmedData = data.slice(-20);
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={trimmedData} // Используем обрезанный массив
margin={{
top: 5, right: 30, left: 20, bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>
</ResponsiveContainer>
);
};
export default SystemStatusChart;

View File

@ -1,369 +1,494 @@
import React, { useEffect, useState, useRef } from 'react';
import axios from 'axios';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { io } from 'socket.io-client';
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 },
];
import { TimeRangeSelector } from './Components/TimeRangeSelector';
import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator';
import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay';
import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants';
import axios from 'axios';
const PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState({});
const [chartData, setChartData] = useState(null);
const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]);
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [useCustomRange, setUseCustomRange] = useState(false);
const [selectedGraphRange, setSelectedGraphRange] = useState(null); // Выбранный диапазон
const [filteredData, setFilteredData] = useState(null); // Отфильтрованные данные
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const [selectedGraphRange, setSelectedGraphRange] = useState(null);
const [filteredData, setFilteredData] = useState(null);
const [isSelectingRange, setIsSelectingRange] = useState(false);
const [lastCustomRange, setLastCustomRange] = useState(null);
const intervalRef = useRef(null);
const socketRef = useRef(null);
const debounceRef = useRef(null);
// Функция для интерполяции данных
const interpolateData = (data, minPoints = 15) => {
if (data.length >= minPoints) return data;
const formatTime = useCallback((timestamp, rangeSeconds) => {
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
const date = new Date(ts);
const interpolatedData = [];
for (let i = 0; i < data.length - 1; i++) {
interpolatedData.push(data[i]);
// Определяем формат в зависимости от диапазона
const showFullDate = rangeSeconds > 86400; // больше суток
const currentPoint = data[i];
const nextPoint = data[i + 1];
const timeOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
};
// Вычисляем разницу во времени между точками
const currentTime = new Date(currentPoint.time).getTime();
const nextTime = new Date(nextPoint.time).getTime();
const timeDiff = nextTime - currentTime;
const dateOptions = showFullDate ? {
month: '2-digit',
day: '2-digit',
...timeOptions
} : timeOptions;
// Добавляем промежуточные точки
const steps = Math.ceil((minPoints - data.length) / (data.length - 1));
for (let j = 1; j <= steps; j++) {
const interpolatedTime = new Date(currentTime + (timeDiff * j) / (steps + 1)).toLocaleString();
const interpolatedPoint = { time: interpolatedTime };
return {
display: date.toLocaleString('ru-RU', dateOptions),
fullDisplay: date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}),
timestamp: ts
};
}, []);
// Интерполируем значения для каждой метрики
Object.keys(currentPoint).forEach(key => {
if (key !== 'time') {
const currentValue = currentPoint[key];
const nextValue = nextPoint[key];
interpolatedPoint[key] = currentValue + ((nextValue - currentValue) * j) / (steps + 1);
}
});
const calculateStep = useCallback((start, end) => {
const range = end - start;
if (range <= MINUTE) return 1; // 1 мин
if (range <= MINUTE * 5) return 5; // 5 мин
if (range <= HOUR / 2) return 15; // 30 мин
if (range <= HOUR) return 30; // 1 час
if (range <= HOUR * 3) return 60; // 3 часа
if (range <= HOUR * 6) return 120; // 6 часов
if (range <= DAY / 2) return 300; // 12 часов
if (range <= DAY) return 600; // 24 часа
return 1800; // > 24 часов
}, []);
interpolatedData.push(interpolatedPoint);
}
const fetchData = useCallback(() => {
if (isSelectingRange) return;
const now = Math.floor(Date.now() / 1000);
const start = now - selectedRange.value;
const end = now;
const step = calculateStep(start, end);
if (socketRef.current?.connected) {
socketRef.current.emit('get-metrics', {
metric: metricName,
start,
end,
step,
_t: Date.now()
});
}
}, [metricName, selectedRange.value, isSelectingRange]);
interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку
return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints
};
const processMetricsData = useCallback((response) => {
console.log('Processing metrics data:', response);
if (response.metric !== metricName) return;
const dataArray = Array.isArray(response.data) ? response.data : [response.data];
if (!dataArray.length) return;
setChartData(prev => {
const newData = { ...(prev || {}) };
const rangeSeconds = useCustomRange
? (endDate.getTime() - startDate.getTime()) / 1000
: selectedRange.value;
dataArray.forEach(item => {
const instance = item.instance || 'default';
if (!newData[instance]) newData[instance] = [];
// Унифицированная конвертация timestamp
let timestamp;
if (typeof item.timestamp === 'number') {
// Определяем, в секундах или миллисекундах пришел timestamp
timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
} else {
timestamp = Date.now();
}
const value = parseFloat(item.value);
const formattedTime = formatTime(timestamp, rangeSeconds);
newData[instance].push({
time: formattedTime.display,
fullTime: formattedTime.fullDisplay,
value: value,
timestamp: timestamp
});
});
// Сортируем и ограничиваем данные
Object.keys(newData).forEach(instance => {
newData[instance] = newData[instance]
.sort((a, b) => a.timestamp - b.timestamp)
.slice(-1000);
});
return newData;
});
}, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
const setupWebSocket = useCallback(() => {
if (socketRef.current) {
// Если соединение уже существует, возвращаем его
if (socketRef.current.connected) return socketRef.current;
// Если соединение в процессе переподключения, тоже возвращаем
if (socketRef.current.reconnecting) return socketRef.current;
}
//VITE_BACK_WS_URL
const socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
socketRef.current = socket;
socket.on('connect', () => {
console.log('WebSocket connected');
setConnectionStatus('connected');
fetchData();
});
socket.on('disconnect', (reason) => {
console.log('WebSocket disconnected:', reason);
setConnectionStatus('disconnected');
if (reason === 'io server disconnect') socket.connect();
});
socket.on('connect_error', (error) => {
console.error('WebSocket connection error:', error);
setConnectionStatus('error');
setTimeout(() => socket.connect(), 1000);
});
socket.on('metrics-data', (response) => {
console.log('Received raw metrics data:', response);
processMetricsData(response);
});
socket.on('metrics-error', (error) => {
console.error('Metrics error:', error);
setConnectionStatus('error');
});
return socket;
}, []);
const fetchCustomRangeData = useCallback(async () => {
const start = Math.floor(startDate.getTime() / 1000);
const end = Math.floor(endDate.getTime() / 1000);
const rangeSeconds = end - start;
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;
else if (range <= 21600) step = 30;
else if (range <= 86400) step = 120;
else step = 300;
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, {
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
params: {
metric: metricName,
start,
end,
step
step: calculateStep(start, end)
}
});
const result = response.data;
let metrics = Array.isArray(result) ? result : result.data || [];
if (!Array.isArray(metrics)) {
metrics = [];
}
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);
}
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;
if (!updatedData[key]) updatedData[key] = {};
updatedData[key][formattedTime] = m.value;
});
const chartData = {};
Object.keys(updatedData).forEach(key => {
chartData[key] = timePoints.map(time => ({
time,
value: updatedData[key][time] ?? null,
if (response.data?.length) {
// Преобразуем данные перед передачей в processMetricsData
const processedData = response.data.map(item => ({
...item,
timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует
value: item.value.toString()
}));
processMetricsData({
metric: metricName,
data: processedData
});
}
} catch (error) {
console.error('Ошибка при получении кастомных данных:', error);
}
}, [metricName, startDate, endDate, calculateStep, processMetricsData]);
const handleRangeChange = useCallback((event) => {
// Очищаем текущий интервал
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
const selectedValue = event.target.value;
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
setSelectedRange(range);
setUseCustomRange(false);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
const now = new Date();
setEndDate(now);
setStartDate(new Date(now.getTime() - range.value * 1000));
// Переподключение сокета
if (!socketRef.current?.connected) {
socketRef.current?.connect();
}
}, []);
const handleCustomRangeChange = useCallback(() => {
// Отключаем WebSocket соединение
if (socketRef.current?.connected) {
socketRef.current.disconnect();
setConnectionStatus('disconnected');
}
// Очищаем интервал обновления
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setUseCustomRange(true);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
fetchCustomRangeData();
}, [fetchCustomRangeData]);
const handleResetZoom = useCallback(() => {
setSelectedGraphRange(null);
setFilteredData(null);
setIsSelectingRange(false);
if (useCustomRange) {
fetchCustomRangeData();
} else {
if (!socketRef.current?.connected) {
socketRef.current?.connect();
}
fetchData();
}
if (lastCustomRange) {
handleRangeSelect(lastCustomRange);
return;
}
}, [fetchData, fetchCustomRangeData, useCustomRange]);
const interpolateData = useCallback((data, targetPointCount) => {
if (!data || data.length < 2) return data;
if (data.length >= targetPointCount) return data;
const interpolated = [];
const step = (data.length - 1) / (targetPointCount - 1);
for (let i = 0; i < targetPointCount; i++) {
const index = i * step;
const lowerIndex = Math.floor(index);
const upperIndex = Math.ceil(index);
if (lowerIndex === upperIndex) {
interpolated.push(data[lowerIndex]);
continue;
}
const fraction = index - lowerIndex;
const interpolatedPoint = {};
Object.keys(data[lowerIndex]).forEach(key => {
if (key === 'timestamp') {
interpolatedPoint[key] = data[lowerIndex][key] +
fraction * (data[upperIndex][key] - data[lowerIndex][key]);
// Добавляем отображаемое время
const { display, fullDisplay } = formatTime(interpolatedPoint[key],
(endDate - startDate) / 1000);
interpolatedPoint.time = display;
interpolatedPoint.fullTime = fullDisplay;
} else if (typeof data[lowerIndex][key] === 'number') {
interpolatedPoint[key] = data[lowerIndex][key] +
fraction * (data[upperIndex][key] - data[lowerIndex][key]);
} else {
interpolatedPoint[key] = data[lowerIndex][key];
}
});
setChartData(chartData);
} catch (error) {
console.error('Ошибка при загрузке метрик:', error);
interpolated.push(interpolatedPoint);
}
};
return interpolated;
}, []);
const handleRangeSelect = useCallback((range) => {
setLastCustomRange(range);
if (!range || !chartData) return;
setIsSelectingRange(true);
setSelectedGraphRange(range);
// Отключаем автоматические обновления
if (socketRef.current?.connected) {
socketRef.current.disconnect();
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Получаем все точки и сортируем по времени
const allPoints = Object.values(chartData).flat();
const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
// Вычисляем абсолютные индексы
const startIndex = Math.floor(range.startIndex * (sortedPoints.length - 1));
const endIndex = Math.floor(range.endIndex * (sortedPoints.length - 1));
// Фильтруем точки по выбранному диапазону
const filtered = sortedPoints.slice(startIndex, endIndex + 1);
// Применяем интерполяцию только если точек меньше 100
const interpolated = filtered.length < 100 ?
interpolateData(filtered, Math.min(100, filtered.length * 3)) :
filtered;
setFilteredData(interpolated);
setIsSelectingRange(false);
}, [chartData, interpolateData, formatTime]);
useEffect(() => {
fetchData();
const socket = setupWebSocket();
return () => {
clearInterval(intervalRef.current);
socket.disconnect();
};
}, [setupWebSocket]);
intervalRef.current = setInterval(() => {
fetchData();
}, selectedRange.interval);
// Обновим useEffect для кастомного диапазона
useEffect(() => {
if (useCustomRange && !isSelectingRange) {
// Очищаем предыдущий таймер
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// Устанавливаем новый таймер с задержкой 500 мс
debounceRef.current = setTimeout(() => {
fetchCustomRangeData();
}, 500);
}
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, [useCustomRange, isSelectingRange, startDate, endDate, fetchCustomRangeData]);
useEffect(() => {
if (useCustomRange || isSelectingRange) return;
const fetchDataWrapper = () => {
try {
fetchData();
} catch (error) {
console.error('Error in interval fetch:', error);
}
};
// Очищаем предыдущий интервал
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// Запускаем сразу и затем по интервалу
fetchDataWrapper();
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [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);
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные
};
const handleCustomRangeChange = () => {
setUseCustomRange(true);
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные
};
}, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]);
useEffect(() => {
if (selectedGraphRange) {
const { startIndex, endIndex } = selectedGraphRange;
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;
});
const filtered = data.slice(startIndex, endIndex + 1);
// Интерполируем данные, если точек меньше 15
const interpolated = interpolateData(filtered, 15);
setFilteredData(interpolated); // Сохраняем интерполированные данные
} else {
setFilteredData(null); // Сбрасываем фильтрацию, если диапазон не выбран
if (!selectedGraphRange || !chartData) {
setFilteredData(null);
return;
}
}, [selectedGraphRange, chartData]);
if (!Object.keys(chartData).length) return <p>Loading...</p>;
const allPoints = Object.values(chartData).flat();
const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
const allTimes = Object.values(chartData)
.flat()
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index);
const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1));
const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1));
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;
});
const filtered = sortedPoints.slice(startIndex, endIndex + 1);
const interpolated = filtered.length > 100 ?
interpolateData(filtered, 100) :
filtered;
setFilteredData(interpolated);
}, [selectedGraphRange, chartData, interpolateData]);
if (chartData === null) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Loading data...</div>;
}
if (Object.keys(chartData).length === 0) {
return <div style={{ padding: '20px', textAlign: 'center' }}>No data available</div>;
}
return (
<div style={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px'
marginBottom: '20px',
position: 'relative'
}}>
{/* Заголовок графика */}
<h3 style={{ marginTop: 0, color: '#333' }}>
</h3>
<ConnectionStatusIndicator connectionStatus={connectionStatus} />
{/* Группа элементов управления */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '15px',
alignItems: 'center',
marginBottom: '15px'
}}>
{/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px' }}>
<label htmlFor="time-range" style={{
display: 'block',
marginBottom: '5px',
fontWeight: '500',
color: '#555'
}}>Стандартные диапазоны:</label>
<select
id="time-range"
value={selectedRange.value}
onChange={handleRangeChange}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd',
color: "#333",
backgroundColor: '#f9f9f9'
}}
>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
<TimeRangeSelector
selectedRange={selectedRange}
handleRangeChange={handleRangeChange}
startDate={startDate}
setStartDate={setStartDate}
endDate={endDate}
setEndDate={setEndDate}
useCustomRange={useCustomRange}
handleCustomRangeChange={handleCustomRangeChange}
handleResetZoom={handleResetZoom}
/>
{/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}>
<div style={{
marginBottom: '10px',
fontWeight: '500',
color: '#555'
}}>
Или укажите свой диапазон:
</div>
<div style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap'
}}>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Начальная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Конечная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<button
onClick={handleCustomRangeChange}
style={{
padding: '8px 16px',
backgroundColor: '#4a6baf',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
flex: '0 0 auto',
alignSelf: 'flex-end'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
>
Применить
</button>
</div>
</div>
</div>
<CurrentRangeDisplay
useCustomRange={useCustomRange}
startDate={startDate}
endDate={endDate}
selectedRange={selectedRange}
/>
{/* Индикатор текущего диапазона */}
<div style={{
margin: '10px 0',
padding: '8px 12px',
backgroundColor: '#f0f7ff',
borderRadius: '4px',
borderLeft: '3px solid #4a6baf'
}}>
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
</div>
{/* График */}
<LineChartComponent
chartData={chartData}
metricName={metricName}
colors={COLORS}
description={metricName}
onRangeSelect={setSelectedGraphRange}
onRangeSelect={handleRangeSelect}
filteredData={filteredData}
/>
</div>
);
};
export default PrometheusChart;
export default React.memo(PrometheusChart);

View File

@ -5,6 +5,22 @@ const SystemStatusChart = ({ data }) => {
// Обрезаем массив, оставляя только последние 20 точек
const trimmedData = data.slice(-20);
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="custom-tooltip" style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc'
}}>
<p>{`Время: ${label}`}</p>
<p>{`Значение: ${payload[0].value}`}</p>
</div>
);
}
return null;
};
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart
@ -16,8 +32,7 @@ const SystemStatusChart = ({ data }) => {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Legend />
<Tooltip content={<CustomTooltip />} />
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>
</ResponsiveContainer>

View File

@ -1,84 +0,0 @@
import React, { useState, useEffect } from "react";
import "../Style/SystemStatusTable.css";
import axios from "axios";
const SystemStatusTable = () => {
const [systemData, setSystemData] = useState([]);
const [expandedRow, setExpandedRow] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Загрузка данных с бэкенда
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get("/trust.json"); // Укажите ваш endpoint
setSystemData(response.data);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
fetchData();
}, []);
// Обработчик для кнопки "Подробнее"
const handleDetailsClick = (id) => {
setExpandedRow(expandedRow === id ? null : id);
};
if (loading) {
return <p>Загрузка данных...</p>;
}
if (error) {
return <p>Ошибка: {error}</p>;
}
return (
<table>
<caption>
<h2>Состояние системы</h2>
</caption>
<thead>
<tr>
<th>Метрика</th>
<th>Значение</th>
<th>Статус</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
{systemData.map((item) => (
<React.Fragment key={item.id}>
<tr>
<td>{item.name}</td>
<td>{item.value}%</td>
<td>
<span className={`status ${item.status}`}>{item.status}</span>
</td>
<td>
<button onClick={() => handleDetailsClick(item.id)}>
{expandedRow === item.id ? "Скрыть" : "Подробнее"}
</button>
</td>
</tr>
{expandedRow === item.id && (
<tr>
<td colSpan="4">
<div className="details">
<p>{item.details}</p>
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
);
};
export default SystemStatusTable;

View File

@ -1,84 +0,0 @@
import React, { useState, useEffect } from "react";
import "../Style/SystemStatusTable.css";
import axios from "axios";
const SystemStatusTableSoftware = () => {
const [systemData, setSystemData] = useState([]);
const [expandedRow, setExpandedRow] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Загрузка данных с бэкенда
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get("/TrustSoftware.json"); // Укажите ваш endpoint
setSystemData(response.data);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
fetchData();
}, []);
// Обработчик для кнопки "Подробнее"
const handleDetailsClick = (id) => {
setExpandedRow(expandedRow === id ? null : id);
};
if (loading) {
return <p>Загрузка данных...</p>;
}
if (error) {
return <p>Ошибка: {error}</p>;
}
return (
<table>
<caption>
<h2>Состояние ПО</h2>
</caption>
<thead>
<tr>
<th>Метрика</th>
<th>Значение</th>
<th>Статус</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
{systemData.map((item) => (
<React.Fragment key={item.id}>
<tr>
<td>{item.name}</td>
<td>{item.value}%</td>
<td>
<span className={`status ${item.status}`}>{item.status}</span>
</td>
<td>
<button onClick={() => handleDetailsClick(item.id)}>
{expandedRow === item.id ? "Скрыть" : "Подробнее"}
</button>
</td>
</tr>
{expandedRow === item.id && (
<tr>
<td colSpan="4">
<div className="details">
<p>{item.details}</p>
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
);
};
export default SystemStatusTableSoftware;

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import { Box, styled } from "@mui/material";
import SidebarMenu from "./SidebarMenu";
import "../../Style/Dashboard.css";
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
import generateTabContent from "../TreeChart/tabContent";
import CustomTabs from "../UI/MUItabs";
@ -9,6 +9,53 @@ import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent";
import menuData from "../TreeChart/menuData.json";
// Создаем стилизованные компоненты
const DashboardContainer = styled(Box)(({ theme }) => ({
display: 'flex',
height: '100vh',
width: '100vw',
overflow: 'hidden',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
const SidebarWrapper = styled(Box)(({ theme }) => ({
position: 'relative',
backgroundColor: theme.palette.custom.sidebar,
color: theme.palette.custom.sidebarText,
}));
const SidebarResizer = styled(Box)(({ theme }) => ({
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: '4px',
cursor: 'col-resize',
'&:hover': {
backgroundColor: theme.palette.primary.main,
},
}));
const MainContent = styled(Box)(({ theme }) => ({
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(2.5), // 20px
overflow: 'auto',
backgroundColor: theme.palette.background.default,
}));
const Content = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.custom.modalBackground,
padding: theme.spacing(2.5),
borderRadius: '10px',
boxShadow: theme.shadows[2],
maxWidth: '100%',
overflow: 'auto',
color: theme.palette.custom.modalText,
}));
const Dashboard = () => {
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
const { sidebarWidth, startResizing } = useSidebarResize(250);
@ -20,13 +67,11 @@ const Dashboard = () => {
history2: [],
});
// Генерация контента для вкладок
useEffect(() => {
const generatedTabContent = generateTabContent(menuData);
setTabContent(generatedTabContent);
}, []);
// Обновление статусов каждые 30 секунд
useEffect(() => {
const interval = setInterval(() => {
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
@ -56,15 +101,20 @@ const Dashboard = () => {
}, [treeData1, treeData2]);
return (
<div className="dashboard-container">
<DashboardContainer>
{/* Сайдбар */}
<div className="sidebar" style={{ width: sidebarWidth }}>
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} />
<div className="sidebar-resizer" onMouseDown={startResizing} />
</div>
<SidebarWrapper sx={{ width: sidebarWidth }}>
<SidebarMenu
data={treeData1}
onOpenTab={handleOpenTab}
sidebarWidth={sidebarWidth}
startResizing={startResizing}
/>
<SidebarResizer onMouseDown={startResizing} />
</SidebarWrapper>
{/* Основной контент */}
<div className="main-content">
<MainContent>
{/* Вкладки */}
<CustomTabs
tabs={tabs}
@ -74,7 +124,7 @@ const Dashboard = () => {
/>
{/* Контент вкладки */}
<div className="content">
<Content>
<TabContent
activeTab={activeTab}
statusHistories={statusHistories}
@ -82,9 +132,9 @@ const Dashboard = () => {
tabContent={tabContent}
handleOpenTab={handleOpenTab}
/>
</div>
</div>
</div>
</Content>
</MainContent>
</DashboardContainer>
);
};

View File

@ -1,48 +1,150 @@
import React from "react";
import { Drawer, List } from "@mui/material";
import React, { useState } from "react";
import {
Drawer,
List,
Typography,
styled,
IconButton,
Tooltip,
Box
} from "@mui/material";
import {
ChevronLeft,
ChevronRight,
Menu as MenuIcon
} from "@mui/icons-material";
import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
const SidebarResizer = styled('div')(({ theme }) => ({
width: "5px",
cursor: "ew-resize",
backgroundColor: 'transparent',
height: "100%",
position: "absolute",
right: 0,
top: 0,
transition: 'background-color 0.2s',
'&:hover': {
backgroundColor: theme.palette.primary.main,
},
zIndex: 2
}));
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
const [collapsed, setCollapsed] = useState(false);
const [hovered, setHovered] = useState(false);
const handleToggleCollapse = () => {
setCollapsed(!collapsed);
};
const handleSelectItem = (id, title, children) => {
onOpenTab(id, title, children);
};
const drawerWidth = collapsed ? 64 : sidebarWidth;
return (
<Drawer
variant="permanent"
<Box
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
sx={{
width: sidebarWidth,
flexShrink: 0,
"& .MuiDrawer-paper": {
width: sidebarWidth,
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
},
position: 'relative',
width: drawerWidth,
transition: 'width 0.3s ease',
}}
>
<List>
<h2 style={{ padding: "16px", fontWeight: "bold", }}>Меню</h2>
<MenuItem item={data} onSelectItem={handleSelectItem} />
</List>
{/* Ресайзер */}
<div
onMouseDown={startResizing}
style={{
width: "5px",
cursor: "ew-resize",
backgroundColor: "#ccc",
height: "100%",
position: "absolute",
right: 0,
top: 0,
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
backgroundColor: 'custom.sidebar',
color: 'custom.sidebarText',
transition: 'width 0.3s ease',
overflowX: 'hidden',
borderRight: 'none'
},
}}
/>
>
{/* Кнопка сворачивания/разворачивания */}
<Box sx={{
display: 'flex',
justifyContent: 'flex-end',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
backgroundColor: 'custom.sidebar'
}}>
<Tooltip title={collapsed ? "Развернуть меню" : "Свернуть меню"}>
<IconButton
onClick={handleToggleCollapse}
size="small"
sx={{
color: 'custom.sidebarText',
'&:hover': {
backgroundColor: 'custom.sidebarHover',
}
}}
>
{collapsed ? (
hovered ? <ChevronRight /> : <MenuIcon />
) : (
<ChevronLeft />
)}
</IconButton>
</Tooltip>
</Box>
<SidebarFooter sidebarWidth={sidebarWidth} />
</Drawer>
{/* Содержимое меню */}
<Box sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}>
<List sx={{
overflowY: 'auto',
overflowX: 'hidden',
flex: '1 1 auto'
}}>
{!collapsed && (
<Typography
variant="h6"
sx={{
p: 2,
fontWeight: 'bold',
textAlign: 'center'
}}
>
Меню
</Typography>
)}
<MenuItem
item={data}
onSelectItem={handleSelectItem}
collapsed={collapsed}
/>
</List>
{/* Футер */}
{!collapsed && (
<SidebarFooter />
)}
</Box>
{/* Ресайзер */}
{!collapsed && (
<SidebarResizer onMouseDown={startResizing} />
)}
</Drawer>
</Box>
);
};

View File

@ -1,87 +1,81 @@
import React from "react";
import { ListItem, ListItemIcon, ListItemText, Collapse, List } from "@mui/material";
import {
ListItem,
ListItemIcon,
ListItemText,
Collapse,
List,
styled
} from "@mui/material";
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
// Функция для сбора всех потомков
const getAllChildren = (node) => {
let children = [];
if (node.items && node.items.length > 0) {
node.items.forEach((child) => {
children.push(child); // Добавляем текущий элемент
children = children.concat(getAllChildren(child)); // Рекурсивно добавляем потомков
});
}
return children;
};
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
cursor: "pointer",
paddingLeft: theme.spacing(2 + level * 2),
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
'&.Mui-selected': {
backgroundColor: theme.palette.custom.sidebarHover,
},
}));
const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен параметр level для отслеживания уровня вложенности
const IconWrapper = styled('div')(({ theme }) => ({
cursor: "pointer",
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(0.5),
'&:hover': {
backgroundColor: theme.palette.action.selected,
},
}));
const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
const [isOpen, setIsOpen] = React.useState(false);
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
const handleToggle = () => {
const handleToggle = (e) => {
e.stopPropagation();
setIsOpen(!isOpen);
};
const handleOpenTab = (e) => {
e.stopPropagation(); // Останавливаем всплытие события
const allChildren = getAllChildren(item); // Собираем всех потомков
onSelectItem(item.id, item.title, allChildren); // Передаем данные в родительский компонент
e.stopPropagation();
const allChildren = getAllChildren(item);
onSelectItem(item.id, item.title, allChildren);
};
return (
<>
<ListItem
<StyledListItem
component="div"
onClick={hasChildren ? handleToggle : handleOpenTab}
level={level}
sx={{
cursor: "pointer", // Курсор pointer везде
pl: 2 + level * 2, // Сдвиг в зависимости от уровня вложенности
"&:hover": {
backgroundColor: "#f5f5f5", // Подсветка при наведении на весь элемент
},
pl: collapsed ? 2 : 2 + level * 2, // Адаптируем отступы
justifyContent: collapsed ? 'center' : 'flex-start',
}}
>
<ListItemIcon>
{hasChildren ? (
<div
onClick={handleOpenTab}
style={{
cursor: "pointer",
borderRadius: "4px", // Скругление углов
padding: "4px", // Отступы для увеличения области hover
"&:hover": {
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
// transform: 2,
},
}}
>
{isOpen ? <FolderOpen /> : <Folder />}
</div>
) : (
<div
onClick={handleOpenTab}
style={{
cursor: "pointer",
borderRadius: "4px", // Скругление углов
padding: "4px", // Отступы для увеличения области hover
"&:hover": {
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
},
}}
>
{/* Здесь можно добавить другую иконку или оставить пустым */}
</div>
)}
<ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
<IconWrapper onClick={handleOpenTab}>
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
</IconWrapper>
</ListItemIcon>
<ListItemText
primary={item.title}
sx={{ cursor: "pointer" }} // Курсор pointer для текста
/>
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
</ListItem>
{hasChildren && (
{!collapsed && ( // Показываем текст только в развернутом состоянии
<>
<ListItemText
primary={item.title}
primaryTypographyProps={{
color: 'custom.sidebarText'
}}
/>
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
</>
)}
</StyledListItem>
{hasChildren && !collapsed && ( // Показываем детей только в развернутом состоянии
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.items.map((child, index) => (
@ -89,7 +83,8 @@ const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен п
key={index}
item={child}
onSelectItem={onSelectItem}
level={level + 1} // Увеличиваем уровень вложенности
level={level + 1}
collapsed={collapsed}
/>
))}
</List>
@ -99,4 +94,16 @@ const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен п
);
};
// Вспомогательная функция (остается без изменений)
const getAllChildren = (node) => {
let children = [];
if (node.items && node.items.length > 0) {
node.items.forEach((child) => {
children.push(child);
children = children.concat(getAllChildren(child));
});
}
return children;
};
export default MenuItem;

View File

@ -1,16 +1,47 @@
import React from "react";
import { List, ListItem, ListItemText } from "@mui/material";
import {
List,
ListItem,
ListItemText,
styled
} from "@mui/material";
const SidebarFooter = ({ sidebarWidth }) => {
const FooterList = styled(List)(({ theme }) => ({
backgroundColor: theme.palette.custom.sidebar,
padding: theme.spacing(1, 0),
borderTop: `1px solid ${theme.palette.divider}`,
marginTop: 'auto'
}));
const FooterListItem = styled(ListItem)(({ theme }) => ({
'&:hover': {
backgroundColor: theme.palette.custom.sidebarHover,
},
padding: theme.spacing(1, 2),
}));
const SidebarFooter = () => {
return (
<List sx={{ marginTop: "auto", backgroundColor: "#ffffff", padding: "10px 0" }}>
<ListItem button={true}>
<ListItemText primary="Помощь" sx={{ color: "#000000" }} />
</ListItem>
<ListItem button={true}>
<ListItemText primary="Настройка" sx={{ color: "#000000" }} />
</ListItem>
</List>
<FooterList>
<FooterListItem button>
<ListItemText
primary="Помощь"
primaryTypographyProps={{
color: 'custom.sidebarText',
variant: 'body2'
}}
/>
</FooterListItem>
<FooterListItem button>
<ListItemText
primary="Настройка"
primaryTypographyProps={{
color: 'custom.sidebarText',
variant: 'body2'
}}
/>
</FooterListItem>
</FooterList>
);
};

View File

@ -1,714 +0,0 @@
{
"title": "Сервис ЗВКС",
"id": "1",
"items": [
{
"title": "Функциональные задачи",
"id": "functional_tasks",
"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": "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": "280",
"title": "Сетевой адаптер №1 (port$261) Eth_1",
"items": [
{
"id": "207",
"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": "281",
"title": "Сетевой адаптер №2 (port$262) Eth_2",
"items": [
{
"id": "224",
"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": "282",
"title": "Сетевой адаптер №3 (port$263) Eth_3",
"items": [
{
"id": "241",
"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": "283",
"title": "Сетевой адаптер №4 (port$264) Eth_4",
"items": [
{
"id": "258",
"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": "Медиа сервер",
"id": "media_server_1",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_1",
"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_1",
"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_2",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_2",
"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_2",
"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_3",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_3",
"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_3",
"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_4",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_4",
"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_4",
"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_5",
"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_5",
"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_6",
"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_6",
"items": [
{
"id": "control_software_1",
"title": "ПО"
},
{
"id": "control_software_2",
"title": "ПО"
},
{
"id": "control_software_3",
"title": "ПО"
},
{
"id": "control_software_4",
"title": "ПО"
}
]
}
]
}
]
}

View File

@ -1,14 +1,13 @@
import React, { lazy, Suspense } from "react";
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
// Функция для генерации названия метрики на основе id
const getMetricName = (id) => {
return `zvks_apiforsnmp_measure_${id}`;
};
//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
// Функция для рекурсивного сбора всех id потомков
const getAllChildIds = (node) => {
let ids = [];
@ -37,6 +36,7 @@ const tabContent = (data) => {
const content = (
<div>
<h2>{node.title}</h2>
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
<p>Контент для {node.title}.</p>
{childrenContent}
</div>

View File

@ -0,0 +1,34 @@
import React from 'react';
import Button from '@mui/material/Button';
import { styled } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
const StyledButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(1),
// Дополнительные стили
}));
const CustomButton = ({
children,
variant = 'contained',
color = 'primary',
loading = false,
startIcon,
endIcon,
...props
}) => {
return (
<StyledButton
variant={variant}
color={color}
startIcon={startIcon && !loading ? startIcon : undefined}
endIcon={endIcon && !loading ? endIcon : undefined}
disabled={loading}
{...props}
>
{loading ? <CircularProgress size={24} /> : children}
</StyledButton>
);
};
export default CustomButton;

View File

@ -1,24 +0,0 @@
import React from "react";
import criticalIcon from "../../assets/images/critical.png"; // Красный треугольник
import warningIcon from "../../assets/images/warning.png"; // Желтый треугольник
import "../../Style/ErrorIndicator.css"; // Подключаем стили
const ErrorIndicator = ({ criticalCount, warningCount }) => {
return (
<div className="error-indicator">
{/* Красный индикатор (критические ошибки) */}
<div className="error-item critical">
<img src={criticalIcon} alt="Критическая ошибка" />
<span>{criticalCount}</span>
</div>
{/* Желтый индикатор (предупреждения) */}
<div className="error-item warning">
<img src={warningIcon} alt="Предупреждение" />
<span>{warningCount}</span>
</div>
</div>
);
};
export default ErrorIndicator;

View File

@ -1,30 +0,0 @@
import React, { useState } from "react";
import "../Style/Expandable.css"
const ExpandableInfo = ({ details }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
<div className="expandable-info">
<button onClick={toggleExpand} className="expand-button">
{isExpanded ? "Скрыть" : "Подробнее"}
</button>
{isExpanded && (
<div className="details-menu">
{details.map((detail, index) => (
<div key={index} className="detail-item">
<span className="label">{detail.label}:</span>
<span className="value">{detail.value}</span>
</div>
))}
</div>
)}
</div>
);
};
export default ExpandableInfo;

View File

@ -17,7 +17,7 @@ const LoginModal = ({ onLogin, onClose }) => {
try {
// Отправляем данные на бэкенд
const response = await fetch(`${import.meta.env.VITE_BACK_URL}/auth/login`, {
const response = await fetch(`${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -1,10 +1,22 @@
import React from "react";
import { Tabs, Tab, Box } from "@mui/material";
import { Tabs, Tab, Box, styled } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
const StyledTab = styled(Tab)(({ theme }) => ({
minHeight: 48,
'&.Mui-selected': {
color: theme.palette.primary.main,
fontWeight: theme.typography.fontWeightMedium,
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: '-2px',
},
}));
const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
const handleMouseDown = (e, id) => {
if (e.button === 1) {
if (e.button === 1) { // Middle mouse button
e.preventDefault();
onCloseTab(id);
}
@ -15,7 +27,13 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
};
return (
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Box sx={{
borderBottom: 1,
borderColor: 'divider',
'& .MuiTabs-indicator': {
backgroundColor: 'primary.main',
}
}}>
<Tabs
value={activeTab}
onChange={handleChange}
@ -23,28 +41,34 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
scrollButtons="auto"
aria-label="tabs"
>
{/* Всегда отображаемые вкладки */}
<Tab
{/* Статические вкладки */}
<StyledTab
label="Главная"
value="Главная"
onMouseDown={(e) => handleMouseDown(e, "Главная")}
/>
<Tab
<StyledTab
label="Визуализация"
value="Визуализация"
onMouseDown={(e) => handleMouseDown(e, "Визуализация")}
/>
{/* Динамически добавляемые вкладки */}
{/* Динамические вкладки */}
{tabs.map((tab) => (
<Tab
<StyledTab
key={tab.id}
label={
<Box sx={{ display: "flex", alignItems: "center" }}>
<span>{tab.title}</span>
<CloseIcon
fontSize="small"
sx={{ ml: 1, cursor: "pointer" }}
sx={{
ml: 1,
cursor: "pointer",
'&:hover': {
color: 'error.main'
}
}}
onClick={(e) => {
e.stopPropagation();
onCloseTab(tab.id);

View File

@ -1,56 +0,0 @@
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;

View File

@ -1,12 +1,27 @@
import React, { useEffect, useRef, useState } from "react";
import "../../Style/TreeTable.css";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Collapse,
Box,
Typography,
useTheme,
Tooltip
} from '@mui/material';
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
const TreeTable = ({ data }) => {
const theme = useTheme();
const tableRef = useRef(null);
const [fontSize, setFontSize] = useState(16);
const [log, setLog] = useState([]);
const [isLogVisible, setIsLogVisible] = useState(true);
const [isLogVisible, setIsLogVisible] = useState(false);
const adjustFontSize = () => {
if (tableRef.current) {
@ -27,6 +42,13 @@ const TreeTable = ({ data }) => {
}
};
useEffect(() => {
adjustFontSize();
window.addEventListener('resize', adjustFontSize);
return () => window.removeEventListener('resize', adjustFontSize);
}, [data]);
// Логирование статусов
useEffect(() => {
const newLog = [];
const traverse = (items) => {
@ -35,7 +57,7 @@ const TreeTable = ({ data }) => {
newLog.push({
title: item.title,
status: item.status,
time: new Date().toLocaleTimeString(), // Добавляем время
time: new Date().toLocaleTimeString(),
});
}
if (item.items) {
@ -44,204 +66,285 @@ const TreeTable = ({ data }) => {
});
};
traverse(data.items);
// Ограничиваем количество сообщений до 50
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
setLog(prevLog => [...newLog, ...prevLog].slice(0, 50));
}, [data]);
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи");
const filteredData = data.items.filter(item => item.title !== "Функциональные задачи");
// Функция для отображения заголовков
const renderHeaders = (items) => {
// Компонент индикаторов статуса
const StatusIndicators = ({ status }) => (
<>
<Box
sx={{
width: '4px',
height: '20px',
display: 'inline-block',
backgroundColor: statusManager1.getStatusColor(status),
marginRight: '4px',
verticalAlign: 'middle'
}}
/>
<Box
sx={{
width: '4px',
height: '20px',
display: 'inline-block',
backgroundColor: statusManager2.getStatusColor(status),
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
</>
);
// Ячейка с тултипом
const TableCellWithTooltip = ({ children, title, ...props }) => (
<Tooltip title={title} arrow>
<TableCell {...props}>
{children}
</TableCell>
</Tooltip>
);
// Рендер заголовков (первый уровень)
const renderMainHeaders = (items) => {
return items.map((item) => {
const colSpan = item.items ? item.items.length : 1;
return (
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}>
<div className="header-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
}}
/>
<TableCellWithTooltip
key={item.id}
colSpan={colSpan}
align="center"
title={item.title}
sx={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="subtitle2" noWrap>
{item.title}
</div>
</th>
</Typography>
</TableCellWithTooltip>
);
});
};
// Функция для отображения подзаголовков
// Рендер подзаголовков (второй уровень)
const renderSubHeaders = (items) => {
return items.map((item) => {
return items.flatMap((item) => {
if (item.items) {
return item.items.map((child) => (
<th key={child.id} className="tree-table-header" title={child.title}>
<div className="header-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(child.status),
marginLeft: "5px",
}}
/>
<TableCellWithTooltip
key={child.id}
align="center"
title={child.title}
sx={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={child.status} />
<Typography component="span" variant="subtitle2" noWrap>
{child.title}
</div>
</th>
</Typography>
</TableCellWithTooltip>
));
} else {
return (
<th key={item.id} className="tree-table-header" title={item.title}>
<div className="header-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
}}
/>
{item.title}
</div>
</th>
);
}
return (
<TableCellWithTooltip
key={item.id}
align="center"
title={item.title}
sx={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="subtitle2" noWrap>
{item.title}
</Typography>
</TableCellWithTooltip>
);
});
};
// Функция для отображения данных
const renderData = (items) => {
return items.map((item) => {
// Рендер данных (третий уровень)
const renderDataCells = (items) => {
return items.flatMap((item) => {
if (item.items) {
return item.items.map((child) => {
return item.items.flatMap((child) => {
if (child.items) {
return child.items.map((subChild) => (
<td key={subChild.id} className="tree-table-cell" title={subChild.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(subChild.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(subChild.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{subChild.title}</span>
</div>
</td>
));
} else {
return (
<td key={child.id} className="tree-table-cell" title={child.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(child.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{child.title}</span>
</div>
</td>
);
}
});
} else {
return (
<td key={item.id} className="tree-table-cell" title={item.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
<TableCellWithTooltip
key={subChild.id}
title={subChild.title}
sx={{
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
/>
<span className="cell-text">{item.title}</span>
</div>
</td>
);
>
<StatusIndicators status={subChild.status} />
<Typography component="span" variant="body2" noWrap>
{subChild.title}
</Typography>
</TableCellWithTooltip>
));
}
return (
<TableCellWithTooltip
key={child.id}
title={child.title}
sx={{
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={child.status} />
<Typography component="span" variant="body2" noWrap>
{child.title}
</Typography>
</TableCellWithTooltip>
);
});
}
return (
<TableCellWithTooltip
key={item.id}
title={item.title}
sx={{
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="body2" noWrap>
{item.title}
</Typography>
</TableCellWithTooltip>
);
});
};
return (
<div className="tree-table-container">
<table ref={tableRef} className="tree-table" style={{ fontSize: `${fontSize}px` }}>
<thead>
<tr>
<th
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)}
className="tree-table-header"
title={data.title}
>
<div className="header-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(data.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(data.status),
marginLeft: "5px",
}}
/>
{data.title}
</div>
</th>
</tr>
<tr>{renderHeaders(filteredData)}</tr>
<tr>{renderSubHeaders(filteredData)}</tr>
</thead>
<tbody>
<tr className="tree-table-row">{renderData(filteredData)}</tr>
</tbody>
</table>
<button
onClick={() => setIsLogVisible(!isLogVisible)}
className="toggle-log-button"
style={{ marginTop: "10px" }}
>
{isLogVisible ? "Скрыть лог" : "Показать лог"}
</button>
{isLogVisible && (
<div className="status-log">
<h3>Лог статусов</h3>
<ul>
<Box sx={{ width: '100%' }}>
<TableContainer
component={Paper}
ref={tableRef}
sx={{
fontSize: `${fontSize}px`,
width: '100%',
'& .MuiTableCell-root': {
py: 1,
px: 2
}
}}
>
<Table sx={{ width: '100%', tableLayout: 'fixed' }}>
<TableHead>
{/* Основной заголовок таблицы */}
<TableRow>
<TableCellWithTooltip
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)}
align="center"
title={data.title}
sx={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
padding: '8px'
}}
>
<StatusIndicators status={data.status} />
<Typography component="span" variant="subtitle1" fontWeight="bold" noWrap>
{data.title}
</Typography>
</TableCellWithTooltip>
</TableRow>
{/* Строка с основными заголовками */}
<TableRow>
{renderMainHeaders(filteredData)}
</TableRow>
{/* Строка с подзаголовками (которая пропала в предыдущей версии) */}
<TableRow>
{renderSubHeaders(filteredData)}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
{renderDataCells(filteredData)}
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Button
variant="outlined"
onClick={() => setIsLogVisible(!isLogVisible)}
size="small"
sx={{ mt: 2 }}
>
{isLogVisible ? 'Скрыть историю изменения статусов' : 'Показать историю изменения статусов'}
</Button>
<Collapse in={isLogVisible}>
<Box sx={{
mt: 2,
p: 2,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
backgroundColor: theme.palette.background.paper
}}>
<Typography variant="h6" gutterBottom>
История изменения статусов
</Typography>
<Box component="ul" sx={{
pl: 2,
maxHeight: 200,
overflow: 'auto',
listStyle: 'none'
}}>
{log.map((entry, index) => (
<li key={index} style={{ color: statusManager1.getStatusColor(entry.status) }}>
<Box
component="li"
key={index}
sx={{
py: 1,
borderBottom: `1px solid ${theme.palette.divider}`,
color: statusManager1.getStatusColor(entry.status)
}}
>
[{entry.time}] {entry.status}: {entry.title}
</li>
</Box>
))}
</ul>
</div>
)}
</div>
</Box>
</Box>
</Collapse>
</Box>
);
};

View File

@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
const LazyChartBatchRenderer = ({ charts, batchSize = 3, delay = 150 }) => {
const [visibleCharts, setVisibleCharts] = useState([]);
useEffect(() => {
let index = 0;
const timer = setInterval(() => {
setVisibleCharts((prev) => [
...prev,
...charts.slice(index, index + batchSize),
]);
index += batchSize;
if (index >= charts.length) clearInterval(timer);
}, delay);
return () => clearInterval(timer);
}, [charts]);
return (
<>
{visibleCharts.map((chart, idx) => (
<div key={idx}>{chart}</div>
))}
</>
);
};
export default LazyChartBatchRenderer;

View File

@ -1,6 +1,5 @@
import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable";
import FlowChart from "../TreeChart/FlowChart";
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {

View File

@ -1,52 +0,0 @@
/* Основной контейнер */
.dashboard-container {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
background-color: var(--background-color);
color: var(--text-color);
}
/* Сайдбар */
.sidebar {
flex-shrink: 0;
height: 100vh;
overflow-y: auto;
background-color: var(--sidebar-color);
color: var(--sidebar-text-color);
transition: width 0.2s ease;
}
/* Основной контент */
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow: auto;
background-color: var(--background-color);
color: var(--text-color);
}
/* Контент */
.content {
background-color: var(--modal-background);
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.521);
max-width: 100%;
overflow: auto;
color: var(--text-color);
}
/* Заголовки */
h2 {
color: var(--text-color);
text-align: center;
}
p {
color: var(--text-color);
}

View File

@ -1,38 +0,0 @@
.error-indicator {
display: flex;
align-items: center;
gap: 15px;
padding-bottom: 20px;
}
.error-item {
display: flex;
align-items: center;
gap: 5px;
}
.error-item img {
width: 30px;
height: 30px;
}
.error-item span {
font-size: 18px;
font-weight: bold;
}
.critical span {
color: red;
}
.warning span {
color: orange;
}
.indicator-container {
display: flex;
align-items: center;
gap: 15px;
justify-content: center;
}

View File

@ -1,39 +0,0 @@
.expandable-info {
margin-top: 10px;
}
.expand-button {
background-color: #444;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
}
.expand-button:hover {
background-color: #333;
}
.details-menu {
margin-top: 10px;
padding: 10px;
border: 1px solid #333;
border-radius: 4px;
background-color: white;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.label {
font-weight: bold;
color: #333
}
.value {
color: #333;
}

View File

@ -1,143 +0,0 @@
/* Сайдбар */
.sidebar {
height: 100vh;
background-color: var(--sidebar-color);
color: var(--sidebar-text-color);
position: fixed;
left: 0;
top: 0;
z-index: 999;
overflow: hidden;
transition: width 0.2s ease;
display: flex;
flex-direction: column;
}
/* Контейнер для основного контента меню */
.sidebar-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 20px;
padding-right: 10px;
/* Отступ справа для скроллбара */
}
/* Заголовок меню */
.sidebar-title {
margin-bottom: 20px;
font-size: 1.5em;
font-weight: bold;
color: var(--sidebar-text-color);
padding: 10px;
text-align: center;
/* font-size: 2vh; */
}
/* Элементы меню */
.menu-item {
margin-bottom: 10px;
color: var(--sidebar-text-color);
width: 100%;
}
/* Элемент для перетаскивания */
.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);
}
/* Стили для заголовка элемента меню */
.menu-item-header {
display: flex;
align-items: center;
justify-content: space-between;
/* Распределяем пространство между элементами */
padding: 10px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
/* Занимаем всю доступную ширину */
box-sizing: border-box;
/* Учитываем padding в ширине */
}
/* Стили для текста элемента меню */
.menu-item-header span {
flex: 1;
/* Текст занимает все доступное пространство */
margin-right: 14px;
/* Отступ справа для текста */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие, если текст не помещается */
}
/* Стили для иконок */
.menu-item-header .open-parent-icon,
.menu-item-header .toggle-icon {
flex-shrink: 0;
/* Запрещаем сжатие иконок */
margin-left: 1px;
/* Отступ между иконками */
cursor: pointer;
}
.menu-item-header:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Круглый индикатор статуса */
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
/* Подменю */
.submenu {
margin-left: 20px;
/* Отступ слева для вложенных элементов */
margin-top: 10px;
}
/* Стили для элементов нижнего уровня вложенности */
/* Дополнительные отступы для элементов без иконок */
.menu-item:not(.has-children) .menu-item-header {
padding-right: 25px;
/* Добавляем отступ справа для элементов без иконок */
}
/* Футер сайдбара */
.sidebar-footer {
padding: 10px;
background-color: var(--sidebar-color);
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
width: 100%;
}
.help,
.settings {
color: var(--sidebar-text-color);
margin: 5px 0;
overflow-x: hidden;
text-align: left;
}

View File

@ -1,62 +0,0 @@
.tree-table-container {
width: 100%;
overflow-x: hidden;
/* Убираем горизонтальный скролл */
}
.tree-table {
width: 100%;
border-collapse: collapse;
text-align: center;
table-layout: fixed;
/* Фиксированная ширина колонок */
background-color: var(--table-cell-background);
color: var(--table-text-color);
}
.tree-table-header {
padding: 10px;
border: 1px solid black;
font-weight: bold;
white-space: nowrap;
/* Текст не переносится */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие */
background-color: var(--table-header-background);
}
.tree-table-cell {
padding: 8px;
border: 1px solid black;
white-space: nowrap;
/* Текст не переносится */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие */
}
.cell-content,
.header-content {
display: flex;
align-items: center;
gap: 2px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.cell-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.status-indicator-bar {
width: 6px;
height: 20px;
border-radius: 3px;
flex-shrink: 0;
}

View File

@ -1,53 +0,0 @@
/* Контейнер для вкладок */
.tabs {
display: flex;
gap: 5px;
padding: 5px;
background-color: var(--sidebar-color);
border-bottom: 2px solid var(--accent-color);
overflow-x: auto;
border-radius: 5px;
white-space: nowrap;
}
/* Стили для отдельной вкладки */
.tab {
display: flex;
align-items: center;
background-color: var(--sidebar-color);
color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
padding: 5px 15px;
border-radius: 5px 5px 0 0;
cursor: pointer;
flex-shrink: 0;
transition: background-color 0.3s ease;
}
/* Активная вкладка */
.tab.active {
background-color: var(--accent-color);
}
/* Кнопка закрытия вкладки */
.close-tab {
background: none;
border: none;
color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
cursor: pointer;
font-size: 16px;
margin-left: 10px;
padding: 0;
transition: color 0.3s ease;
}
/* Эффект при наведении на кнопку закрытия */
.close-tab:hover {
color: #ff6b6b;
}
/* Эффект при наведении на вкладку */
.tab:hover {
background-color: var(--accent-hover-color);
}

View File

@ -1,25 +0,0 @@
/* Темная тема, если пользователь предпочитает ее */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1E1E1E;
--text-color: #E0E0E0;
--header-color: #FFFFFF;
/* Основной цвет текста (светлый) */
--sidebar-color: #2d2d2d;
/* Темный цвет сайдбара */
--sidebar-text-color: #E0E0E0;
/* Светлый текст в сайдбаре */
--modal-background: #2d2d2d;
--modal--btn-background: #333333;
--modal-text: #FFFFFF;
--table-border: #c70a0a;
--table-header-background: #2d2d2d;
--table-cell-background: #333333;
--table-text-color: #E0E0E0;
/* Светлый текст в таблице */
--TreeChart-text-color: #ffffff;
--scrollbar-track-color: #333;
/* hover for buttons */
--hover-button: #333d4d;
}
}

View File

@ -1,23 +0,0 @@
/* Светлая тема по умолчанию */
:root {
--background-color: #FFFFFF;
--text-color: #000000;
--header-color: #333333;
/* Основной цвет текста (черный) */
--sidebar-color: #3d74c7;
/* Синий цвет сайдбара */
--sidebar-text-color: #FFFFFF;
/* Белый текст в сайдбаре и вкладках */
--modal-background: #FFFFFF;
--modal--btn-background: #0f55bec2;
--modal-text: #333333;
--table-border: #ddd;
--table-header-background: #f9f9f9;
--table-cell-background: #FFFFFF;
--table-text-color: #000000;
/* Черный текст в таблице */
/* hover for buttons */
--hover-button: #2d62b1;
--hover-text-color: #FFFFFF
}

View File

@ -1,73 +1,191 @@
import { createTheme } from "@mui/material/styles";
/**
* Общие настройки темы, применяемые для обеих тем (светлой и темной)
*/
const commonThemeSettings = {
// Настройки формы элементов
shape: {
borderRadius: 8, // Базовый радиус скругления углов для всех компонентов
},
// Переопределения стилей конкретных MUI компонентов
components: {
// Стили для компонента Drawer (боковое меню)
MuiDrawer: {
styleOverrides: {
paper: {
borderRight: 'none', // Убираем правую границу у бокового меню
}
},
MuiTab: {
styleOverrides: {
root: {
textTransform: 'none', // Убираем uppercase
minWidth: 'unset', // Убираем минимальную ширину
padding: '6px 16px',
'&:hover': {
color: 'primary.main',
opacity: 1,
},
'&.Mui-selected': {
color: 'primary.main',
},
'&.Mui-focusVisible': {
backgroundColor: 'action.selected',
},
},
},
},
MuiTabs: {
styleOverrides: {
indicator: {
height: 3, // Толщина индикатора
},
},
},
},
// Стили для кнопок-элементов списка
MuiListItemButton: {
styleOverrides: {
root: {
// Стиль для выбранного элемента
'&.Mui-selected': {
backgroundColor: 'rgba(255, 255, 255, 0.16)',
},
// Стиль при наведении на выбранный элемент
'&.Mui-selected:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.24)',
},
}
}
}
}
};
/**
* Светлая тема приложения
*/
export const lightTheme = createTheme({
...commonThemeSettings, // Распаковываем общие настройки
// Цветовая палитра для светлой темы
palette: {
mode: "light",
mode: "light", // Режим светлой темы
// Фоновые цвета
background: {
default: "#FFFFFF",
paper: "#FFFFFF",
default: "#6CACE4", // Основной фон приложения
paper: "#FFFFFF", // Фон "бумажных" поверхностей (карточек, панелей)
},
// Текстовые цвета
text: {
primary: "#000000",
primary: "#000000", // Основной цвет текста
secondary: "#333333", // Вторичный цвет текста
},
// Основные цвета UI
primary: {
main: "#3d74c7",
main: "#3d74c7", // Основной брендовый цвет
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
},
// Дополнительные цвета UI
secondary: {
main: "#0f55bec2",
main: "#0f55bec2", // Вторичный брендовый цвет
},
divider: "#e0e0e0", // Цвет разделителей
// Кастомные цвета для специфических элементов
custom: {
background: "#FFFFFF",
text: "#000000",
sidebar: "#3d74c7",
sidebarText: "#FFFFFF",
modalBackground: "#FFFFFF",
modalBtnBackground: "#0f55bec2",
modalText: "#333333",
tableBorder: "#ddd",
tableHeaderBackground: "#f9f9f9",
tableCellBackground: "#FFFFFF",
tableText: "#000000",
treeChartText: "#000000",
scrollbarTrack: "#f1f1f1",
hoverButton: "#2d62b1",
hoverText: "#FFFFFF",
background: "#D4EFFC", // Кастомный фоновый цвет
text: "#000000", // Кастомный цвет текста
sidebar: "#025EA1", // Фон боковой панели
sidebarText: "#FFFFFF", // Текст в боковой панели
sidebarHover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении в боковой панели
modalBackground: "#FFFFFF", // Фон модальных окон
modalBtnBackground: "#0f55bec2", // Фон кнопок в модальных окнах
modalText: "#333333", // Текст в модальных окнах
tableBorder: "#ddd", // Границы таблиц
tableHeaderBackground: "#f9f9f9", // Фон заголовков таблиц
tableCellBackground: "#FFFFFF", // Фон ячеек таблиц
tableText: "#000000", // Текст в таблицах
treeChartText: "#000000", // Текст в древовидных диаграммах
scrollbarTrack: "#f1f1f1", // Цвет трека скроллбара
hoverButton: "#2d62b1", // Цвет кнопок при наведении
hoverText: "#FFFFFF", // Цвет текста при наведении
},
// Цвета для различных состояний
action: {
hover: "rgba(0, 0, 0, 0.04)", // Цвет при наведении на интерактивные элементы
selected: "rgba(0, 0, 0, 0.08)", // Цвет выбранных элементов
}
},
});
/**
* Темная тема приложения
*/
export const darkTheme = createTheme({
...commonThemeSettings, // Распаковываем общие настройки
// Цветовая палитра для темной темы
palette: {
mode: "dark",
mode: "dark", // Режим темной темы
// Фоновые цвета
background: {
default: "#1E1E1E",
paper: "#2d2d2d",
default: "#1E1E1E", // Основной фон приложения
paper: "#2d2d2d", // Фон "бумажных" поверхностей
},
// Текстовые цвета
text: {
primary: "#E0E0E0",
primary: "#E0E0E0", // Основной цвет текста
secondary: "#B0B0B0", // Вторичный цвет текста
},
// Основные цвета UI
primary: {
main: "#2d2d2d",
main: "#3d74c7", // Основной брендовый цвет (может совпадать со светлой темой)
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
},
// Дополнительные цвета UI
secondary: {
main: "#333333",
main: "#0f55bec2", // Вторичный брендовый цвет
},
divider: "#444444", // Цвет разделителей
// Кастомные цвета для специфических элементов
custom: {
background: "#1E1E1E",
text: "#E0E0E0",
sidebar: "#2d2d2d",
sidebarText: "#E0E0E0",
modalBackground: "#2d2d2d",
modalBtnBackground: "#333333",
modalText: "#FFFFFF",
tableBorder: "#444444",
tableHeaderBackground: "#2d2d2d",
tableCellBackground: "#333333",
tableText: "#E0E0E0",
treeChartText: "#FFFFFF",
scrollbarTrack: "#333",
hoverButton: "#333d4d",
hoverText: "#E0E0E0",
background: "#1E1E1E", // Кастомный фоновый цвет
text: "#E0E0E0", // Кастомный цвет текста
sidebar: "#2d2d2d", // Фон боковой панели
sidebarText: "#E0E0E0", // Текст в боковой панели
sidebarHover: "rgba(255, 255, 255, 0.16)", // Цвет при наведении в боковой панели
modalBackground: "#2d2d2d", // Фон модальных окон
modalBtnBackground: "#333333", // Фон кнопок в модальных окнах
modalText: "#FFFFFF", // Текст в модальных окнах
tableBorder: "#444444", // Границы таблиц
tableHeaderBackground: "#2d2d2d", // Фон заголовков таблиц
tableCellBackground: "#333333", // Фон ячеек таблиц
tableText: "#E0E0E0", // Текст в таблицах
treeChartText: "#FFFFFF", // Текст в древовидных диаграммах
scrollbarTrack: "#333", // Цвет трека скроллбара
hoverButton: "#333d4d", // Цвет кнопок при наведении
hoverText: "#E0E0E0", // Цвет текста при наведении
},
// Цвета для различных состояний
action: {
hover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении на интерактивные элементы
selected: "rgba(255, 255, 255, 0.16)", // Цвет выбранных элементов
}
},
});

View File

@ -83,7 +83,7 @@ button:focus-visible {
/* Фон скроллбара */
::-webkit-scrollbar-track {
background: var(--scrollbar-track-color, #f1f1f1);
background: var(--scrollbar-track-color, #025EA1);
/* Цвет фона */
border-radius: 10px;
/* Скругление углов */
@ -91,7 +91,7 @@ button:focus-visible {
/* Ползунок */
::-webkit-scrollbar-thumb {
background: #3d74c7;
background: #D4EFFC;
/* Основной цвет */
border-radius: 10px;
/* Скругляем края */