redesign and fix graphics
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
parent
ed2e03e202
commit
bd96278895
|
|
@ -1,9 +1,11 @@
|
||||||
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';
|
||||||
|
|
||||||
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
|
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
|
||||||
const [selectionStart, setSelectionStart] = useState(null);
|
const [selectionArea, setSelectionArea] = useState(null);
|
||||||
const [selectionEnd, setSelectionEnd] = useState(null);
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
|
const chartRef = useRef(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
const allTimes = Object.values(chartData)
|
const allTimes = Object.values(chartData)
|
||||||
.flat()
|
.flat()
|
||||||
|
|
@ -21,27 +23,48 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
||||||
|
|
||||||
const displayData = filteredData || data;
|
const displayData = filteredData || data;
|
||||||
|
|
||||||
const handleClick = (e) => {
|
// Блокировка выделения текста при перетаскивании
|
||||||
if (!e || !e.activeLabel) return;
|
useEffect(() => {
|
||||||
|
const handleSelectStart = (e) => {
|
||||||
const clickedTime = e.activeLabel;
|
if (isSelecting) {
|
||||||
|
e.preventDefault();
|
||||||
if (!selectionStart) {
|
|
||||||
setSelectionStart(clickedTime);
|
|
||||||
} else if (!selectionEnd) {
|
|
||||||
setSelectionEnd(clickedTime);
|
|
||||||
|
|
||||||
const startIndex = data.findIndex(point => point.time === selectionStart);
|
|
||||||
const endIndex = data.findIndex(point => point.time === clickedTime);
|
|
||||||
|
|
||||||
onRangeSelect({ startIndex, endIndex });
|
|
||||||
|
|
||||||
setSelectionStart(null);
|
|
||||||
setSelectionEnd(null);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Упрощенный Tooltip без указания instance
|
document.addEventListener('selectstart', handleSelectStart);
|
||||||
|
return () => document.removeEventListener('selectstart', handleSelectStart);
|
||||||
|
}, [isSelecting]);
|
||||||
|
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
if (!e || !e.activeLabel) return;
|
||||||
|
setIsSelecting(true);
|
||||||
|
setSelectionArea({ start: e.activeLabel, end: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
if (!selectionArea?.start || !e?.activeLabel) return;
|
||||||
|
setSelectionArea(prev => ({ ...prev, end: e.activeLabel }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsSelecting(false);
|
||||||
|
|
||||||
|
if (!selectionArea?.start || !selectionArea?.end) {
|
||||||
|
setSelectionArea(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = data.findIndex(point => point.time === selectionArea.start);
|
||||||
|
const endIndex = data.findIndex(point => point.time === selectionArea.end);
|
||||||
|
|
||||||
|
onRangeSelect({
|
||||||
|
startIndex: Math.min(startIndex, endIndex),
|
||||||
|
endIndex: Math.max(startIndex, endIndex)
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectionArea(null);
|
||||||
|
};
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload, label }) => {
|
const CustomTooltip = ({ active, payload, label }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -49,10 +72,15 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
||||||
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' }}>{`Время: ${label}`}</p>
|
||||||
<p>{`Значение: ${payload[0].value}`}</p>
|
{payload.map((item, index) => (
|
||||||
|
<p key={index} style={{ color: item.color }}>
|
||||||
|
{`${item.name}: ${item.value}`}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -60,12 +88,41 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
style={{ position: 'relative' }}
|
||||||
|
ref={containerRef}
|
||||||
|
className={isSelecting ? 'no-selection' : ''}
|
||||||
|
>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.no-selection {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||||
|
padding: '5px 10px',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
zIndex: 10,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
<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
|
||||||
|
|
@ -81,7 +138,6 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
||||||
content={<CustomTooltip />}
|
content={<CustomTooltip />}
|
||||||
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
|
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
|
||||||
/>
|
/>
|
||||||
{/* Убрали <Legend /> чтобы скрыть имена instance */}
|
|
||||||
{Object.keys(chartData).map((key, index) => (
|
{Object.keys(chartData).map((key, index) => (
|
||||||
<Line
|
<Line
|
||||||
key={key}
|
key={key}
|
||||||
|
|
@ -91,9 +147,18 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={false}
|
dot={false}
|
||||||
activeDot={{ r: 6 }}
|
activeDot={{ r: 6 }}
|
||||||
// Убрали name чтобы не отображалось в tooltip
|
name={key}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{selectionArea?.start && selectionArea?.end && (
|
||||||
|
<ReferenceArea
|
||||||
|
x1={selectionArea.start}
|
||||||
|
x2={selectionArea.end}
|
||||||
|
strokeOpacity={0.3}
|
||||||
|
fill="#4a6baf"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
else if (range <= 86400) step = 120;
|
else if (range <= 86400) step = 120;
|
||||||
else step = 300;
|
else step = 300;
|
||||||
|
|
||||||
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, {
|
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
|
||||||
params: {
|
params: {
|
||||||
metric: metricName,
|
metric: metricName,
|
||||||
start,
|
start,
|
||||||
|
|
@ -161,10 +161,17 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
const handleRangeChange = (event) => {
|
const handleRangeChange = (event) => {
|
||||||
const selectedValue = event.target.value;
|
const selectedValue = event.target.value;
|
||||||
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
|
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
|
||||||
setSelectedRange(range);
|
|
||||||
|
// Принудительно сбрасываем состояние
|
||||||
|
setSelectedGraphRange(null);
|
||||||
|
setFilteredData(null);
|
||||||
|
|
||||||
|
// Обновляем диапазон
|
||||||
|
setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение
|
||||||
setUseCustomRange(false);
|
setUseCustomRange(false);
|
||||||
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
|
|
||||||
setFilteredData(null); // Сбрасываем отфильтрованные данные
|
// Принудительно обновляем данные
|
||||||
|
fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCustomRangeChange = () => {
|
const handleCustomRangeChange = () => {
|
||||||
|
|
@ -173,6 +180,12 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
setFilteredData(null); // Сбрасываем отфильтрованные данные
|
setFilteredData(null); // Сбрасываем отфильтрованные данные
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetZoom = () => {
|
||||||
|
setSelectedGraphRange(null);
|
||||||
|
setFilteredData(null);
|
||||||
|
fetchData(); // Принудительно обновляем данные
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedGraphRange) {
|
if (selectedGraphRange) {
|
||||||
const { startIndex, endIndex } = selectedGraphRange;
|
const { startIndex, endIndex } = selectedGraphRange;
|
||||||
|
|
@ -236,7 +249,8 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
marginBottom: '15px'
|
marginBottom: '15px'
|
||||||
}}>
|
}}>
|
||||||
{/* Стандартные диапазоны */}
|
{/* Стандартные диапазоны */}
|
||||||
<div style={{ flex: '1 1 200px' }}>
|
<div style={{ flex: '1 1 200px', display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
|
||||||
|
<div style={{ flex: '1' }}>
|
||||||
<label htmlFor="time-range" style={{
|
<label htmlFor="time-range" style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
marginBottom: '5px',
|
marginBottom: '5px',
|
||||||
|
|
@ -262,6 +276,29 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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={{ flex: '1 1 300px' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|
@ -351,7 +388,7 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
Текущий диапазон: {useCustomRange
|
Текущий диапазон: {useCustomRange
|
||||||
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
|
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
|
||||||
: selectedRange.label}
|
: selectedRange.label}
|
||||||
</div>
|
</div >
|
||||||
|
|
||||||
{/* График */}
|
{/* График */}
|
||||||
<LineChartComponent
|
<LineChartComponent
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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,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",
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Table sx={{ width: '100%', tableLayout: 'fixed' }}>
|
||||||
|
<TableHead>
|
||||||
|
{/* Основной заголовок таблицы */}
|
||||||
|
<TableRow>
|
||||||
|
<TableCellWithTooltip
|
||||||
|
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)}
|
||||||
|
align="center"
|
||||||
|
title={data.title}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
padding: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatusIndicators status={data.status} />
|
||||||
|
<Typography component="span" variant="subtitle1" fontWeight="bold" noWrap>
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</Typography>
|
||||||
</th>
|
</TableCellWithTooltip>
|
||||||
</tr>
|
</TableRow>
|
||||||
<tr>{renderHeaders(filteredData)}</tr>
|
|
||||||
<tr>{renderSubHeaders(filteredData)}</tr>
|
{/* Строка с основными заголовками */}
|
||||||
</thead>
|
<TableRow>
|
||||||
<tbody>
|
{renderMainHeaders(filteredData)}
|
||||||
<tr className="tree-table-row">{renderData(filteredData)}</tr>
|
</TableRow>
|
||||||
</tbody>
|
|
||||||
</table>
|
{/* Строка с подзаголовками (которая пропала в предыдущей версии) */}
|
||||||
<button
|
<TableRow>
|
||||||
|
{renderSubHeaders(filteredData)}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
{renderDataCells(filteredData)}
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
onClick={() => setIsLogVisible(!isLogVisible)}
|
onClick={() => setIsLogVisible(!isLogVisible)}
|
||||||
className="toggle-log-button"
|
size="small"
|
||||||
style={{ marginTop: "10px" }}
|
sx={{ mt: 2 }}
|
||||||
>
|
>
|
||||||
{isLogVisible ? "Скрыть лог" : "Показать лог"}
|
{isLogVisible ? 'Скрыть историю изменения статусов' : 'Показать историю изменения статусов'}
|
||||||
</button>
|
</Button>
|
||||||
{isLogVisible && (
|
|
||||||
<div className="status-log">
|
<Collapse in={isLogVisible}>
|
||||||
<h3>Лог статусов</h3>
|
<Box sx={{
|
||||||
<ul>
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ export const lightTheme = createTheme({
|
||||||
main: "#0f55bec2",
|
main: "#0f55bec2",
|
||||||
},
|
},
|
||||||
custom: {
|
custom: {
|
||||||
background: "#FFFFFF",
|
background: "#025EA1",
|
||||||
text: "#000000",
|
text: "#000000",
|
||||||
sidebar: "#3d74c7",
|
sidebar: "#025EA1",
|
||||||
sidebarText: "#FFFFFF",
|
sidebarText: "#FFFFFF",
|
||||||
modalBackground: "#FFFFFF",
|
modalBackground: "#FFFFFF",
|
||||||
modalBtnBackground: "#0f55bec2",
|
modalBtnBackground: "#0f55bec2",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue