Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
58 changes: 47 additions & 11 deletions web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ import { FeatureSet } from './features';
import { createGridColumns, GRID_STATE } from './utils';


// The row count above which a grid is worth virtualising. It scales with
// the visible column count rather than being a flat row count, since
// render cost tracks total cells (rows * cols), not rows alone: formula
// and bounds from VIBVEL47's PR #10146. A grid reporting no columns yet
// gets a middling default rather than the 25 row floor, so that a grid
// still settling its columns is not virtualised on the strength of a
// momentary zero.
export function getVirtualiseThreshold(visibleColCount) {
if(!visibleColCount) return 100;

return Math.min(400, Math.max(25, Math.round(700 / visibleColCount)));
}


export default function DataGridView({
field, viewHelperProps, accessPath, dataDispatch, containerClassName
}) {
Expand Down Expand Up @@ -122,12 +136,27 @@ export default function DataGridView({
)
).includes(true);

// Virtualising a small grid buys nothing (there's no offscreen window to
// skip rendering) but still pays for measureElement's per-row
// getBoundingClientRect on every mount/remeasure. That remeasure is
// exactly what fires when a dialog tab holding the grid is hidden via
// `display: none` and then shown again, since the scroll viewport
// momentarily measures 0 and the virtualizer's ResizeObserver treats
// that as a real resize. Below the threshold we skip virtualisation
// entirely and render every row in normal document flow, so showing a
// hidden tab is a pure CSS toggle again.
const visibleColCount = table.getVisibleLeafColumns().length;
const virtualiseThreshold = viewHelperProps.virtualiseThreshold ??
getVirtualiseThreshold(visibleColCount);
const shouldVirtualise = rows.length > virtualiseThreshold;

const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableEleRef.current,
estimateSize: () => 50,
measureElement:
typeof window !== 'undefined' &&
shouldVirtualise &&
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? element => element?.getBoundingClientRect().height
: undefined,
Expand All @@ -152,22 +181,29 @@ export default function DataGridView({
ref={tableEleRef} table={table} data-test="data-grid-view"
tableClassName='DataGridView-table'>
<PgReactTableHeader table={table} />
<PgReactTableBody style={{
height: virtualizer.getTotalSize() + 'px'
}}>
<PgReactTableBody style={
shouldVirtualise ? {height: virtualizer.getTotalSize() + 'px'} : undefined
}>
{
virtualizer.getVirtualItems().map((virtualRow) => {
(
shouldVirtualise
? virtualizer.getVirtualItems()
: rows.map((_row, index) => ({index, start: 0}))
).map((virtualRow) => {
const row = rows[virtualRow.index];
return (
<PgReactTableRow
key={row.id}
data-index={virtualRow.index}
ref={node => virtualizer.measureElement(node)}
style={{
// This should always be a `style` as it changes on
// scroll.
transform: `translateY(${virtualRow.start}px)`,
}}
ref={shouldVirtualise ? node => virtualizer.measureElement(node) : undefined}
className={shouldVirtualise ? undefined : 'pgrt-row--static'}
style={
shouldVirtualise ? {
// This should always be a `style` as it changes
// on scroll.
transform: `translateY(${virtualRow.start}px)`,
} : undefined
}
>
<GridRow
rowId={virtualRow.index} isResizing={isResizing}
Expand Down
8 changes: 8 additions & 0 deletions web/pgadmin/static/js/components/PgReactTableStyled.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ const StyledDiv = styled('div')(({theme})=>({
position: 'absolute',
width: '100%',

// Opted out of the virtualizer's absolute positioning for grids
// small enough that virtualisation isn't used. Keeps the row in
// normal document flow so a hidden/shown dialog tab is a pure CSS
// toggle instead of triggering a virtualizer remeasure.
'&.pgrt-row--static': {
position: 'static',
},

'& .pgrt-row-content': {
display: 'flex',
minHeight: 0,
Expand Down
40 changes: 40 additions & 0 deletions web/regression/javascript/SchemaView/SchemaDialogView.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,46 @@ describe('SchemaView', ()=>{
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[1], 'rval51');
expect(ctrl.container.querySelector('[data-test="notifier-message"]')).toHaveTextContent('Field5 in FieldColl must be unique.');
});

it('does not virtualise a small grid, rendering rows in static flow', async ()=>{
await simulateValidData();

const dataRows = ctrl.container.querySelectorAll('[data-test="data-table-row"]');
expect(dataRows.length).toBe(2);

// Every row should be fully mounted and opted out of the
// virtualizer's absolute positioning, so a hidden dialog tab is a
// pure CSS toggle rather than something the virtualizer has to
// remeasure when the tab is shown again.
const pgrtRows = ctrl.container.querySelectorAll('.pgrt-row');
expect(pgrtRows.length).toBe(2);
pgrtRows.forEach((rowEl)=>{
expect(rowEl.classList.contains('pgrt-row--static')).toBe(true);
expect(rowEl.style.transform).toBe('');
});
});

it('virtualises a large grid, mounting only a window of rows', async ()=>{
// Clearing the default threshold would need several hundred rows,
// which is slow enough to time out on the Windows runners, so the
// threshold is pinned low instead and the scaling of the default
// is covered directly by getVirtualiseThreshold's own tests.
const manyRows = Array.from({length: 60}, (_, i)=>(
{field3: i, field4: 'field4val', field5: `field5val${i}`}
));

await ctrlMount({
viewHelperProps: {mode: 'create', virtualiseThreshold: 25},
getInitData: ()=>Promise.resolve({fieldcoll: manyRows}),
});

const pgrtRows = ctrl.container.querySelectorAll('.pgrt-row');
expect(pgrtRows.length).toBeGreaterThan(0);
expect(pgrtRows.length).toBeLessThan(manyRows.length);
pgrtRows.forEach((rowEl)=>{
expect(rowEl.classList.contains('pgrt-row--static')).toBe(false);
});
});
});

describe('SQL tab', ()=>{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import { getVirtualiseThreshold }
from 'sources/SchemaView/DataGridView/grid';

describe('getVirtualiseThreshold', ()=>{
it('scales inversely with the visible column count', ()=>{
expect(getVirtualiseThreshold(4)).toBe(175);
expect(getVirtualiseThreshold(7)).toBe(100);
expect(getVirtualiseThreshold(14)).toBe(50);
});

it('never goes above 400, however few the columns', ()=>{
expect(getVirtualiseThreshold(1)).toBe(400);
expect(getVirtualiseThreshold(2)).toBe(350);
});

it('never goes below 25, however many the columns', ()=>{
expect(getVirtualiseThreshold(28)).toBe(25);
expect(getVirtualiseThreshold(200)).toBe(25);
});

it('uses a middling default when no columns are reported yet', ()=>{
expect(getVirtualiseThreshold(0)).toBe(100);
expect(getVirtualiseThreshold(undefined)).toBe(100);
});
});
Loading