diff --git a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js index 8142d6520..a21332d45 100644 --- a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js +++ b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js @@ -37,7 +37,7 @@ const StyledWrapper = styled.div` overflow: hidden; display: flex; flex-direction: column; - min-height: 0; /* Important for proper flex behavior */ + min-height: 0; } .network-empty { @@ -68,47 +68,40 @@ const StyledWrapper = styled.div` flex-direction: column; height: 100%; overflow: hidden; - min-height: 0; /* Important for proper flex behavior */ + min-height: 0; position: relative; - } - .col-separator { - position: absolute; - top: 0; - bottom: 0; - width: 1px; - background: ${(props) => props.theme.console.border}; - pointer-events: none; - z-index: 2; + &.is-resizing { + cursor: col-resize; + user-select: none; + } } .requests-header { display: grid; - padding: 0; + flex-shrink: 0; background: ${(props) => props.theme.console.headerBg}; border-bottom: 1px solid ${(props) => props.theme.console.border}; font-size: 10px; color: ${(props) => props.theme.console.titleColor}; text-transform: uppercase; letter-spacing: 0.5px; - flex-shrink: 0; + + & > * { + min-width: 0; + overflow: hidden; + } .header-cell { display: flex; align-items: center; - justify-content: space-between; gap: 4px; padding: 4px 8px; cursor: pointer; user-select: none; - &:first-child { - padding-left: 16px; - } - - &:last-child { - padding-right: 16px; - } + &:first-child { padding-left: 16px; } + &:last-child { padding-right: 16px; } &:hover { color: ${(props) => props.theme.console.messageColor}; @@ -120,10 +113,7 @@ const StyledWrapper = styled.div` white-space: nowrap; } - svg { - flex-shrink: 0; - } - + svg { flex-shrink: 0; } } } @@ -131,48 +121,70 @@ const StyledWrapper = styled.div` flex: 1; overflow-y: auto; overflow-x: hidden; - min-height: 0; /* Important for proper scrolling */ + min-height: 0; } .request-row { display: grid; - padding: 0; cursor: pointer; transition: background-color 0.1s ease; font-size: ${(props) => props.theme.font.size.sm}; align-items: center; - &:hover { - background: ${(props) => props.theme.console.logHoverBg}; + & > * { + min-width: 0; + overflow: hidden; } + &:hover { background: ${(props) => props.theme.console.logHoverBg}; } + &.selected { background: ${(props) => props.theme.console.logHoverBg}; box-shadow: inset 3px 0 0 ${(props) => props.theme.console.checkboxColor}; } } - .request-method { - padding: 2px 8px 2px 16px; + .col-separator { + position: absolute; + top: 0; + bottom: 0; + width: 4px; + transform: translateX(-2px); + cursor: col-resize; + z-index: 3; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 1px; + height: 100%; + background: ${(props) => props.theme.sidebar.dragbar.border}; + } + + &:hover::after, + &.resizing::after { + background: ${(props) => props.theme.sidebar.dragbar.activeBorder}; + } } - .request-status { - padding: 2px 8px; - } + .request-method { padding: 2px 8px 2px 16px; } + .request-status { padding: 2px 8px; } .method-badge { display: inline-flex; align-items: center; - justify-content: start; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; - min-width: 45px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - .status-badge { - font-size: ${(props) => props.theme.font.size.sm}; - } + .status-badge { font-size: ${(props) => props.theme.font.size.sm}; } .request-domain { padding: 2px 8px; @@ -196,6 +208,9 @@ const StyledWrapper = styled.div` color: ${(props) => props.theme.console.timestampColor}; font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: ${(props) => props.theme.font.size.xs}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .request-duration { @@ -204,11 +219,12 @@ const StyledWrapper = styled.div` font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: ${(props) => props.theme.font.size.xs}; text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - .text-right { - text-align: right; - } + .text-right { text-align: right; } .request-size { padding: 2px 8px; @@ -216,6 +232,9 @@ const StyledWrapper = styled.div` font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: ${(props) => props.theme.font.size.xs}; text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } `; diff --git a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js index 4fae5745c..79d68dd3f 100644 --- a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js +++ b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js @@ -1,4 +1,5 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; +import { usePersistedState } from 'hooks/usePersistedState'; import { useSelector, useDispatch } from 'react-redux'; import { IconNetwork, @@ -8,17 +9,17 @@ import { import { setSelectedRequest } from 'providers/ReduxStore/slices/logs'; +import { useResizableColumns } from 'hooks/useResizableColumns'; import StyledWrapper from './StyledWrapper'; -import { getGridTemplate, getSeparatorPositions, sortRequests } from './utils'; +import { sortRequests } from './utils'; -// TODO: Columns will be resizable in the future, so width can be null (for auto) or a number (for fixed width) const COLUMNS = [ - { key: 'method', label: 'Method', width: 90, align: 'left' }, - { key: 'status', label: 'Status', width: 80, align: 'left' }, - { key: 'domain', label: 'Domain', width: 200, align: 'left' }, - { key: 'path', label: 'Path', width: null, align: 'left' }, - { key: 'time', label: 'Time', width: 100, align: 'left' }, - { key: 'duration', label: 'Duration', width: 120, align: 'right' }, + { key: 'method', label: 'Method', width: 80, align: 'left' }, + { key: 'status', label: 'Status', width: 70, align: 'left' }, + { key: 'domain', label: 'Domain', width: 180, align: 'left' }, + { key: 'path', label: 'Path', width: 300, align: 'left' }, + { key: 'time', label: 'Time', width: 110, align: 'left' }, + { key: 'duration', label: 'Duration', width: 100, align: 'right' }, { key: 'size', label: 'Size', width: 80, align: 'right' } ]; @@ -133,15 +134,27 @@ const RequestRow = ({ request, isSelected, onClick, gridTemplateColumns }) => { const NetworkTab = () => { const dispatch = useDispatch(); - const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); - const gridTemplateColumns = useMemo(() => getGridTemplate(COLUMNS), []); - const separatorPositions = useMemo(() => getSeparatorPositions(COLUMNS), []); + const [sortConfig, setSortConfig] = usePersistedState({ key: 'devtools-network-sort', default: { key: null, direction: null } }); + const [savedColWidths, setSavedColWidths] = usePersistedState({ key: 'devtools-network-col-widths', default: null }); + + const { + containerRef, + gridTemplateColumns, + separatorPositions, + resizingIdx, + handleResizeStart + } = useResizableColumns({ + defaultWidths: COLUMNS.map((c) => c.width), + initialWidths: savedColWidths, + minColWidth: 60, + onResizeEnd: setSavedColWidths + }); + const { networkFilters, selectedRequest } = useSelector((state) => state.logs); const collections = useSelector((state) => state.collections.collections); const allRequests = useMemo(() => { const requests = []; - collections.forEach((collection) => { if (collection.timeline) { collection.timeline @@ -155,7 +168,6 @@ const NetworkTab = () => { }); } }); - return requests.sort((a, b) => a.timestamp - b.timestamp); }, [collections]); @@ -166,15 +178,11 @@ const NetworkTab = () => { }); }, [allRequests, networkFilters]); - const handleRequestClick = (request) => { - dispatch(setSelectedRequest(request)); - }; + const handleRequestClick = (request) => dispatch(setSelectedRequest(request)); const handleHeaderClick = (key) => { setSortConfig((prev) => { - // If clicking a different column, start with ascending sort if (prev.key !== key) return { key, direction: 'asc' }; - if (prev.direction === 'asc') return { key, direction: 'desc' }; return { key: null, direction: null }; }); @@ -195,7 +203,7 @@ const NetworkTab = () => { Requests will appear here as you make API calls ) : ( -