Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/trace-viewer/src/ui/networkFilters.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@

.network-filters {
display: flex;
gap: 16px;
background-color: var(--vscode-sideBar-background);
padding: 4px 8px;
min-height: 32px;
}

.network-filters input[type="search"] {
padding: 0 5px;
margin-right: 8px;
}

.network-filters-resource-types {
display: flex;
gap: 8px;
align-items: center;
width: 100%
}

.network-filters-resource-type {
Expand All @@ -41,6 +41,10 @@
text-overflow: ellipsis;
}

.network-filters-resource-types .tabbed-pane-tab {
min-width: 50px;
}

.network-filters-resource-type.selected {
background-color: var(--vscode-list-inactiveSelectionBackground);
}
22 changes: 12 additions & 10 deletions packages/trace-viewer/src/ui/networkFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { TabbedPane } from '@web/components/tabbedPane';
import './networkFilters.css';

const resourceTypes = ['All', 'Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image'] as const;
Expand Down Expand Up @@ -41,16 +42,17 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: {
/>

<div className='network-filters-resource-types'>
{resourceTypes.map(resourceType => (
<div
key={resourceType}
title={resourceType}
onClick={() => onFilterStateChange({ ...filterState, resourceType })}
className={`network-filters-resource-type ${filterState.resourceType === resourceType ? 'selected' : ''}`}
>
{resourceType}
</div>
))}
<TabbedPane
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other places that use TabbedPane? Do they upgrade to the new functionality for free?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I experimented with another component where this worked like a charm. Reference commit: Zemotacqy@8df5e25

tabs={resourceTypes.map(resourceType => ({
id: resourceType,
title: resourceType,
render: () => <></>
}))}
selectedTab={filterState.resourceType}
setSelectedTab={tabId => onFilterStateChange({ ...filterState, resourceType: tabId as ResourceType })}
mode='default'
overflowMode='select'
/>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/networkResourceDetails.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
.tab-network .toolbar {
min-height: 30px !important;
background-color: initial !important;
border-bottom: 1px solid var(--vscode-panel-border);
}

.tab-network .toolbar::after {
Expand All @@ -77,6 +76,7 @@

.tab-network .tabbed-pane-tab.selected {
font-weight: bold;
background-color: var(--vscode-list-inactiveSelectionBackground) !important;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is !important required? If necessary, the reason should be documented, but I think it's probably not necessary.

Copy link
Contributor Author

@Zemotacqy Zemotacqy Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, that we started using TabbedPane component. So, style:

.tabbed-pane-tab.selected {
background-color: var(--vscode-tab-activeBackground);
}
was overriding styles in networkFilters.css.

Which was a white background selection instead of blue background on selection.

So, to honour the blue background selection, added this important here. This style gets applied only on network tab selection on hover.

Seems necessary. Based on the reasoning, do you think I should add a comment?

}

.copy-request-dropdown {
Expand Down
163 changes: 145 additions & 18 deletions packages/web/src/components/tabbedPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,164 @@ export const TabbedPane: React.FunctionComponent<{
setSelectedTab?: (tab: string) => void,
dataTestId?: string,
mode?: 'default' | 'select',
}> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar, dataTestId, mode }) => {
overflowMode?: 'none' | 'select'
}> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar, dataTestId, mode, overflowMode }) => {
const id = React.useId();
if (!selectedTab)
selectedTab = tabs[0].id;
if (!mode)
mode = 'default';
return <div className='tabbed-pane' data-testid={dataTestId}>
if (!overflowMode)
overflowMode = 'none';

const containerRef = React.useRef<HTMLDivElement>(null);
const [visibleTabs, setVisibleTabs] = React.useState<TabbedPaneTabModel[]>(mode !== 'select' ? tabs : []);
const [overflowTabs, setOverflowTabs] = React.useState<TabbedPaneTabModel[]>(mode === 'select' ? tabs : []);
const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
const [, setContainerWidth] = React.useState<number>(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you using this for rerender management? Why does it matter, given you're also setting other state every time this is set? Those other sets will always be new/unique as well, so they will also cause a rerender.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @agg23 , This makes sense. I verified, containerWidth wasn't to re-render other states. It was only used for calculation of available space. Have pushed a commit to remove it usage.

const [tabbedPaneWidth, setTabbedPaneWidth] = React.useState<number>(0);

// Initial measurements
const measureContainerTabs = React.useCallback(() => {
const container = containerRef.current;
if (!container)
return;

const containerWidth = container.getBoundingClientRect().width;
setContainerWidth(containerWidth);

const tabWidths: Record<string, number> = {};
const tabbedPaneTabs = container.querySelectorAll('.tabbed-pane-tab');
tabbedPaneTabs.forEach(tabbedPane => {
const element = tabbedPane as HTMLElement;
if (element && element.title)
tabWidths[element.title] = tabbedPane.scrollWidth;
});
setTabWidths(tabWidths);

// For width calculation: Assume the dropdown width is 1.5x of the rightmost menu item
const tabWidthValues = Object.values(tabWidths);
const tabbedPaneWidth = 1.5 * tabWidthValues[tabWidthValues.length - 1] || 0;
if (tabbedPaneWidth > 0)
setTabbedPaneWidth(tabbedPaneWidth);
}, []);

const calculateVisibleTabCount = React.useCallback((availableWidth: number, tabWidths: Record<string, number>, tabs: TabbedPaneTabModel[]): number => {
let requiredWidth = 0;

for (const tabWidth of Object.values(tabWidths)) {
requiredWidth += tabWidth;
if (requiredWidth > availableWidth) {
// Overflow detected, calculate how many tabs can fit
let visibleCount = 0;
let cumulativeWidth = 0;

for (let index = 0; index < tabs.length; index++) {
const tab = tabs[index];
const tabWidth = tabWidths[tab.title];
cumulativeWidth += tabWidth;

if (cumulativeWidth > availableWidth) {
visibleCount = index;
break;
}
}
return visibleCount;
}
}

return tabs.length;
}, []);

const adjustElementRenderings = React.useCallback(() => {
const container = containerRef.current;
if (!container)
return;

const containerWidth = container.getBoundingClientRect().width;
setContainerWidth(containerWidth);

const initialAvailableWidth = containerWidth - (overflowTabs.length > 0 ? tabbedPaneWidth : 0);
const finalAvailableWidth = containerWidth - tabbedPaneWidth;

const visibleCount = calculateVisibleTabCount(initialAvailableWidth, tabWidths, tabs) === tabs.length
? tabs.length
: calculateVisibleTabCount(finalAvailableWidth, tabWidths, tabs);

const visibleTabsList = tabs.slice(0, visibleCount);
const overflowTabsList = tabs.slice(visibleCount);

setVisibleTabs(visibleTabsList);
setOverflowTabs(overflowTabsList);
}, [tabWidths, overflowTabs, tabbedPaneWidth, tabs, calculateVisibleTabCount]);

// Initial measurement and setup
React.useEffect(() => {
if (overflowMode !== 'select')
return;

measureContainerTabs();
}, [measureContainerTabs, overflowMode]);

// Adjust when Tab widths change
React.useEffect(() => {
if (overflowMode !== 'select')
return;

if (overflowTabs.length > 0)
adjustElementRenderings();
}, [adjustElementRenderings, overflowMode, overflowTabs.length]);

React.useEffect(() => {
if (overflowMode !== 'select')
return;

const container = containerRef.current;
if (!container)
return;

const handleResize = () => {
setTimeout(adjustElementRenderings, 0);
};

const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(container);
handleResize();

return () => {
resizeObserver.disconnect();
};
}, [adjustElementRenderings, overflowMode]);

return <div className='tabbed-pane' data-testid={dataTestId} ref={containerRef}>
<div className='vbox'>
<Toolbar>
{ leftToolbar && <div style={{ flex: 'none', display: 'flex', margin: '0 4px', alignItems: 'center' }}>
{...leftToolbar}
</div>}
{mode === 'default' && <div style={{ flex: 'auto', display: 'flex', height: '100%', overflow: 'hidden' }} role='tablist'>
{[...tabs.map(tab => (
<TabbedPaneTab
key={tab.id}
id={tab.id}
ariaControls={`${id}-${tab.id}`}
title={tab.title}
count={tab.count}
errorCount={tab.errorCount}
selected={selectedTab === tab.id}
onSelect={setSelectedTab}
/>)),
]}
{visibleTabs.length > 0 && <div style={{ flex: 'auto', display: 'flex', height: '100%', overflow: 'hidden' }} role='tablist'>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does mode continue to work in the same way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. mode continues to work in the same way because by default every tab is set as visible tab here:

const [visibleTabs, setVisibleTabs] = React.useState<TabbedPaneTabModel[]>(mode !== 'select' ? tabs : []);
const [overflowTabs, setOverflowTabs] = React.useState<TabbedPaneTabModel[]>(mode === 'select' ? tabs : []);

{visibleTabs.map(visibleTab => {
const tab = tabs.find(t => t.id === visibleTab.id) || visibleTab;
return (
<TabbedPaneTab
key={tab.id}
id={tab.id}
ariaControls={`${id}-${tab.id}`}
title={tab.title}
count={tab.count}
errorCount={tab.errorCount}
selected={selectedTab === tab.id}
onSelect={setSelectedTab}
/>
);
})}
</div>}
{mode === 'select' && <div style={{ flex: 'auto', display: 'flex', height: '100%', overflow: 'hidden' }} role='tablist'>
{overflowTabs.length > 0 && <div style={{ flex: 'auto', display: 'flex', height: '100%', overflow: 'hidden' }} role='tablist'>
<select style={{ width: '100%', background: 'none', cursor: 'pointer' }} value={selectedTab} onChange={e => {
setSelectedTab?.(tabs[e.currentTarget.selectedIndex].id);
setSelectedTab?.(overflowTabs[e.currentTarget.selectedIndex].id);
}}>
{tabs.map(tab => {
{overflowTabs.map(overflowTab => {
const tab = tabs.find(t => t.id === overflowTab.id) || overflowTab;
let suffix = '';
if (tab.count)
suffix = ` (${tab.count})`;
Expand Down
22 changes: 22 additions & 0 deletions tests/playwright-test/ui-mode-test-network-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,25 @@ test('should not duplicate network entries from beforeAll', {
await page.getByText('Network', { exact: true }).click();
await expect(page.getByTestId('network-list').getByText('empty.html')).toHaveCount(1);
});

test('should render network tabs with overflow dropdown', async ({ runUITest, server }) => {
const { page } = await runUITest({
'network-tab.test.ts': `
import { test, expect } from '@playwright/test';
test('network tab test', async ({ page }) => {
await page.goto('${server.PREFIX}/network-tab/network.html');
await page.evaluate(() => (window as any).donePromise);
});
`,
});

await page.getByText('network tab test').dblclick();
await page.getByText('Network', { exact: true }).click();

const sidebar = await page.locator('.network-filters-resource-types');
await sidebar.evaluate(element => {
element.style.width = '300px';
});

await expect(page.locator('.network-filters-resource-types .tabbed-pane-tab')).toHaveCount(4);
});
Loading