mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-22 12:15:38 +00:00
feat(dev-tools-rquest-resize): dev tools details panel can be resized horizontally via a drag handle (#8234)
This commit is contained in:
committed by
GitHub
parent
e7e6cdfa51
commit
db195fe302
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
69
packages/bruno-app/src/hooks/useResizablePanel/index.js
Normal file
69
packages/bruno-app/src/hooks/useResizablePanel/index.js
Normal 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 };
|
||||
}
|
||||
192
packages/bruno-app/src/hooks/useResizablePanel/index.spec.js
Normal file
192
packages/bruno-app/src/hooks/useResizablePanel/index.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user