Skip to content

Commit ec08b6a

Browse files
committed
Fixed the review comments
1 parent 1531ff6 commit ec08b6a

2 files changed

Lines changed: 79 additions & 179 deletions

File tree

web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx

Lines changed: 4 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ const StyledBox = styled(Box)(({theme}) => ({
5050
},
5151
}));
5252

53-
const PK_COLUMN_NAMES = ['id', 'oid'];
54-
5553
function parseEwkbData(rows, column) {
5654
let key = column.key;
5755
const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB
@@ -194,41 +192,6 @@ function parseData(rows, columns, column) {
194192
};
195193
}
196194

197-
// Find primary key column i.e a column with unique values from columns array in Data Output tab
198-
function findPkColumn(columns) {
199-
return columns.find(c => PK_COLUMN_NAMES.includes(c.name));
200-
}
201-
202-
// Hash function for row objects
203-
function hashRow(row) {
204-
const str = Object.keys(row).sort().map(k => `${k}:${row[k]}`).join('|');
205-
let hash = 0;
206-
for (let i = 0; i < str.length; i++) {
207-
const char = str.charCodeAt(i);
208-
hash = ((hash << 5) - hash) + char;
209-
hash = hash & hash;
210-
}
211-
return `hash_${hash}`;
212-
}
213-
214-
// Get unique row identifier using PK column or first column
215-
function getRowIdentifier(row, pkColumn, columns) {
216-
if (pkColumn?.key && row[pkColumn.key] !== undefined) {
217-
return row[pkColumn.key];
218-
}
219-
const firstKey = columns[0]?.key;
220-
if (firstKey && row[firstKey] !== undefined) {
221-
return row[firstKey];
222-
}
223-
return hashRow(row);
224-
}
225-
226-
// Match rows from previous selection to current rows
227-
function matchRowSelection(prevIdentifiers, currentRows, pkColumn, columns) {
228-
if (prevIdentifiers.size === 0) return [];
229-
230-
return currentRows.filter(row => prevIdentifiers.has(getRowIdentifier(row, pkColumn, columns)));
231-
}
232195

233196
function PopupTable({data}) {
234197

@@ -485,86 +448,8 @@ export function GeometryViewer({rows, columns, column}) {
485448
const contentRef = React.useRef();
486449
const queryToolCtx = React.useContext(QueryToolContext);
487450

488-
// Track previous column state AND selected row data
489-
const prevStateRef = React.useRef({
490-
columnKey: null,
491-
columnNames: null,
492-
selectedRowIdentifiers: new Set(),
493-
});
494-
495-
const [mapKey, setMapKey] = React.useState(0);
496451
const currentColumnKey = useMemo(() => column?.key, [column]);
497-
const currentColumnNames = React.useMemo(
498-
() => columns.map(c => c.key).sort().join(','),
499-
[columns]
500-
);
501-
502-
const pkColumn = useMemo(() => findPkColumn(columns), [columns]);
503-
504-
// Detect when to clear, filter, or re-render the map based on changes in geometry column, columns list, or rows
505-
useEffect(() => {
506-
const prevState = prevStateRef.current;
507-
508-
if (!currentColumnKey) {
509-
setMapKey(prev => prev + 1);
510-
prevStateRef.current = {
511-
columnKey: null,
512-
columnNames: null,
513-
selectedRowIdentifiers: new Set(),
514-
};
515-
return;
516-
}
517-
518-
if (currentColumnKey !== prevState.columnKey ||
519-
currentColumnNames !== prevState.columnNames) {
520-
setMapKey(prev => prev + 1);
521-
prevStateRef.current = {
522-
columnKey: currentColumnKey,
523-
columnNames: currentColumnNames,
524-
selectedRowIdentifiers: new Set(rows.map(r => getRowIdentifier(r, pkColumn, columns))),
525-
};
526-
return;
527-
}
528-
529-
if (currentColumnKey === prevState.columnKey &&
530-
currentColumnNames === prevState.columnNames &&
531-
rows.length > 0) {
532-
prevStateRef.current.selectedRowIdentifiers = new Set(
533-
displayRows.map(r => getRowIdentifier(r, pkColumn, columns))
534-
);
535-
}
536-
}, [currentColumnKey, currentColumnNames, rows, pkColumn, columns]);
537-
538-
// Get rows to display based on selection
539-
const displayRows = React.useMemo(() => {
540-
// No geometry column selected or no rows available - nothing to display
541-
if (!currentColumnKey || rows.length === 0) return [];
542-
const prevState = prevStateRef.current;
543-
544-
// Column context changed (different geometry column or different query schema)
545-
// Show all new rows since previous selection is no longer valid
546-
if (currentColumnKey !== prevState.columnKey || currentColumnNames !== prevState.columnNames) {
547-
return rows;
548-
}
549-
550-
const prevIdentifiers = prevState.selectedRowIdentifiers;
551-
// No previous selection recorded - show all rows
552-
if (prevIdentifiers.size === 0) return rows;
553-
554-
// Previous selection was a subset of total rows, meaning user had specific rows selected.
555-
// Try to match those previously selected rows in the new result set using stable
556-
// row identifiers (PK value, first column value, or hash fallback).
557-
// This handles the case where same query reruns with more/fewer rows
558-
if (prevIdentifiers.size < rows.length) {
559-
const matched = matchRowSelection(prevIdentifiers, rows, pkColumn, columns);
560-
// If matched rows found, show only those; otherwise fall back to all rows
561-
return matched.length > 0 ? matched : rows;
562-
}
563-
// Previous selection covered all rows (or same count) - show all current rows
564-
return rows;
565-
}, [rows, currentColumnKey, currentColumnNames, pkColumn, columns]);
566452

567-
// Parse geometry data only when needed
568453
const data = React.useMemo(() => {
569454
if (!currentColumnKey) {
570455
const hasGeometryColumn = columns.some(c => c.cell === 'geometry' || c.cell === 'geography');
@@ -577,8 +462,8 @@ export function GeometryViewer({rows, columns, column}) {
577462
: [gettext('No spatial data found. At least one geometry or geography column is required for visualization.')],
578463
};
579464
}
580-
return parseData(displayRows, columns, column);
581-
}, [displayRows, columns, column, currentColumnKey]);
465+
return parseData(rows, columns, column);
466+
}, [rows, columns, column, currentColumnKey]);
582467

583468
useEffect(()=>{
584469
let timeoutId;
@@ -599,9 +484,9 @@ export function GeometryViewer({rows, columns, column}) {
599484
};
600485
}, [queryToolCtx]);
601486

602-
// Dynamic CRS is not supported. Use srid and mapKey as key and recreate the map on change
487+
// Dynamic CRS is not supported. Use srid and column key as key and recreate the map on change
603488
return (
604-
<StyledBox ref={contentRef} width="100%" height="100%" key={`${data.selectedSRID}-${mapKey}`}>
489+
<StyledBox ref={contentRef} width="100%" height="100%" key={`${data.selectedSRID}-${currentColumnKey || 'none'}`}>
605490
<MapContainer
606491
crs={data.selectedSRID === 4326 ? CRS.EPSG3857 : CRS.Simple}
607492
zoom={2} center={[20, 100]}

web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx

Lines changed: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -876,12 +876,14 @@ export function ResultSet() {
876876
rsu.current.setLoaderText = setLoaderText;
877877

878878
const isDataChangedRef = useRef(false);
879-
const prevRowsRef = React.useRef(null);
880-
const prevColumnsRef = React.useRef(null);
881-
const gvClearedForColumnsRef = useRef(null);
882879
const lastGvSelectionRef = useRef({
883-
type: 'all', // 'all' | 'rows' | 'columns'
884-
selectedColumns: new Set(),
880+
type: 'all', // 'all' | 'rows' | 'columns' | 'range' | 'cell'
881+
geometryColumnKey: null,
882+
rowIndices: [],
883+
columnIndices: new Set(),
884+
rangeStartIdx: null,
885+
rangeEndIdx: null,
886+
cellIdx: null,
885887
});
886888

887889
useEffect(()=>{
@@ -1468,86 +1470,99 @@ export function ResultSet() {
14681470
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows);
14691471
}, [columns, selectedRows.size]);
14701472

1471-
const getFilteredRowsForGeometryViewer = React.useCallback((useLastGvSelection = false) => {
1472-
let selRowsData = rows;
1473-
if(selectedRows.size != 0) {
1474-
selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r)));
1475-
} else if(selectedColumns.size > 0) {
1476-
let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1));
1477-
selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key)));
1478-
} else if(useLastGvSelection && lastGvSelectionRef.current.type === 'columns'
1479-
&& lastGvSelectionRef.current.selectedColumns.size > 0) {
1480-
let selectedCols = _.filter(columns, (_c, i)=>lastGvSelectionRef.current.selectedColumns.has(i+1));
1481-
if(selectedCols.length > 0) {
1482-
selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key)));
1483-
}
1484-
} else if(selectedRange.current) {
1485-
let [,, startRowIdx, endRowIdx] = getRangeIndexes();
1486-
selRowsData = rows.slice(startRowIdx, endRowIdx+1);
1487-
} else if(selectedCell.current?.[0]) {
1488-
selRowsData = [selectedCell.current[0]];
1489-
}
1490-
return selRowsData;
1491-
}, [rows, columns, selectedRows, selectedColumns]);
1492-
14931473
const openGeometryViewerTab = React.useCallback((column, rowsData) => {
14941474
layoutDocker.openTab({
14951475
id: PANELS.GEOMETRY,
14961476
title: gettext('Geometry Viewer'),
1497-
content: <GeometryViewer rows={rowsData} columns={columns} column={column} />,
1477+
content: <GeometryViewer rows={rowsData} columns={columns} column={column}/>,
14981478
closable: true,
14991479
}, PANELS.MESSAGES, 'after-tab', true);
15001480
}, [layoutDocker, columns]);
15011481

1502-
// Handle manual Geometry Viewer opening
1482+
// Handle manual Geometry Viewer opening.
1483+
// Determines which rows to plot based on the current grid selection (rows, columns,
1484+
// range, or cell) and stores the selection indices in lastGvSelectionRef so the
1485+
// auto-update effect can re-apply the same selection on subsequent query re-runs.
15031486
useEffect(()=>{
15041487
const renderGeometries = (column)=>{
1505-
gvClearedForColumnsRef.current = null;
1488+
const defaultSel = { geometryColumnKey: column?.key, rowIndices: [], columnIndices: new Set(), rangeStartIdx: null, rangeEndIdx: null, cellIdx: null };
1489+
let selRowsData = rows;
1490+
15061491
if(selectedRows.size > 0) {
1507-
lastGvSelectionRef.current = { type: 'rows', selectedColumns: new Set() };
1492+
// Specific rows selected in the grid — plot only those rows
1493+
const rowIndices = [];
1494+
rows.forEach((r, i) => {
1495+
if(selectedRows.has(rowKeyGetter(r))) {
1496+
rowIndices.push(i);
1497+
}
1498+
});
1499+
selRowsData = rowIndices.map(i => rows[i]);
1500+
lastGvSelectionRef.current = { ...defaultSel, type: 'rows', rowIndices };
15081501
} else if(selectedColumns.size > 0) {
1509-
lastGvSelectionRef.current = { type: 'columns', selectedColumns: new Set(selectedColumns) };
1502+
// Specific columns selected — plot all rows but only with selected column data
1503+
let selectedCols = _.filter(columns, (_c, i) => selectedColumns.has(i + 1));
1504+
selRowsData = _.map(rows, (r) => _.pick(r, _.map(selectedCols, (c) => c.key)));
1505+
lastGvSelectionRef.current = { ...defaultSel, type: 'columns', columnIndices: new Set(selectedColumns) };
1506+
} else if(selectedRange.current) {
1507+
// Cell range selected — plot the rows within the range
1508+
let [,, startRowIdx, endRowIdx] = getRangeIndexes();
1509+
selRowsData = rows.slice(startRowIdx, endRowIdx + 1);
1510+
lastGvSelectionRef.current = { ...defaultSel, type: 'range', rangeStartIdx: startRowIdx, rangeEndIdx: endRowIdx };
1511+
} else if(selectedCell.current?.[0]) {
1512+
// Single cell selected — plot only that row
1513+
const cellIdx = rows.indexOf(selectedCell.current[0]);
1514+
selRowsData = [selectedCell.current[0]];
1515+
lastGvSelectionRef.current = { ...defaultSel, type: 'cell', cellIdx: cellIdx >= 0 ? cellIdx : null };
15101516
} else {
1511-
lastGvSelectionRef.current = { type: 'all', selectedColumns: new Set() };
1517+
// No selection — plot all rows
1518+
lastGvSelectionRef.current = { ...defaultSel, type: 'all' };
15121519
}
1513-
const selRowsData = getFilteredRowsForGeometryViewer();
1520+
15141521
openGeometryViewerTab(column, selRowsData);
15151522
};
15161523
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
15171524
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
1518-
}, [getFilteredRowsForGeometryViewer, openGeometryViewerTab, eventBus, selectedRows, selectedColumns]);
1525+
}, [openGeometryViewerTab, eventBus, rows, columns, selectedRows, selectedColumns]);
15191526

15201527
// Auto-update Geometry Viewer when rows/columns change
15211528
useEffect(()=>{
1522-
const rowsChanged = prevRowsRef.current !== rows;
1523-
const columnsChanged = prevColumnsRef.current !== columns;
1524-
const currentGeometryColumn = columns.find(col => col.cell === 'geometry' || col.cell === 'geography');
1525-
1526-
if((rowsChanged || columnsChanged) && layoutDocker.isTabOpen(PANELS.GEOMETRY)) {
1527-
1528-
const prevColumnNames = prevColumnsRef.current?.map(c => c.key).sort().join(',') ?? '';
1529-
const currColumnNames = columns.map(c => c.key).sort().join(',');
1530-
const columnsChanged = prevColumnNames !== currColumnNames;
1531-
1532-
if(columnsChanged && currentGeometryColumn) {
1533-
gvClearedForColumnsRef.current = currColumnNames;
1534-
lastGvSelectionRef.current = { type: 'all', selectedColumns: new Set() };
1535-
openGeometryViewerTab(null, []);
1536-
} else if(gvClearedForColumnsRef.current === currColumnNames) {
1537-
openGeometryViewerTab(null, []);
1538-
} else if(currentGeometryColumn && rowsChanged) {
1539-
const useColSelection = lastGvSelectionRef.current.type === 'columns';
1540-
const selRowsData = getFilteredRowsForGeometryViewer(useColSelection);
1541-
openGeometryViewerTab(currentGeometryColumn, selRowsData);
1529+
if(layoutDocker.isTabOpen(PANELS.GEOMETRY)) {
1530+
const lastGeomKey = lastGvSelectionRef.current.geometryColumnKey;
1531+
const matchedGeomCol = lastGeomKey
1532+
? columns.find(c => c.key === lastGeomKey && (c.cell === 'geometry' || c.cell === 'geography'))
1533+
: null;
1534+
1535+
if(matchedGeomCol) {
1536+
// Previously plotted geometry column still exists → re-apply selection and re-render
1537+
const lastSel = lastGvSelectionRef.current;
1538+
let selRowsData = rows;
1539+
1540+
if(lastSel.type === 'rows' && lastSel.rowIndices.length > 0) {
1541+
if(lastSel.rowIndices.every(idx => idx < rows.length)) {
1542+
selRowsData = lastSel.rowIndices.map(idx => rows[idx]);
1543+
}
1544+
} else if(lastSel.type === 'columns' && lastSel.columnIndices.size > 0) {
1545+
let selectedCols = _.filter(columns, (_c, i) => lastSel.columnIndices.has(i + 1));
1546+
if(selectedCols.length > 0) {
1547+
selRowsData = _.map(rows, (r) => _.pick(r, _.map(selectedCols, (c) => c.key)));
1548+
}
1549+
} else if(lastSel.type === 'range' && lastSel.rangeStartIdx != null) {
1550+
if(lastSel.rangeStartIdx < rows.length && lastSel.rangeEndIdx < rows.length) {
1551+
selRowsData = rows.slice(lastSel.rangeStartIdx, lastSel.rangeEndIdx + 1);
1552+
}
1553+
} else if(lastSel.type === 'cell' && lastSel.cellIdx != null) {
1554+
if(lastSel.cellIdx < rows.length) {
1555+
selRowsData = [rows[lastSel.cellIdx]];
1556+
}
1557+
}
1558+
1559+
openGeometryViewerTab(matchedGeomCol, selRowsData);
15421560
} else {
1543-
// No geometry column
1561+
// Previously plotted geometry column not found → clear GV
15441562
openGeometryViewerTab(null, []);
15451563
}
15461564
}
1547-
1548-
prevRowsRef.current = rows;
1549-
prevColumnsRef.current = columns;
1550-
}, [rows, columns, getFilteredRowsForGeometryViewer, layoutDocker]);
1565+
}, [rows, columns, layoutDocker]);
15511566

15521567
const triggerResetScroll = () => {
15531568
// Reset the scroll position to previously saved location.

0 commit comments

Comments
 (0)