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
50 changes: 39 additions & 11 deletions web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,33 @@ 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.
//
// The threshold scales with 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.
const visibleColCount = table.getVisibleLeafColumns().length;
const virtualiseThreshold = viewHelperProps.virtualiseThreshold ??
(visibleColCount > 0
? Math.min(400, Math.max(25, Math.round(700 / visibleColCount)))
: 100);
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 +173,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
38 changes: 38 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,44 @@ 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 ()=>{
// The default threshold scales with visible column count (capped at
// 400), so this needs to comfortably clear that cap regardless of
// how many columns FieldColl renders.
const manyRows = Array.from({length: 450}, (_, i)=>(
{field3: i, field4: 'field4val', field5: `field5val${i}`}
));

await ctrlMount({
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
Loading