feat(dev-tools-rquest-resize): dev tools details panel can be resized horizontally via a drag handle (#8234)

This commit is contained in:
sachin-thakur-bruno
2026-06-12 18:10:02 +05:30
committed by GitHub
parent e7e6cdfa51
commit db195fe302
6 changed files with 329 additions and 8 deletions

View File

@@ -4,11 +4,8 @@ const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background: ${(props) => props.theme.console.contentBg};
border-left: 1px solid ${(props) => props.theme.console.border};
min-width: 400px;
max-width: 600px;
width: 40%;
overflow: hidden;
.panel-header {

View File

@@ -144,6 +144,41 @@ const StyledWrapper = styled.div`
gap: 4px;
}
.details-panel-wrapper {
position: relative;
flex-shrink: 0;
height: 100%;
display: flex;
}
div.details-drag-handle {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: col-resize;
background-color: transparent;
width: 6px;
position: absolute;
left: -3px;
top: 0;
z-index: 10;
transition: opacity 0.2s ease;
div.drag-request-border {
width: 1px;
height: 100%;
border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.border};
}
&:hover div.drag-request-border {
width: 1px;
height: 100%;
border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.activeBorder};
}
}
.action-controls {
display: flex;
align-items: center;

View File

@@ -23,7 +23,8 @@ import {
setActiveTab,
clearDebugErrors,
updateNetworkFilter,
toggleAllNetworkFilters
toggleAllNetworkFilters,
updateRequestDetailsPanelWidth
} from 'providers/ReduxStore/slices/logs';
import NetworkTab from './NetworkTab';
@@ -33,6 +34,10 @@ import RequestDetailsPanel from './RequestDetailsPanel';
import ErrorDetailsPanel from './ErrorDetailsPanel';
import Performance from '../Performance';
import StyledWrapper from './StyledWrapper';
import { useResizablePanel } from 'hooks/useResizablePanel';
const MIN_DETAILS_PANEL_WIDTH = 280;
const MAX_DETAILS_PANEL_WIDTH = 800;
const LogIcon = ({ type }) => {
const iconProps = { size: 16, strokeWidth: 1.5 };
@@ -381,8 +386,17 @@ const Console = () => {
const dispatch = useDispatch();
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
const savedDetailsPanelWidth = useSelector((state) => state.logs.requestDetailsPanelWidth);
const consoleRef = useRef(null);
const { width: detailsPanelWidth, handleDragStart: handleDetailsPanelDragStart } = useResizablePanel({
initialWidth: savedDetailsPanelWidth,
minWidth: MIN_DETAILS_PANEL_WIDTH,
maxWidth: MAX_DETAILS_PANEL_WIDTH,
direction: 'right',
onResizeEnd: (newWidth) => dispatch(updateRequestDetailsPanelWidth({ requestDetailsPanelWidth: newWidth }))
});
const logCounts = logs.reduce((counts, log) => {
counts[log.type] = (counts[log.type] || 0) + 1;
return counts;
@@ -614,7 +628,16 @@ const Console = () => {
<div className="network-main">
{renderTabContent()}
</div>
<RequestDetailsPanel />
<div className="details-panel-wrapper" style={{ width: detailsPanelWidth }}>
<div
className="details-drag-handle"
onMouseDown={handleDetailsPanelDragStart}
data-testid="details-panel-drag-handle"
>
<div className="drag-request-border" />
</div>
<RequestDetailsPanel />
</div>
</div>
) : activeTab === 'debug' && selectedError ? (
<div className="debug-with-details">

View File

@@ -0,0 +1,69 @@
import { useEffect, useRef, useState } from 'react';
/**
* Drag-to-resize behavior for a side panel.
*
* @param {object} options
* @param {number} options.initialWidth - Starting width in px
* @param {number} options.minWidth - Minimum allowed width in px
* @param {number} options.maxWidth - Maximum allowed width in px
* @param {'left' | 'right'} options.direction - Panel side. 'right' means dragging
* left expands the panel; 'left' means dragging right expands it.
* @param {function} [options.onResizeEnd] - Called with the final width on mouseup
*
* @returns {{ width: number, handleDragStart: function }}
*/
export function useResizablePanel({
initialWidth,
minWidth,
maxWidth,
direction = 'left',
onResizeEnd
}) {
const [width, setWidth] = useState(initialWidth);
const isDragging = useRef(false);
const dragStartX = useRef(0);
const dragStartWidth = useRef(0);
const currentWidth = useRef(initialWidth);
const clamp = (w) => Math.min(maxWidth, Math.max(minWidth, w));
const handleDragStart = (e) => {
isDragging.current = true;
dragStartX.current = e.clientX;
dragStartWidth.current = currentWidth.current;
e.preventDefault();
};
const handleMouseMove = (e) => {
if (!isDragging.current) return;
const delta
= direction === 'right'
? dragStartX.current - e.clientX // drag left = expand
: e.clientX - dragStartX.current; // drag right = expand
const newWidth = clamp(dragStartWidth.current + delta);
currentWidth.current = newWidth;
setWidth(newWidth);
};
const handleMouseUp = () => {
if (isDragging.current) {
if (onResizeEnd) onResizeEnd(currentWidth.current);
}
isDragging.current = false;
};
useEffect(() => {
// Note: tying the events to the document instead of the parent to avoid fast movement
// from breaking the flow state of dragging
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, []);
return { width, handleDragStart };
}

View File

@@ -0,0 +1,192 @@
const { describe, it, expect, jest } = require('@jest/globals');
import { renderHook, act } from '@testing-library/react';
import { useResizablePanel } from './index';
const MIN_WIDTH = 280;
const MAX_WIDTH = 800;
const INITIAL_WIDTH = 400;
const renderResizablePanel = ({
initialWidth = INITIAL_WIDTH,
direction = 'right',
onResizeEnd = jest.fn()
} = {}) => {
const result = renderHook(
({ initialWidth, direction, onResizeEnd }) =>
useResizablePanel({
initialWidth,
minWidth: MIN_WIDTH,
maxWidth: MAX_WIDTH,
direction,
onResizeEnd
}),
{ initialProps: { initialWidth, direction, onResizeEnd } }
);
return { ...result, onResizeEnd };
};
const fireMouse = (type, clientX) => {
act(() => {
document.dispatchEvent(new MouseEvent(type, { clientX, bubbles: true }));
});
};
describe('useResizablePanel', () => {
it('returns the initial width on first render', () => {
const { result } = renderResizablePanel();
expect(result.current.width).toBe(INITIAL_WIDTH);
});
it('handleDragStart does not change width', () => {
const { result } = renderResizablePanel();
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
expect(result.current.width).toBe(INITIAL_WIDTH);
});
it('mousemove without an active drag is a no-op', () => {
const { result } = renderResizablePanel();
fireMouse('mousemove', 300);
expect(result.current.width).toBe(INITIAL_WIDTH);
});
it('mouseup without an active drag does not call onResizeEnd', () => {
const { result, onResizeEnd } = renderResizablePanel();
fireMouse('mouseup', 300);
expect(onResizeEnd).not.toHaveBeenCalled();
});
describe('direction: right', () => {
it('dragging left increases width', () => {
const { result } = renderResizablePanel({ direction: 'right' });
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', 400); // moved 100px left → delta = +100
expect(result.current.width).toBe(INITIAL_WIDTH + 100);
});
it('dragging right decreases width', () => {
const { result } = renderResizablePanel({ direction: 'right' });
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', 600); // moved 100px right → delta = -100
expect(result.current.width).toBe(INITIAL_WIDTH - 100);
});
});
describe('direction: left', () => {
it('dragging right increases width', () => {
const { result } = renderResizablePanel({ direction: 'left' });
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', 600); // moved 100px right → delta = +100
expect(result.current.width).toBe(INITIAL_WIDTH + 100);
});
it('dragging left decreases width', () => {
const { result } = renderResizablePanel({ direction: 'left' });
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', 400); // moved 100px left → delta = -100
expect(result.current.width).toBe(INITIAL_WIDTH - 100);
});
});
it('clamps width to minWidth', () => {
const { result } = renderResizablePanel({ direction: 'right' });
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', 1000); // large rightward move → would go below minWidth
expect(result.current.width).toBe(MIN_WIDTH);
});
it('clamps width to maxWidth', () => {
const { result } = renderResizablePanel({ direction: 'right' });
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', -1000); // large leftward move → would exceed maxWidth
expect(result.current.width).toBe(MAX_WIDTH);
});
it('mouseup calls onResizeEnd with the final width', () => {
const { result, onResizeEnd } = renderResizablePanel({
direction: 'right'
});
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', 400);
fireMouse('mouseup', 400);
expect(onResizeEnd).toHaveBeenCalledTimes(1);
expect(onResizeEnd).toHaveBeenCalledWith(INITIAL_WIDTH + 100);
});
it('stops updating width after mouseup', () => {
const { result } = renderResizablePanel({ direction: 'right' });
act(() => {
result.current.handleDragStart({
clientX: 500,
preventDefault: jest.fn()
});
});
fireMouse('mousemove', 400);
fireMouse('mouseup', 400);
fireMouse('mousemove', 200); // further move after release — should be ignored
expect(result.current.width).toBe(INITIAL_WIDTH + 100);
});
});

View File

@@ -33,7 +33,8 @@ const initialState = {
selectedRequest: null,
selectedError: null,
maxLogs: 1000,
maxDebugErrors: 500
maxDebugErrors: 500,
requestDetailsPanelWidth: 400
};
export const logsSlice = createSlice({
@@ -127,6 +128,9 @@ export const logsSlice = createSlice({
},
clearSelectedError: (state) => {
state.selectedError = null;
},
updateRequestDetailsPanelWidth: (state, action) => {
state.requestDetailsPanelWidth = action.payload.requestDetailsPanelWidth;
}
}
});
@@ -146,7 +150,8 @@ export const {
setSelectedRequest,
clearSelectedRequest,
setSelectedError,
clearSelectedError
clearSelectedError,
updateRequestDetailsPanelWidth
} = logsSlice.actions;
export default logsSlice.reducer;