From eddc9ce85d2a6a14f97e8d7cedea02640becba4f Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Wed, 18 Feb 2026 11:27:16 +0530 Subject: [PATCH 1/4] Fixed an issue where Geometry Viewer was showing stale data and not auto-updating on query reruns or new query runs with new data or different geometry columns in Query tool. #9392 --- .../js/components/sections/GeometryViewer.jsx | 97 +++++++++++++++++-- .../js/components/sections/ResultSet.jsx | 72 ++++++++++---- 2 files changed, 144 insertions(+), 25 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 8842d6d287c..5aa12ee35e5 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -436,25 +436,110 @@ export function GeometryViewer({rows, columns, column}) { const mapRef = React.useRef(); const contentRef = React.useRef(); - const data = parseData(rows, columns, column); const queryToolCtx = React.useContext(QueryToolContext); + // Track previous state to detect changes + const prevStateRef = React.useRef({ + columnKey: null, + columnNames: null, + selectedRowPKs: [], + }); + + const [mapKey, setMapKey] = React.useState(0); + const currentColumnKey = column?.key; + const currentColumnNames = React.useMemo( + () => columns.map(c => c.key).sort().join(','), + [columns] + ); + + // Detect when to clear, filter, or re-render the map based on changes in geometry column, columns list, or rows + useEffect(() => { + const prevState = prevStateRef.current; + + if (!currentColumnKey) { + setMapKey(prev => prev + 1); + prevStateRef.current = { + columnKey: null, + columnNames: null, + selectedRowPKs: [], + }; + return; + } + + if (currentColumnKey !== prevState.columnKey || + currentColumnNames !== prevState.columnNames) { + setMapKey(prev => prev + 1); + prevStateRef.current = { + columnKey: currentColumnKey, + columnNames: currentColumnNames, + selectedRowPKs: [], + }; + return; + } + + if (currentColumnKey === prevState.columnKey && + currentColumnNames === prevState.columnNames && + rows.length > 0) { + + // If user previously selected specific rows, filter them from new data + if (prevState.selectedRowPKs.length > 0 && prevState.selectedRowPKs.length < rows.length) { + const newSelectedPKs = rows + .filter(row => prevState.selectedRowPKs.includes(row.__temp_PK)) + .map(row => row.__temp_PK); + + prevStateRef.current.selectedRowPKs = newSelectedPKs.length > 0 ? newSelectedPKs : rows.map(r => r.__temp_PK); + } else { + // All rows are displayed + const allPKs = rows.map(r => r.__temp_PK); + prevStateRef.current.selectedRowPKs = allPKs; + } + } + }, [currentColumnKey, currentColumnNames, rows]); + + const displayRows = React.useMemo(() => { + if (!currentColumnKey || rows.length === 0) return []; + + const selectedPKs = prevStateRef.current.selectedRowPKs; + return selectedPKs.length > 0 && selectedPKs.length < rows.length + ? rows.filter(row => selectedPKs.includes(row.__temp_PK)) + : rows; + }, [rows, currentColumnKey]); + + // Parse geometry data only when needed + const data = React.useMemo(() => { + if (!currentColumnKey) { + return { + 'geoJSONs': [], + 'selectedSRID': 0, + 'getPopupContent': undefined, + 'infoList': [gettext('Select a geometry/geography column to visualize.')], + }; + } + return parseData(displayRows, columns, column); + }, [displayRows, columns, column, currentColumnKey]); + useEffect(()=>{ let timeoutId; const contentResizeObserver = new ResizeObserver(()=>{ clearTimeout(timeoutId); - if(queryToolCtx.docker.isTabVisible(PANELS.GEOMETRY)) { + if(queryToolCtx?.docker?.isTabVisible(PANELS.GEOMETRY)) { timeoutId = setTimeout(function () { mapRef.current?.invalidateSize(); }, 100); } }); - contentResizeObserver.observe(contentRef.current); - }, []); + if(contentRef.current) { + contentResizeObserver.observe(contentRef.current); + } + return () => { + clearTimeout(timeoutId); + contentResizeObserver.disconnect(); + }; + }, [queryToolCtx]); - // Dyanmic CRS is not supported. Use srid as key and recreate the map on change + // Dyanmic CRS is not supported. Use srid and mapKey as key and recreate the map on change return ( - + { isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted)); }, [dataChangeStore]); @@ -1460,30 +1463,61 @@ export function ResultSet() { return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows); }, [columns, selectedRows.size]); + const getFilteredRowsForGeometryViewer = React.useCallback(() => { + let selRowsData = rows; + if(selectedRows.size != 0) { + selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r))); + } else if(selectedColumns.size > 0) { + let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1)); + selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key))); + } else if(selectedRange.current) { + let [,, startRowIdx, endRowIdx] = getRangeIndexes(); + selRowsData = rows.slice(startRowIdx, endRowIdx+1); + } else if(selectedCell.current?.[0]) { + selRowsData = [selectedCell.current[0]]; + } + return selRowsData; + }, [rows, columns, selectedRows, selectedColumns]); + + const openGeometryViewerTab = React.useCallback((column, rowsData) => { + layoutDocker.openTab({ + id: PANELS.GEOMETRY, + title: gettext('Geometry Viewer'), + content: , + closable: true, + }, PANELS.MESSAGES, 'after-tab', true); + }, [layoutDocker, columns]); + + // Handle manual Geometry Viewer opening useEffect(()=>{ const renderGeometries = (column)=>{ - let selRowsData = rows; - if(selectedRows.size != 0) { - selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r))); - } else if(selectedColumns.size > 0) { - let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1)); - selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key))); - } else if(selectedRange.current) { - let [,, startRowIdx, endRowIdx] = getRangeIndexes(); - selRowsData = rows.slice(startRowIdx, endRowIdx+1); - } else if(selectedCell.current?.[0]) { - selRowsData = [selectedCell.current[0]]; - } - layoutDocker.openTab({ - id: PANELS.GEOMETRY, - title:gettext('Geometry Viewer'), - content: , - closable: true, - }, PANELS.MESSAGES, 'after-tab', true); + const selRowsData = getFilteredRowsForGeometryViewer(); + openGeometryViewerTab(column, selRowsData); }; eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); - }, [rows, columns, selectedRows.size, selectedColumns.size]); + }, [getFilteredRowsForGeometryViewer, openGeometryViewerTab, eventBus]); + + // Auto-update Geometry Viewer when rows/columns change + useEffect(()=>{ + const rowsChanged = prevRowsRef.current !== rows; + const columnsChanged = prevColumnsRef.current !== columns; + const currentGeometryColumn = columns.find(col => col.cell === 'geometry' || col.cell === 'geography'); + + if((rowsChanged || columnsChanged) && layoutDocker.isTabOpen(PANELS.GEOMETRY)) { + + if(currentGeometryColumn) { + const selRowsData = getFilteredRowsForGeometryViewer(); + openGeometryViewerTab(currentGeometryColumn, selRowsData); + } else { + // No geometry column + openGeometryViewerTab(null, []); + } + } + + prevRowsRef.current = rows; + prevColumnsRef.current = columns; + }, [rows, columns, getFilteredRowsForGeometryViewer, openGeometryViewerTab, layoutDocker]); const triggerResetScroll = () => { // Reset the scroll position to previously saved location. From a047a51e0a031ce77a5c1ea0d85a7a5133e6e0d2 Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Wed, 18 Feb 2026 18:47:04 +0530 Subject: [PATCH 2/4] Fixed coderabbit comments and removed the __temp_PK to identifier based of first column or rowdata --- .../js/components/sections/GeometryViewer.jsx | 80 +++++++++++++------ 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 5aa12ee35e5..78301f49b73 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -6,7 +6,7 @@ // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { styled } from '@mui/material/styles'; import ReactDOMServer from 'react-dom/server'; import _ from 'lodash'; @@ -49,6 +49,8 @@ const StyledBox = styled(Box)(({theme}) => ({ }, })); +const PK_COLUMN_NAMES = ['id', 'oid', 'ctid']; + function parseEwkbData(rows, column) { let key = column.key; const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB @@ -191,6 +193,33 @@ function parseData(rows, columns, column) { }; } +// Find primary key column from columns array +function findPkColumn(columns) { + return columns.find(c => PK_COLUMN_NAMES.includes(c.name)); +} + +// Get unique row identifier using PK column or first column +function getRowIdentifier(row, pkColumn, columns) { + if (pkColumn?.key && row[pkColumn.key] !== undefined) { + return row[pkColumn.key]; + } + const firstKey = columns[0]?.key; + return firstKey && row[firstKey] !== undefined ? row[firstKey] : JSON.stringify(row); +} + +// Create Set of row identifiers +function createIdentifierSet(rowData, pkColumn, columns) { + return new Set(rowData.map(row => getRowIdentifier(row, pkColumn, columns))); +} + +// Match rows from previous selection to current rows +function matchRowSelection(prevRowData, currentRows, pkColumn, columns) { + if (prevRowData.length === 0) return []; + + const prevIdSet = createIdentifierSet(prevRowData, pkColumn, columns); + return currentRows.filter(row => prevIdSet.has(getRowIdentifier(row, pkColumn, columns))); +} + function PopupTable({data}) { return ( @@ -438,20 +467,22 @@ export function GeometryViewer({rows, columns, column}) { const contentRef = React.useRef(); const queryToolCtx = React.useContext(QueryToolContext); - // Track previous state to detect changes + // Track previous column state AND selected row data const prevStateRef = React.useRef({ columnKey: null, columnNames: null, - selectedRowPKs: [], + selectedRowData: [], }); const [mapKey, setMapKey] = React.useState(0); - const currentColumnKey = column?.key; + const currentColumnKey = useMemo(() => column?.key, [column]); const currentColumnNames = React.useMemo( () => columns.map(c => c.key).sort().join(','), [columns] ); + const pkColumn = useMemo(() => findPkColumn(columns), [columns]); + // Detect when to clear, filter, or re-render the map based on changes in geometry column, columns list, or rows useEffect(() => { const prevState = prevStateRef.current; @@ -461,7 +492,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: null, columnNames: null, - selectedRowPKs: [], + selectedRowData: [], }; return; } @@ -472,7 +503,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: currentColumnKey, columnNames: currentColumnNames, - selectedRowPKs: [], + selectedRowData: [], }; return; } @@ -480,30 +511,31 @@ export function GeometryViewer({rows, columns, column}) { if (currentColumnKey === prevState.columnKey && currentColumnNames === prevState.columnNames && rows.length > 0) { - - // If user previously selected specific rows, filter them from new data - if (prevState.selectedRowPKs.length > 0 && prevState.selectedRowPKs.length < rows.length) { - const newSelectedPKs = rows - .filter(row => prevState.selectedRowPKs.includes(row.__temp_PK)) - .map(row => row.__temp_PK); - - prevStateRef.current.selectedRowPKs = newSelectedPKs.length > 0 ? newSelectedPKs : rows.map(r => r.__temp_PK); + let newSelectedRowData; + if (prevState.selectedRowData.length === 0) { + // No previous selection, show all rows + newSelectedRowData = rows; + } else if (prevState.selectedRowData.length < rows.length) { + const matched = matchRowSelection(prevState.selectedRowData, rows, pkColumn, columns); + newSelectedRowData = matched.length > 0 ? matched : rows; } else { - // All rows are displayed - const allPKs = rows.map(r => r.__temp_PK); - prevStateRef.current.selectedRowPKs = allPKs; + newSelectedRowData = rows; } + prevStateRef.current.selectedRowData = newSelectedRowData; } - }, [currentColumnKey, currentColumnNames, rows]); + }, [currentColumnKey, currentColumnNames, rows, pkColumn, columns]); + // Get rows to display based on selection const displayRows = React.useMemo(() => { if (!currentColumnKey || rows.length === 0) return []; + const prevState = prevStateRef.current; + if (currentColumnKey !== prevState.columnKey || currentColumnNames !== prevState.columnNames) { + return rows; + } - const selectedPKs = prevStateRef.current.selectedRowPKs; - return selectedPKs.length > 0 && selectedPKs.length < rows.length - ? rows.filter(row => selectedPKs.includes(row.__temp_PK)) - : rows; - }, [rows, currentColumnKey]); + const selected = prevState.selectedRowData; + return selected.length > 0 && selected.length < rows.length ? selected : rows; + }, [rows, currentColumnKey, currentColumnNames]); // Parse geometry data only when needed const data = React.useMemo(() => { @@ -537,7 +569,7 @@ export function GeometryViewer({rows, columns, column}) { }; }, [queryToolCtx]); - // Dyanmic CRS is not supported. Use srid and mapKey as key and recreate the map on change + // Dynamic CRS is not supported. Use srid and mapKey as key and recreate the map on change return ( Date: Thu, 19 Feb 2026 10:10:16 +0530 Subject: [PATCH 3/4] Fixed some minor data assignment issue in react states --- .../static/js/components/sections/GeometryViewer.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 78301f49b73..85e8d42314a 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -49,7 +49,7 @@ const StyledBox = styled(Box)(({theme}) => ({ }, })); -const PK_COLUMN_NAMES = ['id', 'oid', 'ctid']; +const PK_COLUMN_NAMES = ['id', 'oid']; function parseEwkbData(rows, column) { let key = column.key; @@ -503,7 +503,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: currentColumnKey, columnNames: currentColumnNames, - selectedRowData: [], + selectedRowData: rows, }; return; } From 98a3124a847040bbff87eb427db8a822f60f145a Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Thu, 19 Feb 2026 11:55:27 +0530 Subject: [PATCH 4/4] Fixed more coderabbit review comments for some scenario it suggested --- .../js/components/sections/GeometryViewer.jsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 85e8d42314a..a27923872de 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -511,17 +511,7 @@ export function GeometryViewer({rows, columns, column}) { if (currentColumnKey === prevState.columnKey && currentColumnNames === prevState.columnNames && rows.length > 0) { - let newSelectedRowData; - if (prevState.selectedRowData.length === 0) { - // No previous selection, show all rows - newSelectedRowData = rows; - } else if (prevState.selectedRowData.length < rows.length) { - const matched = matchRowSelection(prevState.selectedRowData, rows, pkColumn, columns); - newSelectedRowData = matched.length > 0 ? matched : rows; - } else { - newSelectedRowData = rows; - } - prevStateRef.current.selectedRowData = newSelectedRowData; + prevStateRef.current.selectedRowData = displayRows; } }, [currentColumnKey, currentColumnNames, rows, pkColumn, columns]); @@ -532,10 +522,15 @@ export function GeometryViewer({rows, columns, column}) { if (currentColumnKey !== prevState.columnKey || currentColumnNames !== prevState.columnNames) { return rows; } - - const selected = prevState.selectedRowData; - return selected.length > 0 && selected.length < rows.length ? selected : rows; - }, [rows, currentColumnKey, currentColumnNames]); + + const prevSelected = prevState.selectedRowData; + if (prevSelected.length === 0) return rows; + if (prevSelected.length < rows.length) { + const matched = matchRowSelection(prevSelected, rows, pkColumn, columns); + return matched.length > 0 ? matched : rows; + } + return rows; + }, [rows, currentColumnKey, currentColumnNames, pkColumn, columns]); // Parse geometry data only when needed const data = React.useMemo(() => {