redesign and fix graphics
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details

pull/29/head
DmitriyA 2025-03-27 10:09:58 -04:00
parent ed2e03e202
commit bd96278895
6 changed files with 465 additions and 226 deletions

View File

@ -1,9 +1,11 @@
import React, { useState } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from 'recharts';
import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null);
const [selectionArea, setSelectionArea] = useState(null);
const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null);
const containerRef = useRef(null);
const allTimes = Object.values(chartData)
.flat()
@ -21,27 +23,48 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
const displayData = filteredData || data;
const handleClick = (e) => {
// Блокировка выделения текста при перетаскивании
useEffect(() => {
const handleSelectStart = (e) => {
if (isSelecting) {
e.preventDefault();
}
};
document.addEventListener('selectstart', handleSelectStart);
return () => document.removeEventListener('selectstart', handleSelectStart);
}, [isSelecting]);
const handleMouseDown = (e) => {
if (!e || !e.activeLabel) return;
const clickedTime = e.activeLabel;
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);
}
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);
};
// Упрощенный Tooltip без указания instance
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
@ -49,10 +72,15 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc',
borderRadius: '4px'
borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`Время: ${label}`}</p>
<p>{`Значение: ${payload[0].value}`}</p>
{payload.map((item, index) => (
<p key={index} style={{ color: item.color }}>
{`${item.name}: ${item.value}`}
</p>
))}
</div>
);
}
@ -60,12 +88,41 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
};
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}>
<LineChart
data={displayData}
onClick={handleClick}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
ref={chartRef}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
@ -81,7 +138,6 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
content={<CustomTooltip />}
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
/>
{/* Убрали <Legend /> чтобы скрыть имена instance */}
{Object.keys(chartData).map((key, index) => (
<Line
key={key}
@ -91,9 +147,18 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
// Убрали name чтобы не отображалось в tooltip
name={key}
/>
))}
{selectionArea?.start && selectionArea?.end && (
<ReferenceArea
x1={selectionArea.start}
x2={selectionArea.end}
strokeOpacity={0.3}
fill="#4a6baf"
/>
)}
</LineChart>
</ResponsiveContainer>
</div>

View File

@ -92,7 +92,7 @@ const PrometheusChart = ({ metricName }) => {
else if (range <= 86400) step = 120;
else step = 300;
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, {
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
params: {
metric: metricName,
start,
@ -100,7 +100,7 @@ const PrometheusChart = ({ metricName }) => {
step
}
});
const result = response.data;
let metrics = Array.isArray(result) ? result : result.data || [];
@ -161,10 +161,17 @@ const PrometheusChart = ({ metricName }) => {
const handleRangeChange = (event) => {
const selectedValue = event.target.value;
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
setSelectedRange(range);
// Принудительно сбрасываем состояние
setSelectedGraphRange(null);
setFilteredData(null);
// Обновляем диапазон
setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение
setUseCustomRange(false);
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные
// Принудительно обновляем данные
fetchData();
};
const handleCustomRangeChange = () => {
@ -173,6 +180,12 @@ const PrometheusChart = ({ metricName }) => {
setFilteredData(null); // Сбрасываем отфильтрованные данные
};
const handleResetZoom = () => {
setSelectedGraphRange(null);
setFilteredData(null);
fetchData(); // Принудительно обновляем данные
};
useEffect(() => {
if (selectedGraphRange) {
const { startIndex, endIndex } = selectedGraphRange;
@ -236,32 +249,56 @@ const PrometheusChart = ({ metricName }) => {
marginBottom: '15px'
}}>
{/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px' }}>
<label htmlFor="time-range" style={{
display: 'block',
marginBottom: '5px',
fontWeight: '500',
color: '#555'
}}>Стандартные диапазоны:</label>
<select
id="time-range"
value={selectedRange.value}
onChange={handleRangeChange}
<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={{
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
padding: '8px 16px',
backgroundColor: '#f0f0f0',
color: '#333',
border: '1px solid #ddd',
color: "#333",
backgroundColor: '#f9f9f9'
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'}
>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
Сбросить
</button>
</div>
{/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}>
<div style={{
@ -351,7 +388,7 @@ const PrometheusChart = ({ metricName }) => {
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
</div>
</div >
{/* График */}
<LineChartComponent

View File

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

View File

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

View File

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

View File

@ -17,9 +17,9 @@ export const lightTheme = createTheme({
main: "#0f55bec2",
},
custom: {
background: "#FFFFFF",
background: "#025EA1",
text: "#000000",
sidebar: "#3d74c7",
sidebar: "#025EA1",
sidebarText: "#FFFFFF",
modalBackground: "#FFFFFF",
modalBtnBackground: "#0f55bec2",