redesign #36
|
|
@ -26,7 +26,8 @@
|
||||||
"@mui/icons-material": "^6.4.8",
|
"@mui/icons-material": "^6.4.8",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"vite-plugin-svgr": "^4.3.0",
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
"react-scripts": "^5.0.1"
|
"react-scripts": "^5.0.1",
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,103 +1,229 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from 'recharts';
|
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 LineChartComponent = ({
|
||||||
const [selectionStart, setSelectionStart] = useState(null);
|
chartData,
|
||||||
const [selectionEnd, setSelectionEnd] = useState(null);
|
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()
|
.flat()
|
||||||
.map(point => point.time)
|
.map(point => point.timestamp)
|
||||||
.filter((time, index, self) => self.indexOf(time) === index);
|
.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 => {
|
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;
|
point[key] = instanceData ? instanceData.value : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return point;
|
return point;
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayData = filteredData || data;
|
const displayData = filteredData || data;
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const instanceKeys = displayData.length
|
||||||
if (!e || !e.activeLabel) return;
|
? 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) {
|
const range = data[data.length - 1].timestamp - data[0].timestamp;
|
||||||
setSelectionStart(clickedTime);
|
|
||||||
} else if (!selectionEnd) {
|
|
||||||
setSelectionEnd(clickedTime);
|
|
||||||
|
|
||||||
const startIndex = data.findIndex(point => point.time === selectionStart);
|
if (range > DAY) return TIME_FORMATS.LONG;
|
||||||
const endIndex = data.findIndex(point => point.time === clickedTime);
|
if (range > HOUR) return TIME_FORMATS.MEDIUM;
|
||||||
|
return TIME_FORMATS.SHORT;
|
||||||
|
};
|
||||||
|
|
||||||
onRangeSelect({ startIndex, endIndex });
|
useEffect(() => {
|
||||||
|
const handleSelectStart = (e) => {
|
||||||
setSelectionStart(null);
|
if (isSelecting) {
|
||||||
setSelectionEnd(null);
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Упрощенный Tooltip без указания instance
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload, label }) => {
|
const CustomTooltip = ({ active, payload, label }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
|
const currentPoint = data.find(point => point.timestamp === label);
|
||||||
return (
|
return (
|
||||||
<div className="custom-tooltip" style={{
|
<div style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
border: '1px solid #ccc',
|
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 style={{ fontWeight: 'bold', marginBottom: '5px' }}>
|
||||||
<p>{`Значение: ${payload[0].value}`}</p>
|
{currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')}
|
||||||
|
</p>
|
||||||
|
{payload.map((item, index) => (
|
||||||
|
<p key={index} style={{ color: item.color }}>
|
||||||
|
{`Значение: ${item.value}`}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ position: 'relative', height: '400px' }}>
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<LineChart
|
<LineChart
|
||||||
data={displayData}
|
data={displayData}
|
||||||
onClick={handleClick}
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
ref={chartRef}
|
||||||
>
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="timestamp"
|
||||||
tick={{ fill: '#666' }}
|
height={75}
|
||||||
tickMargin={10}
|
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' }}
|
<YAxis tick={{ fontSize: 12 }} />
|
||||||
tickMargin={10}
|
<Tooltip content={<CustomTooltip />} />
|
||||||
/>
|
{instanceKeys.map((instance, index) => (
|
||||||
<Tooltip
|
|
||||||
content={<CustomTooltip />}
|
|
||||||
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
|
|
||||||
/>
|
|
||||||
{/* Убрали <Legend /> чтобы скрыть имена instance */}
|
|
||||||
{Object.keys(chartData).map((key, index) => (
|
|
||||||
<Line
|
<Line
|
||||||
key={key}
|
key={instance}
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey={key}
|
dataKey={instance}
|
||||||
|
name={instance}
|
||||||
stroke={colors[index % colors.length]}
|
stroke={colors[index % colors.length]}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={false}
|
dot={false}
|
||||||
activeDot={{ r: 6 }}
|
activeDot={{ r: 6 }}
|
||||||
// Убрали name чтобы не отображалось в tooltip
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{selectionArea?.start && selectionArea?.end && (
|
||||||
|
<ReferenceArea
|
||||||
|
x1={selectionArea.start}
|
||||||
|
x2={selectionArea.end}
|
||||||
|
strokeOpacity={0.3}
|
||||||
|
fill="#4a6baf"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LineChartComponent;
|
export default React.memo(LineChartComponent);
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,369 +1,494 @@
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
import axios from 'axios';
|
import { io } from 'socket.io-client';
|
||||||
import DatePicker from 'react-datepicker';
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
|
||||||
import LineChartComponent from './Components/LineChartComponent';
|
import LineChartComponent from './Components/LineChartComponent';
|
||||||
|
import { TimeRangeSelector } from './Components/TimeRangeSelector';
|
||||||
const MAX_POINTS = 20;
|
import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator';
|
||||||
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850'];
|
import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay';
|
||||||
const TIME_RANGES = [
|
import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants';
|
||||||
{ label: '1 минута', value: 60, interval: 3000 },
|
import axios from 'axios';
|
||||||
{ label: '5 минут', value: 300, interval: 15000 },
|
|
||||||
{ label: '30 минут', value: 1800, interval: 90000 },
|
|
||||||
{ label: '1 час', value: 3600, interval: 180000 },
|
|
||||||
{ label: '3 часа', value: 10800, interval: 540000 },
|
|
||||||
{ label: '6 часов', value: 21600, interval: 1080000 },
|
|
||||||
{ label: '12 часов', value: 43200, interval: 2160000 },
|
|
||||||
{ label: '24 часа', value: 86400, interval: 4320000 },
|
|
||||||
{ label: '2 дня', value: 172800, interval: 8640000 },
|
|
||||||
{ label: '7 дней', value: 604800, interval: 30240000 },
|
|
||||||
{ label: '30 дней', value: 2592000, interval: 129600000 },
|
|
||||||
{ label: '90 дней', value: 7776000, interval: 388800000 },
|
|
||||||
{ label: '6 месяцев', value: 15552000, interval: 777600000 },
|
|
||||||
{ label: '9 месяцев', value: 23328000, interval: 1166400000 },
|
|
||||||
{ label: '1 год', value: 31536000, interval: 1576800000 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const PrometheusChart = ({ metricName }) => {
|
const PrometheusChart = ({ metricName }) => {
|
||||||
const [chartData, setChartData] = useState({});
|
const [chartData, setChartData] = useState(null);
|
||||||
const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]);
|
const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]);
|
||||||
const [startDate, setStartDate] = useState(new Date());
|
const [startDate, setStartDate] = useState(new Date());
|
||||||
const [endDate, setEndDate] = useState(new Date());
|
const [endDate, setEndDate] = useState(new Date());
|
||||||
const [useCustomRange, setUseCustomRange] = useState(false);
|
const [useCustomRange, setUseCustomRange] = useState(false);
|
||||||
const [selectedGraphRange, setSelectedGraphRange] = useState(null); // Выбранный диапазон
|
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
||||||
const [filteredData, setFilteredData] = useState(null); // Отфильтрованные данные
|
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 intervalRef = useRef(null);
|
||||||
|
const socketRef = useRef(null);
|
||||||
|
const debounceRef = useRef(null);
|
||||||
|
|
||||||
// Функция для интерполяции данных
|
const formatTime = useCallback((timestamp, rangeSeconds) => {
|
||||||
const interpolateData = (data, minPoints = 15) => {
|
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
|
||||||
if (data.length >= minPoints) return data;
|
const date = new Date(ts);
|
||||||
|
|
||||||
const interpolatedData = [];
|
// Определяем формат в зависимости от диапазона
|
||||||
for (let i = 0; i < data.length - 1; i++) {
|
const showFullDate = rangeSeconds > 86400; // больше суток
|
||||||
interpolatedData.push(data[i]);
|
|
||||||
|
|
||||||
const currentPoint = data[i];
|
const timeOptions = {
|
||||||
const nextPoint = data[i + 1];
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
// Вычисляем разницу во времени между точками
|
second: '2-digit',
|
||||||
const currentTime = new Date(currentPoint.time).getTime();
|
hour12: false
|
||||||
const nextTime = new Date(nextPoint.time).getTime();
|
|
||||||
const timeDiff = nextTime - currentTime;
|
|
||||||
|
|
||||||
// Добавляем промежуточные точки
|
|
||||||
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 };
|
|
||||||
|
|
||||||
// Интерполируем значения для каждой метрики
|
|
||||||
Object.keys(currentPoint).forEach(key => {
|
|
||||||
if (key !== 'time') {
|
|
||||||
const currentValue = currentPoint[key];
|
|
||||||
const nextValue = nextPoint[key];
|
|
||||||
interpolatedPoint[key] = currentValue + ((nextValue - currentValue) * j) / (steps + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
interpolatedData.push(interpolatedPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку
|
|
||||||
return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
const dateOptions = showFullDate ? {
|
||||||
try {
|
month: '2-digit',
|
||||||
let start, end;
|
day: '2-digit',
|
||||||
|
...timeOptions
|
||||||
|
} : timeOptions;
|
||||||
|
|
||||||
if (useCustomRange) {
|
return {
|
||||||
start = Math.floor(startDate.getTime() / 1000);
|
display: date.toLocaleString('ru-RU', dateOptions),
|
||||||
end = Math.floor(endDate.getTime() / 1000);
|
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
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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 часов
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
end = Math.floor(Date.now() / 1000);
|
timestamp = Date.now();
|
||||||
start = end - selectedRange.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let step;
|
const value = parseFloat(item.value);
|
||||||
const range = end - start;
|
const formattedTime = formatTime(timestamp, rangeSeconds);
|
||||||
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`, {
|
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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
|
||||||
params: {
|
params: {
|
||||||
metric: metricName,
|
metric: metricName,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
step
|
step: calculateStep(start, end)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = response.data;
|
if (response.data?.length) {
|
||||||
let metrics = Array.isArray(result) ? result : result.data || [];
|
// Преобразуем данные перед передачей в processMetricsData
|
||||||
|
const processedData = response.data.map(item => ({
|
||||||
if (!Array.isArray(metrics)) {
|
...item,
|
||||||
metrics = [];
|
timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует
|
||||||
}
|
value: item.value.toString()
|
||||||
|
|
||||||
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,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
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);
|
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(() => {
|
||||||
|
const socket = setupWebSocket();
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
socket.disconnect();
|
||||||
|
};
|
||||||
|
}, [setupWebSocket]);
|
||||||
|
|
||||||
|
// Обновим 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) {
|
} catch (error) {
|
||||||
console.error('Ошибка при загрузке метрик:', error);
|
console.error('Error in interval fetch:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Очищаем предыдущий интервал
|
||||||
fetchData();
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
intervalRef.current = setInterval(() => {
|
// Запускаем сразу и затем по интервалу
|
||||||
fetchData();
|
fetchDataWrapper();
|
||||||
}, selectedRange.interval);
|
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [metricName, selectedRange, useCustomRange, startDate, endDate]);
|
}, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]);
|
||||||
|
|
||||||
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); // Сбрасываем отфильтрованные данные
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedGraphRange) {
|
if (!selectedGraphRange || !chartData) {
|
||||||
const { startIndex, endIndex } = selectedGraphRange;
|
setFilteredData(null);
|
||||||
const allTimes = Object.values(chartData)
|
return;
|
||||||
.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); // Сбрасываем фильтрацию, если диапазон не выбран
|
|
||||||
}
|
}
|
||||||
}, [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)
|
const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1));
|
||||||
.flat()
|
const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1));
|
||||||
.map(point => point.time)
|
|
||||||
.filter((time, index, self) => self.indexOf(time) === index);
|
|
||||||
|
|
||||||
const data = allTimes.map(time => {
|
const filtered = sortedPoints.slice(startIndex, endIndex + 1);
|
||||||
const point = { time };
|
const interpolated = filtered.length > 100 ?
|
||||||
Object.keys(chartData).forEach(key => {
|
interpolateData(filtered, 100) :
|
||||||
const instanceData = chartData[key].find(p => p.time === time);
|
filtered;
|
||||||
point[key] = instanceData ? instanceData.value : null;
|
|
||||||
});
|
setFilteredData(interpolated);
|
||||||
return point;
|
}, [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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
marginBottom: '20px'
|
marginBottom: '20px',
|
||||||
|
position: 'relative'
|
||||||
}}>
|
}}>
|
||||||
{/* Заголовок графика */}
|
<ConnectionStatusIndicator connectionStatus={connectionStatus} />
|
||||||
<h3 style={{ marginTop: 0, color: '#333' }}>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Группа элементов управления */}
|
<TimeRangeSelector
|
||||||
<div style={{
|
selectedRange={selectedRange}
|
||||||
display: 'flex',
|
handleRangeChange={handleRangeChange}
|
||||||
flexWrap: 'wrap',
|
startDate={startDate}
|
||||||
gap: '15px',
|
setStartDate={setStartDate}
|
||||||
alignItems: 'center',
|
endDate={endDate}
|
||||||
marginBottom: '15px'
|
setEndDate={setEndDate}
|
||||||
}}>
|
useCustomRange={useCustomRange}
|
||||||
{/* Стандартные диапазоны */}
|
handleCustomRangeChange={handleCustomRangeChange}
|
||||||
<div style={{ flex: '1 1 200px' }}>
|
handleResetZoom={handleResetZoom}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Кастомный диапазон */}
|
|
||||||
<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' }}>
|
<CurrentRangeDisplay
|
||||||
<DatePicker
|
useCustomRange={useCustomRange}
|
||||||
selected={endDate}
|
startDate={startDate}
|
||||||
onChange={(date) => setEndDate(date)}
|
endDate={endDate}
|
||||||
showTimeSelect
|
selectedRange={selectedRange}
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Индикатор текущего диапазона */}
|
|
||||||
<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
|
<LineChartComponent
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
metricName={metricName}
|
metricName={metricName}
|
||||||
colors={COLORS}
|
colors={COLORS}
|
||||||
description={metricName}
|
onRangeSelect={handleRangeSelect}
|
||||||
onRangeSelect={setSelectedGraphRange}
|
|
||||||
filteredData={filteredData}
|
filteredData={filteredData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PrometheusChart;
|
export default React.memo(PrometheusChart);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,22 @@ const SystemStatusChart = ({ data }) => {
|
||||||
// Обрезаем массив, оставляя только последние 20 точек
|
// Обрезаем массив, оставляя только последние 20 точек
|
||||||
const trimmedData = data.slice(-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 (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<LineChart
|
<LineChart
|
||||||
|
|
@ -16,8 +32,7 @@ const SystemStatusChart = ({ data }) => {
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="time" />
|
<XAxis dataKey="time" />
|
||||||
<YAxis domain={[0, 100]} />
|
<YAxis domain={[0, 100]} />
|
||||||
<Tooltip />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
|
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Box, styled } from "@mui/material";
|
||||||
import SidebarMenu from "./SidebarMenu";
|
import SidebarMenu from "./SidebarMenu";
|
||||||
import "../../Style/Dashboard.css";
|
|
||||||
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
||||||
import generateTabContent from "../TreeChart/tabContent";
|
import generateTabContent from "../TreeChart/tabContent";
|
||||||
import CustomTabs from "../UI/MUItabs";
|
import CustomTabs from "../UI/MUItabs";
|
||||||
|
|
@ -9,6 +9,53 @@ import useSidebarResize from "../hooks/useSidebarResize";
|
||||||
import TabContent from "../hooks/TabContent";
|
import TabContent from "../hooks/TabContent";
|
||||||
import menuData from "../TreeChart/menuData.json";
|
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 Dashboard = () => {
|
||||||
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
|
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
|
||||||
const { sidebarWidth, startResizing } = useSidebarResize(250);
|
const { sidebarWidth, startResizing } = useSidebarResize(250);
|
||||||
|
|
@ -20,13 +67,11 @@ const Dashboard = () => {
|
||||||
history2: [],
|
history2: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Генерация контента для вкладок
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generatedTabContent = generateTabContent(menuData);
|
const generatedTabContent = generateTabContent(menuData);
|
||||||
setTabContent(generatedTabContent);
|
setTabContent(generatedTabContent);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Обновление статусов каждые 30 секунд
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
|
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
|
||||||
|
|
@ -56,15 +101,20 @@ const Dashboard = () => {
|
||||||
}, [treeData1, treeData2]);
|
}, [treeData1, treeData2]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-container">
|
<DashboardContainer>
|
||||||
{/* Сайдбар */}
|
{/* Сайдбар */}
|
||||||
<div className="sidebar" style={{ width: sidebarWidth }}>
|
<SidebarWrapper sx={{ width: sidebarWidth }}>
|
||||||
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} />
|
<SidebarMenu
|
||||||
<div className="sidebar-resizer" onMouseDown={startResizing} />
|
data={treeData1}
|
||||||
</div>
|
onOpenTab={handleOpenTab}
|
||||||
|
sidebarWidth={sidebarWidth}
|
||||||
|
startResizing={startResizing}
|
||||||
|
/>
|
||||||
|
<SidebarResizer onMouseDown={startResizing} />
|
||||||
|
</SidebarWrapper>
|
||||||
|
|
||||||
{/* Основной контент */}
|
{/* Основной контент */}
|
||||||
<div className="main-content">
|
<MainContent>
|
||||||
{/* Вкладки */}
|
{/* Вкладки */}
|
||||||
<CustomTabs
|
<CustomTabs
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
|
@ -74,7 +124,7 @@ const Dashboard = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Контент вкладки */}
|
{/* Контент вкладки */}
|
||||||
<div className="content">
|
<Content>
|
||||||
<TabContent
|
<TabContent
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
statusHistories={statusHistories}
|
statusHistories={statusHistories}
|
||||||
|
|
@ -82,9 +132,9 @@ const Dashboard = () => {
|
||||||
tabContent={tabContent}
|
tabContent={tabContent}
|
||||||
handleOpenTab={handleOpenTab}
|
handleOpenTab={handleOpenTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Content>
|
||||||
</div>
|
</MainContent>
|
||||||
</div>
|
</DashboardContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,150 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Drawer, List } from "@mui/material";
|
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 MenuItem from "./SidebarMenuComponents/MenuItem";
|
||||||
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
|
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
|
||||||
|
|
||||||
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
const SidebarResizer = styled('div')(({ theme }) => ({
|
||||||
const handleSelectItem = (id, title, children) => {
|
|
||||||
onOpenTab(id, title, children);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
variant="permanent"
|
|
||||||
sx={{
|
|
||||||
width: sidebarWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
"& .MuiDrawer-paper": {
|
|
||||||
width: sidebarWidth,
|
|
||||||
boxSizing: "border-box",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<List>
|
|
||||||
<h2 style={{ padding: "16px", fontWeight: "bold", }}>Меню</h2>
|
|
||||||
<MenuItem item={data} onSelectItem={handleSelectItem} />
|
|
||||||
</List>
|
|
||||||
|
|
||||||
{/* Ресайзер */}
|
|
||||||
<div
|
|
||||||
onMouseDown={startResizing}
|
|
||||||
style={{
|
|
||||||
width: "5px",
|
width: "5px",
|
||||||
cursor: "ew-resize",
|
cursor: "ew-resize",
|
||||||
backgroundColor: "#ccc",
|
backgroundColor: 'transparent',
|
||||||
height: "100%",
|
height: "100%",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
}}
|
transition: 'background-color 0.2s',
|
||||||
/>
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
zIndex: 2
|
||||||
|
}));
|
||||||
|
|
||||||
<SidebarFooter sidebarWidth={sidebarWidth} />
|
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 (
|
||||||
|
<Box
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
width: drawerWidth,
|
||||||
|
transition: 'width 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Содержимое меню */}
|
||||||
|
<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>
|
</Drawer>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,81 @@
|
||||||
import React from "react";
|
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";
|
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
||||||
|
|
||||||
// Функция для сбора всех потомков
|
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
|
||||||
const getAllChildren = (node) => {
|
cursor: "pointer",
|
||||||
let children = [];
|
paddingLeft: theme.spacing(2 + level * 2),
|
||||||
if (node.items && node.items.length > 0) {
|
'&:hover': {
|
||||||
node.items.forEach((child) => {
|
backgroundColor: theme.palette.action.hover,
|
||||||
children.push(child); // Добавляем текущий элемент
|
},
|
||||||
children = children.concat(getAllChildren(child)); // Рекурсивно добавляем потомков
|
'&.Mui-selected': {
|
||||||
});
|
backgroundColor: theme.palette.custom.sidebarHover,
|
||||||
}
|
},
|
||||||
return children;
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
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 [isOpen, setIsOpen] = React.useState(false);
|
||||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
|
const handleToggle = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenTab = (e) => {
|
const handleOpenTab = (e) => {
|
||||||
e.stopPropagation(); // Останавливаем всплытие события
|
e.stopPropagation();
|
||||||
const allChildren = getAllChildren(item); // Собираем всех потомков
|
const allChildren = getAllChildren(item);
|
||||||
onSelectItem(item.id, item.title, allChildren); // Передаем данные в родительский компонент
|
onSelectItem(item.id, item.title, allChildren);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<StyledListItem
|
||||||
component="div"
|
component="div"
|
||||||
onClick={hasChildren ? handleToggle : handleOpenTab}
|
onClick={hasChildren ? handleToggle : handleOpenTab}
|
||||||
|
level={level}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer", // Курсор pointer везде
|
pl: collapsed ? 2 : 2 + level * 2, // Адаптируем отступы
|
||||||
pl: 2 + level * 2, // Сдвиг в зависимости от уровня вложенности
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "#f5f5f5", // Подсветка при наведении на весь элемент
|
|
||||||
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
|
||||||
{hasChildren ? (
|
<IconWrapper onClick={handleOpenTab}>
|
||||||
<div
|
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
|
||||||
onClick={handleOpenTab}
|
</IconWrapper>
|
||||||
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>
|
</ListItemIcon>
|
||||||
|
|
||||||
|
{!collapsed && ( // Показываем текст только в развернутом состоянии
|
||||||
|
<>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={item.title}
|
primary={item.title}
|
||||||
sx={{ cursor: "pointer" }} // Курсор pointer для текста
|
primaryTypographyProps={{
|
||||||
|
color: 'custom.sidebarText'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
||||||
</ListItem>
|
</>
|
||||||
{hasChildren && (
|
)}
|
||||||
|
</StyledListItem>
|
||||||
|
|
||||||
|
{hasChildren && !collapsed && ( // Показываем детей только в развернутом состоянии
|
||||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||||
<List component="div" disablePadding>
|
<List component="div" disablePadding>
|
||||||
{item.items.map((child, index) => (
|
{item.items.map((child, index) => (
|
||||||
|
|
@ -89,7 +83,8 @@ const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен п
|
||||||
key={index}
|
key={index}
|
||||||
item={child}
|
item={child}
|
||||||
onSelectItem={onSelectItem}
|
onSelectItem={onSelectItem}
|
||||||
level={level + 1} // Увеличиваем уровень вложенности
|
level={level + 1}
|
||||||
|
collapsed={collapsed}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</List>
|
</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;
|
export default MenuItem;
|
||||||
|
|
@ -1,16 +1,47 @@
|
||||||
import React from "react";
|
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 (
|
return (
|
||||||
<List sx={{ marginTop: "auto", backgroundColor: "#ffffff", padding: "10px 0" }}>
|
<FooterList>
|
||||||
<ListItem button={true}>
|
<FooterListItem button>
|
||||||
<ListItemText primary="Помощь" sx={{ color: "#000000" }} />
|
<ListItemText
|
||||||
</ListItem>
|
primary="Помощь"
|
||||||
<ListItem button={true}>
|
primaryTypographyProps={{
|
||||||
<ListItemText primary="Настройка" sx={{ color: "#000000" }} />
|
color: 'custom.sidebarText',
|
||||||
</ListItem>
|
variant: 'body2'
|
||||||
</List>
|
}}
|
||||||
|
/>
|
||||||
|
</FooterListItem>
|
||||||
|
<FooterListItem button>
|
||||||
|
<ListItemText
|
||||||
|
primary="Настройка"
|
||||||
|
primaryTypographyProps={{
|
||||||
|
color: 'custom.sidebarText',
|
||||||
|
variant: 'body2'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FooterListItem>
|
||||||
|
</FooterList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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": "ПО"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import React, { lazy, Suspense } from "react";
|
import React, { lazy, Suspense } from "react";
|
||||||
|
|
||||||
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
|
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
|
||||||
|
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
|
||||||
|
|
||||||
// Функция для генерации названия метрики на основе id
|
// Функция для генерации названия метрики на основе id
|
||||||
const getMetricName = (id) => {
|
const getMetricName = (id) => {
|
||||||
return `zvks_apiforsnmp_measure_${id}`;
|
return `zvks_apiforsnmp_measure_${id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
|
|
||||||
|
|
||||||
// Функция для рекурсивного сбора всех id потомков
|
// Функция для рекурсивного сбора всех id потомков
|
||||||
const getAllChildIds = (node) => {
|
const getAllChildIds = (node) => {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
|
|
@ -37,6 +36,7 @@ const tabContent = (data) => {
|
||||||
const content = (
|
const content = (
|
||||||
<div>
|
<div>
|
||||||
<h2>{node.title}</h2>
|
<h2>{node.title}</h2>
|
||||||
|
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
|
||||||
<p>Контент для {node.title}.</p>
|
<p>Контент для {node.title}.</p>
|
||||||
{childrenContent}
|
{childrenContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -17,7 +17,7 @@ const LoginModal = ({ onLogin, onClose }) => {
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
import React from "react";
|
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";
|
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 CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
|
||||||
const handleMouseDown = (e, id) => {
|
const handleMouseDown = (e, id) => {
|
||||||
if (e.button === 1) {
|
if (e.button === 1) { // Middle mouse button
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onCloseTab(id);
|
onCloseTab(id);
|
||||||
}
|
}
|
||||||
|
|
@ -15,7 +27,13 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
<Box sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
'& .MuiTabs-indicator': {
|
||||||
|
backgroundColor: 'primary.main',
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|
@ -23,28 +41,34 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
|
||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
aria-label="tabs"
|
aria-label="tabs"
|
||||||
>
|
>
|
||||||
{/* Всегда отображаемые вкладки */}
|
{/* Статические вкладки */}
|
||||||
<Tab
|
<StyledTab
|
||||||
label="Главная"
|
label="Главная"
|
||||||
value="Главная"
|
value="Главная"
|
||||||
onMouseDown={(e) => handleMouseDown(e, "Главная")}
|
onMouseDown={(e) => handleMouseDown(e, "Главная")}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<StyledTab
|
||||||
label="Визуализация"
|
label="Визуализация"
|
||||||
value="Визуализация"
|
value="Визуализация"
|
||||||
onMouseDown={(e) => handleMouseDown(e, "Визуализация")}
|
onMouseDown={(e) => handleMouseDown(e, "Визуализация")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Динамически добавляемые вкладки */}
|
{/* Динамические вкладки */}
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<Tab
|
<StyledTab
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
label={
|
label={
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<span>{tab.title}</span>
|
<span>{tab.title}</span>
|
||||||
<CloseIcon
|
<CloseIcon
|
||||||
fontSize="small"
|
fontSize="small"
|
||||||
sx={{ ml: 1, cursor: "pointer" }}
|
sx={{
|
||||||
|
ml: 1,
|
||||||
|
cursor: "pointer",
|
||||||
|
'&:hover': {
|
||||||
|
color: 'error.main'
|
||||||
|
}
|
||||||
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onCloseTab(tab.id);
|
onCloseTab(tab.id);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
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";
|
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
|
||||||
|
|
||||||
const TreeTable = ({ data }) => {
|
const TreeTable = ({ data }) => {
|
||||||
|
const theme = useTheme();
|
||||||
const tableRef = useRef(null);
|
const tableRef = useRef(null);
|
||||||
const [fontSize, setFontSize] = useState(16);
|
const [fontSize, setFontSize] = useState(16);
|
||||||
const [log, setLog] = useState([]);
|
const [log, setLog] = useState([]);
|
||||||
const [isLogVisible, setIsLogVisible] = useState(true);
|
const [isLogVisible, setIsLogVisible] = useState(false);
|
||||||
|
|
||||||
const adjustFontSize = () => {
|
const adjustFontSize = () => {
|
||||||
if (tableRef.current) {
|
if (tableRef.current) {
|
||||||
|
|
@ -27,6 +42,13 @@ const TreeTable = ({ data }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
adjustFontSize();
|
||||||
|
window.addEventListener('resize', adjustFontSize);
|
||||||
|
return () => window.removeEventListener('resize', adjustFontSize);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// Логирование статусов
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newLog = [];
|
const newLog = [];
|
||||||
const traverse = (items) => {
|
const traverse = (items) => {
|
||||||
|
|
@ -35,7 +57,7 @@ const TreeTable = ({ data }) => {
|
||||||
newLog.push({
|
newLog.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
time: new Date().toLocaleTimeString(), // Добавляем время
|
time: new Date().toLocaleTimeString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
|
|
@ -44,204 +66,285 @@ const TreeTable = ({ data }) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
traverse(data.items);
|
traverse(data.items);
|
||||||
|
setLog(prevLog => [...newLog, ...prevLog].slice(0, 50));
|
||||||
// Ограничиваем количество сообщений до 50
|
|
||||||
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
|
|
||||||
}, [data]);
|
}, [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) => {
|
return items.map((item) => {
|
||||||
const colSpan = item.items ? item.items.length : 1;
|
const colSpan = item.items ? item.items.length : 1;
|
||||||
return (
|
return (
|
||||||
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}>
|
<TableCellWithTooltip
|
||||||
<div className="header-content">
|
key={item.id}
|
||||||
<div
|
colSpan={colSpan}
|
||||||
className="status-indicator-bar"
|
align="center"
|
||||||
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
|
title={item.title}
|
||||||
/>
|
sx={{
|
||||||
<div
|
backgroundColor: theme.palette.background.paper,
|
||||||
className="status-indicator-bar"
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
style={{
|
padding: '8px',
|
||||||
backgroundColor: statusManager2.getStatusColor(item.status),
|
whiteSpace: 'nowrap',
|
||||||
marginLeft: "5px",
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<StatusIndicators status={item.status} />
|
||||||
|
<Typography component="span" variant="subtitle2" noWrap>
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</Typography>
|
||||||
</th>
|
</TableCellWithTooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для отображения подзаголовков
|
// Рендер подзаголовков (второй уровень)
|
||||||
const renderSubHeaders = (items) => {
|
const renderSubHeaders = (items) => {
|
||||||
return items.map((item) => {
|
return items.flatMap((item) => {
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
return item.items.map((child) => (
|
return item.items.map((child) => (
|
||||||
<th key={child.id} className="tree-table-header" title={child.title}>
|
<TableCellWithTooltip
|
||||||
<div className="header-content">
|
key={child.id}
|
||||||
<div
|
align="center"
|
||||||
className="status-indicator-bar"
|
title={child.title}
|
||||||
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
|
sx={{
|
||||||
/>
|
backgroundColor: theme.palette.background.paper,
|
||||||
<div
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
className="status-indicator-bar"
|
padding: '8px',
|
||||||
style={{
|
whiteSpace: 'nowrap',
|
||||||
backgroundColor: statusManager2.getStatusColor(child.status),
|
overflow: 'hidden',
|
||||||
marginLeft: "5px",
|
textOverflow: 'ellipsis'
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<StatusIndicators status={child.status} />
|
||||||
|
<Typography component="span" variant="subtitle2" noWrap>
|
||||||
{child.title}
|
{child.title}
|
||||||
</div>
|
</Typography>
|
||||||
</th>
|
</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) => {
|
const renderDataCells = (items) => {
|
||||||
return items.map((item) => {
|
return items.flatMap((item) => {
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
return item.items.map((child) => {
|
return item.items.flatMap((child) => {
|
||||||
if (child.items) {
|
if (child.items) {
|
||||||
return child.items.map((subChild) => (
|
return child.items.map((subChild) => (
|
||||||
<td key={subChild.id} className="tree-table-cell" title={subChild.title}>
|
<TableCellWithTooltip
|
||||||
<div className="cell-content">
|
key={subChild.id}
|
||||||
<div
|
title={subChild.title}
|
||||||
className="status-indicator-bar"
|
sx={{
|
||||||
style={{ backgroundColor: statusManager1.getStatusColor(subChild.status) }}
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
/>
|
padding: '8px',
|
||||||
<div
|
whiteSpace: 'nowrap',
|
||||||
className="status-indicator-bar"
|
overflow: 'hidden',
|
||||||
style={{
|
textOverflow: 'ellipsis'
|
||||||
backgroundColor: statusManager2.getStatusColor(subChild.status),
|
|
||||||
marginLeft: "5px",
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<span className="cell-text">{subChild.title}</span>
|
<StatusIndicators status={subChild.status} />
|
||||||
</div>
|
<Typography component="span" variant="body2" noWrap>
|
||||||
</td>
|
{subChild.title}
|
||||||
|
</Typography>
|
||||||
|
</TableCellWithTooltip>
|
||||||
));
|
));
|
||||||
} 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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} 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",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="cell-text">{item.title}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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 (
|
return (
|
||||||
<div className="tree-table-container">
|
<Box sx={{ width: '100%' }}>
|
||||||
<table ref={tableRef} className="tree-table" style={{ fontSize: `${fontSize}px` }}>
|
<TableContainer
|
||||||
<thead>
|
component={Paper}
|
||||||
<tr>
|
ref={tableRef}
|
||||||
<th
|
sx={{
|
||||||
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)}
|
fontSize: `${fontSize}px`,
|
||||||
className="tree-table-header"
|
width: '100%',
|
||||||
title={data.title}
|
'& .MuiTableCell-root': {
|
||||||
>
|
py: 1,
|
||||||
<div className="header-content">
|
px: 2
|
||||||
<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 ? "Скрыть лог" : "Показать лог"}
|
<Table sx={{ width: '100%', tableLayout: 'fixed' }}>
|
||||||
</button>
|
<TableHead>
|
||||||
{isLogVisible && (
|
{/* Основной заголовок таблицы */}
|
||||||
<div className="status-log">
|
<TableRow>
|
||||||
<h3>Лог статусов</h3>
|
<TableCellWithTooltip
|
||||||
<ul>
|
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) => (
|
{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}
|
[{entry.time}] {entry.status}: {entry.title}
|
||||||
</li>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
)}
|
</Collapse>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
||||||
import TreeTable from "../UI/TreeTable";
|
import TreeTable from "../UI/TreeTable";
|
||||||
|
|
||||||
import FlowChart from "../TreeChart/FlowChart";
|
import FlowChart from "../TreeChart/FlowChart";
|
||||||
|
|
||||||
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +1,191 @@
|
||||||
import { createTheme } from "@mui/material/styles";
|
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({
|
export const lightTheme = createTheme({
|
||||||
|
...commonThemeSettings, // Распаковываем общие настройки
|
||||||
|
|
||||||
|
// Цветовая палитра для светлой темы
|
||||||
palette: {
|
palette: {
|
||||||
mode: "light",
|
mode: "light", // Режим светлой темы
|
||||||
|
|
||||||
|
// Фоновые цвета
|
||||||
background: {
|
background: {
|
||||||
default: "#FFFFFF",
|
default: "#6CACE4", // Основной фон приложения
|
||||||
paper: "#FFFFFF",
|
paper: "#FFFFFF", // Фон "бумажных" поверхностей (карточек, панелей)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Текстовые цвета
|
||||||
text: {
|
text: {
|
||||||
primary: "#000000",
|
primary: "#000000", // Основной цвет текста
|
||||||
|
secondary: "#333333", // Вторичный цвет текста
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Основные цвета UI
|
||||||
primary: {
|
primary: {
|
||||||
main: "#3d74c7",
|
main: "#3d74c7", // Основной брендовый цвет
|
||||||
|
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Дополнительные цвета UI
|
||||||
secondary: {
|
secondary: {
|
||||||
main: "#0f55bec2",
|
main: "#0f55bec2", // Вторичный брендовый цвет
|
||||||
},
|
},
|
||||||
|
|
||||||
|
divider: "#e0e0e0", // Цвет разделителей
|
||||||
|
|
||||||
|
// Кастомные цвета для специфических элементов
|
||||||
custom: {
|
custom: {
|
||||||
background: "#FFFFFF",
|
background: "#D4EFFC", // Кастомный фоновый цвет
|
||||||
text: "#000000",
|
text: "#000000", // Кастомный цвет текста
|
||||||
sidebar: "#3d74c7",
|
sidebar: "#025EA1", // Фон боковой панели
|
||||||
sidebarText: "#FFFFFF",
|
sidebarText: "#FFFFFF", // Текст в боковой панели
|
||||||
modalBackground: "#FFFFFF",
|
sidebarHover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении в боковой панели
|
||||||
modalBtnBackground: "#0f55bec2",
|
modalBackground: "#FFFFFF", // Фон модальных окон
|
||||||
modalText: "#333333",
|
modalBtnBackground: "#0f55bec2", // Фон кнопок в модальных окнах
|
||||||
tableBorder: "#ddd",
|
modalText: "#333333", // Текст в модальных окнах
|
||||||
tableHeaderBackground: "#f9f9f9",
|
tableBorder: "#ddd", // Границы таблиц
|
||||||
tableCellBackground: "#FFFFFF",
|
tableHeaderBackground: "#f9f9f9", // Фон заголовков таблиц
|
||||||
tableText: "#000000",
|
tableCellBackground: "#FFFFFF", // Фон ячеек таблиц
|
||||||
treeChartText: "#000000",
|
tableText: "#000000", // Текст в таблицах
|
||||||
scrollbarTrack: "#f1f1f1",
|
treeChartText: "#000000", // Текст в древовидных диаграммах
|
||||||
hoverButton: "#2d62b1",
|
scrollbarTrack: "#f1f1f1", // Цвет трека скроллбара
|
||||||
hoverText: "#FFFFFF",
|
hoverButton: "#2d62b1", // Цвет кнопок при наведении
|
||||||
|
hoverText: "#FFFFFF", // Цвет текста при наведении
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Цвета для различных состояний
|
||||||
|
action: {
|
||||||
|
hover: "rgba(0, 0, 0, 0.04)", // Цвет при наведении на интерактивные элементы
|
||||||
|
selected: "rgba(0, 0, 0, 0.08)", // Цвет выбранных элементов
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Темная тема приложения
|
||||||
|
*/
|
||||||
export const darkTheme = createTheme({
|
export const darkTheme = createTheme({
|
||||||
|
...commonThemeSettings, // Распаковываем общие настройки
|
||||||
|
|
||||||
|
// Цветовая палитра для темной темы
|
||||||
palette: {
|
palette: {
|
||||||
mode: "dark",
|
mode: "dark", // Режим темной темы
|
||||||
|
|
||||||
|
// Фоновые цвета
|
||||||
background: {
|
background: {
|
||||||
default: "#1E1E1E",
|
default: "#1E1E1E", // Основной фон приложения
|
||||||
paper: "#2d2d2d",
|
paper: "#2d2d2d", // Фон "бумажных" поверхностей
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Текстовые цвета
|
||||||
text: {
|
text: {
|
||||||
primary: "#E0E0E0",
|
primary: "#E0E0E0", // Основной цвет текста
|
||||||
|
secondary: "#B0B0B0", // Вторичный цвет текста
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Основные цвета UI
|
||||||
primary: {
|
primary: {
|
||||||
main: "#2d2d2d",
|
main: "#3d74c7", // Основной брендовый цвет (может совпадать со светлой темой)
|
||||||
|
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Дополнительные цвета UI
|
||||||
secondary: {
|
secondary: {
|
||||||
main: "#333333",
|
main: "#0f55bec2", // Вторичный брендовый цвет
|
||||||
},
|
},
|
||||||
|
|
||||||
|
divider: "#444444", // Цвет разделителей
|
||||||
|
|
||||||
|
// Кастомные цвета для специфических элементов
|
||||||
custom: {
|
custom: {
|
||||||
background: "#1E1E1E",
|
background: "#1E1E1E", // Кастомный фоновый цвет
|
||||||
text: "#E0E0E0",
|
text: "#E0E0E0", // Кастомный цвет текста
|
||||||
sidebar: "#2d2d2d",
|
sidebar: "#2d2d2d", // Фон боковой панели
|
||||||
sidebarText: "#E0E0E0",
|
sidebarText: "#E0E0E0", // Текст в боковой панели
|
||||||
modalBackground: "#2d2d2d",
|
sidebarHover: "rgba(255, 255, 255, 0.16)", // Цвет при наведении в боковой панели
|
||||||
modalBtnBackground: "#333333",
|
modalBackground: "#2d2d2d", // Фон модальных окон
|
||||||
modalText: "#FFFFFF",
|
modalBtnBackground: "#333333", // Фон кнопок в модальных окнах
|
||||||
tableBorder: "#444444",
|
modalText: "#FFFFFF", // Текст в модальных окнах
|
||||||
tableHeaderBackground: "#2d2d2d",
|
tableBorder: "#444444", // Границы таблиц
|
||||||
tableCellBackground: "#333333",
|
tableHeaderBackground: "#2d2d2d", // Фон заголовков таблиц
|
||||||
tableText: "#E0E0E0",
|
tableCellBackground: "#333333", // Фон ячеек таблиц
|
||||||
treeChartText: "#FFFFFF",
|
tableText: "#E0E0E0", // Текст в таблицах
|
||||||
scrollbarTrack: "#333",
|
treeChartText: "#FFFFFF", // Текст в древовидных диаграммах
|
||||||
hoverButton: "#333d4d",
|
scrollbarTrack: "#333", // Цвет трека скроллбара
|
||||||
hoverText: "#E0E0E0",
|
hoverButton: "#333d4d", // Цвет кнопок при наведении
|
||||||
|
hoverText: "#E0E0E0", // Цвет текста при наведении
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Цвета для различных состояний
|
||||||
|
action: {
|
||||||
|
hover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении на интерактивные элементы
|
||||||
|
selected: "rgba(255, 255, 255, 0.16)", // Цвет выбранных элементов
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -83,7 +83,7 @@ button:focus-visible {
|
||||||
|
|
||||||
/* Фон скроллбара */
|
/* Фон скроллбара */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: var(--scrollbar-track-color, #f1f1f1);
|
background: var(--scrollbar-track-color, #025EA1);
|
||||||
/* Цвет фона */
|
/* Цвет фона */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
/* Скругление углов */
|
/* Скругление углов */
|
||||||
|
|
@ -91,7 +91,7 @@ button:focus-visible {
|
||||||
|
|
||||||
/* Ползунок */
|
/* Ползунок */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #3d74c7;
|
background: #D4EFFC;
|
||||||
/* Основной цвет */
|
/* Основной цвет */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
/* Скругляем края */
|
/* Скругляем края */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue