diff --git a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/DockModeSwitcher.tsx b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/DockModeSwitcher.tsx new file mode 100644 index 00000000000..017474405c8 --- /dev/null +++ b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/DockModeSwitcher.tsx @@ -0,0 +1,77 @@ +import { ReactNode } from 'react'; +import { Dock, PanelBottom, PanelRight } from '@signozhq/icons'; +import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group'; +import { + TooltipContent, + TooltipProvider, + TooltipRoot, + TooltipTrigger, +} from '@signozhq/ui/tooltip'; + +import { SpanDetailVariant } from './constants'; + +interface DockOption { + value: SpanDetailVariant; + icon: ReactNode; + tooltip: string; +} + +const DOCK_OPTIONS: DockOption[] = [ + { + value: SpanDetailVariant.DIALOG, + icon: , + tooltip: 'Open as floating panel', + }, + { + value: SpanDetailVariant.DOCKED, + icon: , + tooltip: 'Dock at the bottom', + }, + { + value: SpanDetailVariant.DOCKED_RIGHT, + icon: , + tooltip: 'Dock on the right', + }, +]; + +interface DockModeSwitcherProps { + value: SpanDetailVariant; + onChange: (value: SpanDetailVariant) => void; + tooltipClassName?: string; +} + +function DockModeSwitcher({ + value, + onChange, + tooltipClassName, +}: DockModeSwitcherProps): JSX.Element { + return ( + + { + if (v) { + onChange(v as SpanDetailVariant); + } + }} + size="sm" + > + {DOCK_OPTIONS.map((option) => ( + + + + {option.icon} + + + + {option.tooltip} + + + ))} + + + ); +} + +export default DockModeSwitcher; diff --git a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.module.scss b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.module.scss index 076355aa96d..741d01647af 100644 --- a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.module.scss +++ b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.module.scss @@ -3,6 +3,10 @@ display: flex; flex-direction: column; overflow: hidden; + + :global(.details-header) { + height: 39px; + } } .body { diff --git a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx index 03dc80a006f..f0aa945e6bd 100644 --- a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx +++ b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx @@ -1,25 +1,16 @@ import { useCallback, useMemo } from 'react'; -import { Button } from '@signozhq/ui/button'; import { TabsContent, TabsList, TabsRoot, TabsTrigger, } from '@signozhq/ui/tabs'; -import { - TooltipRoot, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@signozhq/ui/tooltip'; import { Bookmark, CalendarClock, ChartColumnBig, - Dock, Link2, List, - PanelBottom, ScrollText, Timer, } from '@signozhq/icons'; @@ -61,6 +52,7 @@ import { SpanDetailVariant, VISIBLE_ACTIONS, } from './constants'; +import DockModeSwitcher from './DockModeSwitcher'; import { useSpanAttributeActions } from './hooks/useSpanAttributeActions'; import { useTracePinnedFields } from './hooks/useTracePinnedFields'; import { @@ -492,31 +484,14 @@ function SpanDetailsPanel({ ]; if (onVariantChange) { - const isDocked = variant === SpanDetailVariant.DOCKED; actions.push({ - key: 'dock-toggle', + key: 'dock-mode', component: ( - - - - - - - {isDocked ? 'Open as floating panel' : 'Dock at the bottom'} - - - + ), }); } @@ -553,7 +528,10 @@ function SpanDetailsPanel({ ); - if (variant === SpanDetailVariant.DOCKED) { + if ( + variant === SpanDetailVariant.DOCKED || + variant === SpanDetailVariant.DOCKED_RIGHT + ) { return
{content}
; } diff --git a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/constants.ts b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/constants.ts index 96315a4245e..17cf0b38c61 100644 --- a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/constants.ts +++ b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/constants.ts @@ -22,6 +22,7 @@ export enum SpanDetailVariant { DRAWER = 'drawer', DIALOG = 'dialog', DOCKED = 'docked', + DOCKED_RIGHT = 'right', } export const KEY_ATTRIBUTE_KEYS: Record = { diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsV3.module.scss b/frontend/src/pages/TraceDetailsV3/TraceDetailsV3.module.scss index dd66e107ed6..940942d80c2 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceDetailsV3.module.scss +++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsV3.module.scss @@ -4,13 +4,28 @@ flex-direction: column; } +.layoutRow { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; +} + .content { display: flex; flex-direction: column; flex: 1; + min-width: 0; overflow: hidden; } +.rightDock { + display: flex; + flex-direction: column; + border-left: 1px solid var(--l2-border); + min-width: 0; +} + // Shared Ant Collapse chrome reset for both the flamegraph and waterfall // collapse panels. .flameCollapse, diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx index 7e5e3afdd70..ad28350a63c 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx +++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx @@ -893,7 +893,7 @@ function Success(props: ISuccessProps): JSX.Element { /> {/* Left panel - table with horizontal scroll */} (getLocalStorageKey( LOCALSTORAGE.TRACE_DETAILS_SPAN_DETAILS_POSITION, - ) as SpanDetailVariant) || SpanDetailVariant.DIALOG, + ) as SpanDetailVariant) || SpanDetailVariant.DOCKED_RIGHT, ); + const RIGHT_DOCK_MIN = 480; + const RIGHT_DOCK_MAX = 720; + const [rightDockWidth, setRightDockWidth] = useState(RIGHT_DOCK_MIN); + const handleVariantChange = useCallback( (newVariant: SpanDetailVariant): void => { setLocalStorageKey( @@ -291,7 +295,9 @@ function TraceDetailsV3(): JSX.Element { (!!errorFetchingTraceData || !traceData?.payload?.spans?.length); const isDocked = spanDetailVariant === SpanDetailVariant.DOCKED; + const isRightDocked = spanDetailVariant === SpanDetailVariant.DOCKED_RIGHT; const isWaterfallDocked = panelState.isOpen && isDocked; + const showRightDock = panelState.isOpen && isRightDocked; const waterfallChildren = ( ) : ( <> -
- k === 'flame')} - onChange={(): void => handleCollapseChange('flame')} - size="small" - className={styles.flameCollapse} - items={[ - { - key: 'flame', - label: ( -
- - Flame Graph - {traceData?.payload?.totalSpansCount && - traceData.payload.totalSpansCount > FLAMEGRAPH_SPAN_LIMIT && ( - - )} - - {traceData?.payload?.totalSpansCount ? ( - - - - Spans: {traceData.payload.totalSpansCount} - - 0, - })} - > - - Errors: {traceData.payload.totalErrorSpansCount ?? 0} - +
+
+ k === 'flame')} + onChange={(): void => handleCollapseChange('flame')} + size="small" + className={styles.flameCollapse} + items={[ + { + key: 'flame', + label: ( +
+ + Flame Graph + {traceData?.payload?.totalSpansCount && + traceData.payload.totalSpansCount > FLAMEGRAPH_SPAN_LIMIT && ( + + )} - ) : null} -
- ), - children: ( - - - - ), - }, - ]} - /> - - k === 'waterfall')} - onChange={(): void => handleCollapseChange('waterfall')} - size="small" - className={cx(styles.waterfallCollapse, { - [styles.isDocked]: isWaterfallDocked, - })} - items={[ - { - key: 'waterfall', - label: 'Waterfall', - children: activeKeys.includes('waterfall') ? waterfallChildren : null, - }, - ]} - /> - - {panelState.isOpen && isDocked && ( -
+ {traceData?.payload?.totalSpansCount ? ( + + + + Spans: {traceData.payload.totalSpansCount} + + 0, + })} + > + + Errors: {traceData.payload.totalErrorSpansCount ?? 0} + + + ) : null} +
+ ), + children: ( + + + + ), + }, + ]} + /> + + k === 'waterfall')} + onChange={(): void => handleCollapseChange('waterfall')} + size="small" + className={cx(styles.waterfallCollapse, { + [styles.isDocked]: isWaterfallDocked, + })} + items={[ + { + key: 'waterfall', + label: 'Waterfall', + children: activeKeys.includes('waterfall') + ? waterfallChildren + : null, + }, + ]} + /> + + {panelState.isOpen && isDocked && ( +
+ +
+ )} +
+ + {showRightDock && ( + -
+ )}
- {panelState.isOpen && !isDocked && ( + {panelState.isOpen && spanDetailVariant === SpanDetailVariant.DIALOG && ( (null); @@ -40,10 +47,13 @@ function ResizableBox({ const startSize = size; const min = isHorizontal ? minWidth : minHeight; const max = isHorizontal ? maxWidth : maxHeight; + // Start-edge handle: pointer moving away from content (negative delta) + // grows the box, so invert the sign. + const deltaSign = isStartHandle ? -1 : 1; const onMouseMove = (moveEvent: MouseEvent): void => { const currentPos = isHorizontal ? moveEvent.clientX : moveEvent.clientY; - const delta = currentPos - startPos; + const delta = (currentPos - startPos) * deltaSign; const newSize = Math.min(max, Math.max(min, startSize + delta)); setSize(newSize); onResize?.(newSize); @@ -61,7 +71,16 @@ function ResizableBox({ document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }, - [size, isHorizontal, minWidth, maxWidth, minHeight, maxHeight, onResize], + [ + size, + isHorizontal, + isStartHandle, + minWidth, + maxWidth, + minHeight, + maxHeight, + onResize, + ], ); const containerStyle = disabled @@ -69,7 +88,7 @@ function ResizableBox({ : isHorizontal ? { width: size } : { height: size }; - const handleClass = `resizable-box__handle resizable-box__handle--${direction}`; + const handleClass = `resizable-box__handle resizable-box__handle--${handle}`; return (