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 (