From ec30ef634c116f4f775be9bf694b4200451237d3 Mon Sep 17 00:00:00 2001 From: zivglik Date: Sun, 7 Jun 2026 13:38:59 +0300 Subject: [PATCH 1/2] Implement dynamic row height adjustment in trace viewer and span row components --- .../Tables/Jobs/Trace/ModernTraceViewer.jsx | 8 ++- src/Routes/Tables/Jobs/Trace/SpanRow.jsx | 36 ++++++++-- .../Tables/Jobs/Trace/traceConstants.js | 3 + .../Tables/Jobs/Trace/useTraceRowHeight.js | 65 +++++++++++++++++++ 4 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 src/Routes/Tables/Jobs/Trace/useTraceRowHeight.js diff --git a/src/Routes/Tables/Jobs/Trace/ModernTraceViewer.jsx b/src/Routes/Tables/Jobs/Trace/ModernTraceViewer.jsx index fe4453324..9f9a10ef3 100644 --- a/src/Routes/Tables/Jobs/Trace/ModernTraceViewer.jsx +++ b/src/Routes/Tables/Jobs/Trace/ModernTraceViewer.jsx @@ -15,6 +15,7 @@ import TraceTimeline from './TraceTimeline'; import TraceTimelineMinimap from './TraceTimelineMinimap'; import SpanRow from './SpanRow'; import TraceLogsModal from './TraceLogsModal'; +import { useTraceRowHeight } from './useTraceRowHeight'; import { getCurrentTheme, getSystemColors } from './traceConstants'; /* @@ -80,6 +81,7 @@ const ModernTraceViewer = ({ data }) => { const [minimapMode, setMinimapMode] = useState('highlight'); const [isDark, setIsDark] = useState(getCurrentTheme() === 'DARK'); const [logsModalContext, setLogsModalContext] = useState(null); + const { rowHeight, onRowHeightChange } = useTraceRowHeight(); const { kibanaUrl, @@ -224,9 +226,9 @@ const ModernTraceViewer = ({ data }) => { nodeRect.top - containerRect.top + container.scrollTop; const containerHeight = container.clientHeight; - const rowHeight = node.offsetHeight; + const currentRowHeight = node.offsetHeight; const scrollTarget = - rowTopRelativeToContainer - containerHeight / 2 + rowHeight / 2; + rowTopRelativeToContainer - containerHeight / 2 + currentRowHeight / 2; container.scrollTo({ top: Math.max(0, scrollTarget), @@ -343,6 +345,8 @@ const ModernTraceViewer = ({ data }) => { onOpenLogs={handleOpenLogs} onOpenKibana={handleOpenKibana} isKibanaConfigured={Boolean(kibanaUrl)} + rowHeight={rowHeight} + onRowHeightChange={onRowHeightChange} rowRef={node => registerSpanRef(span.spanID, node)} /> ))} diff --git a/src/Routes/Tables/Jobs/Trace/SpanRow.jsx b/src/Routes/Tables/Jobs/Trace/SpanRow.jsx index 9648a6b28..5d237e6ce 100644 --- a/src/Routes/Tables/Jobs/Trace/SpanRow.jsx +++ b/src/Routes/Tables/Jobs/Trace/SpanRow.jsx @@ -25,6 +25,7 @@ import { DEPTH_INDENT, MAX_DEPTH_INDENT, } from './traceConstants'; +import { useTraceRowResize } from './useTraceRowHeight'; const { Title, Text } = Typography; @@ -75,7 +76,7 @@ const SpanNameWrapper = styled.div` padding: 8px 12px; display: flex; align-items: center; - min-height: 36px; + min-height: ${props => props.$rowHeight}px; font-size: 14px; border-right: 1px solid ${props => { @@ -178,7 +179,7 @@ const OperationText = styled(Text)` const SpanBarContainer = styled.div` flex: 1; margin: 0 12px; - height: 36px; + height: ${props => props.$rowHeight}px; display: flex; align-items: center; position: relative; @@ -192,7 +193,7 @@ const SpanBarContainer = styled.div` const SpanBarTrack = styled.div` width: 100%; - height: 24px; + height: ${props => Math.max(props.$rowHeight - 12, 14)}px; background: ${props => { const colors = getSystemColors(props.$isDark); return props.$isDark ? '#1a2332' : colors.lightGrey; @@ -388,6 +389,16 @@ const KibanaIconWrap = styled.span` font-size: 17px; `; +const RowResizeHandle = styled.div` + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 6px; + cursor: row-resize; + z-index: 5; +`; + const getTagValue = (tags, keys) => { if (!Array.isArray(tags)) { return ''; @@ -614,6 +625,8 @@ const SpanRow = ({ onOpenLogs, onOpenKibana, isKibanaConfigured, + rowHeight, + onRowHeightChange, }) => { const [isHovered, setIsHovered] = useState(false); const [isTimelineHovered, setIsTimelineHovered] = useState(false); @@ -639,6 +652,11 @@ const SpanRow = ({ const relativeStart = (span.relativeStartTime / totalDuration) * 100; const width = Math.max((span.duration / totalDuration) * 100, 0.5); + const { startRowResize } = useTraceRowResize({ + rowHeight, + onRowHeightChange, + }); + const matchesSearch = !searchTerm || span.operationName.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -673,6 +691,7 @@ const SpanRow = ({ onToggle(span.spanID)} @@ -717,8 +736,9 @@ const SpanRow = ({ setIsTimelineHovered(true)} onMouseLeave={() => setIsTimelineHovered(false)} + $rowHeight={rowHeight} $isDark={isDark}> - + + {isExpanded && ( @@ -957,6 +983,8 @@ SpanRow.propTypes = { onOpenLogs: PropTypes.func, onOpenKibana: PropTypes.func, isKibanaConfigured: PropTypes.bool, + rowHeight: PropTypes.number.isRequired, + onRowHeightChange: PropTypes.func.isRequired, }; SpanRow.defaultProps = { diff --git a/src/Routes/Tables/Jobs/Trace/traceConstants.js b/src/Routes/Tables/Jobs/Trace/traceConstants.js index afdbdb69c..7a31a4cdc 100644 --- a/src/Routes/Tables/Jobs/Trace/traceConstants.js +++ b/src/Routes/Tables/Jobs/Trace/traceConstants.js @@ -100,6 +100,9 @@ export const LOGS_COL_WIDTH = 90; export const DEPTH_INDENT = 20; export const MAX_DEPTH_INDENT = 120; // caps at depth 6 effectively export const SPAN_LIST_MIN_HEIGHT = 200; +export const DEFAULT_TRACE_ROW_HEIGHT = 36; +export const MIN_TRACE_ROW_HEIGHT = 28; +export const MAX_TRACE_ROW_HEIGHT = 72; // Helper to detect current theme export const getCurrentTheme = () => { diff --git a/src/Routes/Tables/Jobs/Trace/useTraceRowHeight.js b/src/Routes/Tables/Jobs/Trace/useTraceRowHeight.js new file mode 100644 index 000000000..9453e5997 --- /dev/null +++ b/src/Routes/Tables/Jobs/Trace/useTraceRowHeight.js @@ -0,0 +1,65 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; +import { + DEFAULT_TRACE_ROW_HEIGHT, + MIN_TRACE_ROW_HEIGHT, + MAX_TRACE_ROW_HEIGHT, +} from './traceConstants'; + +const clampRowHeight = nextHeight => + Math.max(MIN_TRACE_ROW_HEIGHT, Math.min(MAX_TRACE_ROW_HEIGHT, nextHeight)); + +export const useTraceRowHeight = () => { + const [rowHeight, setRowHeight] = useState(DEFAULT_TRACE_ROW_HEIGHT); + + const onRowHeightChange = useCallback(nextHeight => { + setRowHeight(clampRowHeight(nextHeight)); + }, []); + + return { + rowHeight, + onRowHeightChange, + }; +}; + +export const useTraceRowResize = ({ rowHeight, onRowHeightChange }) => { + const resizeStateRef = useRef({ startY: 0, startHeight: rowHeight }); + + const onRowResize = useCallback( + event => { + const delta = event.clientY - resizeStateRef.current.startY; + onRowHeightChange(resizeStateRef.current.startHeight + delta); + }, + [onRowHeightChange] + ); + + const stopRowResize = useCallback(() => { + window.removeEventListener('mousemove', onRowResize); + window.removeEventListener('mouseup', stopRowResize); + }, [onRowResize]); + + const startRowResize = useCallback( + event => { + event.preventDefault(); + event.stopPropagation(); + resizeStateRef.current = { + startY: event.clientY, + startHeight: rowHeight, + }; + window.addEventListener('mousemove', onRowResize); + window.addEventListener('mouseup', stopRowResize); + }, + [onRowResize, rowHeight, stopRowResize] + ); + + useEffect( + () => () => { + window.removeEventListener('mousemove', onRowResize); + window.removeEventListener('mouseup', stopRowResize); + }, + [onRowResize, stopRowResize] + ); + + return { + startRowResize, + }; +}; From ba76083bafb860f785dee362370068d7d9fd984a Mon Sep 17 00:00:00 2001 From: zivglik Date: Mon, 8 Jun 2026 12:34:20 +0300 Subject: [PATCH 2/2] Refactor TimelineMarkers to align with responsive breakpoints and use shared constants for padding --- .../Tables/Jobs/Trace/TimelineMarkers.jsx | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/src/Routes/Tables/Jobs/Trace/TimelineMarkers.jsx b/src/Routes/Tables/Jobs/Trace/TimelineMarkers.jsx index eb834fa0b..f72a60b71 100644 --- a/src/Routes/Tables/Jobs/Trace/TimelineMarkers.jsx +++ b/src/Routes/Tables/Jobs/Trace/TimelineMarkers.jsx @@ -7,25 +7,20 @@ import { getCurrentTheme, getSystemColors, NAME_COL_WIDTH, - NAME_COL_PADDING, + METRICS_COL_WIDTH, + LOGS_COL_WIDTH, } from './traceConstants'; const { Text } = Typography; /* - * The left padding must equal the name column width plus the left padding of - * the TimelineHeader so the time markers sit directly above the timeline bar - * area — not above the service/operation label column. - * - * Previously this was hardcoded to 266px (250 + 16). Now it is derived from - * the shared constants so a single change in traceConstants keeps everything - * aligned automatically. + * Mirror TraceTimeline's column structure so markers are rendered only inside + * the Timeline column width while staying aligned on responsive breakpoints. */ const MarkersContainer = styled.div` display: flex; - justify-content: space-between; - padding: 8px ${NAME_COL_PADDING}px; - padding-left: ${NAME_COL_WIDTH + NAME_COL_PADDING}px; + align-items: center; + padding: 8px 16px; background: ${props => { const colors = getSystemColors(props.$isDark); return colors.cardBackground; @@ -37,6 +32,29 @@ const MarkersContainer = styled.div` }}; `; +const ServiceSpacer = styled.div` + flex: 0 0 ${NAME_COL_WIDTH}px; +`; + +const TimelineColumn = styled.div` + display: flex; + flex: 1; + justify-content: space-between; + min-width: 0; +`; + +const MetricsSpacer = styled.div` + flex: 0 0 ${METRICS_COL_WIDTH}px; + + @media (max-width: 500px) { + display: none; + } +`; + +const LogsSpacer = styled.div` + flex: 0 0 ${LOGS_COL_WIDTH}px; +`; + const MarkerText = styled(Text)` font-size: 11px; color: ${props => (props.$isDark ? '#b8bfc7' : '#4a4a4a')}; @@ -72,7 +90,14 @@ const TimelineMarkers = ({ duration }) => { ); } - return {markers}; + return ( + + + {markers} + + + + ); }; TimelineMarkers.propTypes = {