Skip to content

Commit 395ff36

Browse files
authored
Ensure that the Geometry Viewer refreshes when re-running queries or switching geometry columns, preventing stale data from being displayed. #9392
1 parent 00a44a5 commit 395ff36

File tree

2 files changed

+138
-30
lines changed

2 files changed

+138
-30
lines changed

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

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// This software is released under the PostgreSQL Licence
77
//
88
//////////////////////////////////////////////////////////////
9-
import React, { useEffect, useRef } from 'react';
9+
import React, { useEffect, useRef, useMemo } from 'react';
1010
import { styled } from '@mui/material/styles';
1111
import ReactDOMServer from 'react-dom/server';
1212
import _ from 'lodash';
@@ -18,10 +18,12 @@ import gettext from 'sources/gettext';
1818
import Theme from 'sources/Theme';
1919
import PropTypes from 'prop-types';
2020
import { Box } from '@mui/material';
21+
import EmptyPanelMessage from '../../../../../../static/js/components/EmptyPanelMessage';
2122
import { PANELS } from '../QueryToolConstants';
2223
import { QueryToolContext } from '../QueryToolComponent';
2324

2425
const StyledBox = styled(Box)(({theme}) => ({
26+
position: 'relative',
2527
'& .GeometryViewer-mapContainer': {
2628
backgroundColor: theme.palette.background.default,
2729
height: '100%',
@@ -191,6 +193,7 @@ function parseData(rows, columns, column) {
191193
};
192194
}
193195

196+
194197
function PopupTable({data}) {
195198

196199
return (
@@ -285,20 +288,10 @@ GeoJsonLayer.propTypes = {
285288

286289
function TheMap({data}) {
287290
const mapObj = useMap();
288-
const infoControl = useRef(null);
289291
const resetLayersKey = useRef(0);
290292
const zoomControlWithHome = useRef(null);
291293
const homeCoordinates = useRef(null);
292294
useEffect(()=>{
293-
infoControl.current = Leaflet.control({position: 'topright'});
294-
infoControl.current.onAdd = function () {
295-
let ele = Leaflet.DomUtil.create('div', 'geometry-viewer-info-control');
296-
ele.innerHTML = data.infoList.join('<br />');
297-
return ele;
298-
};
299-
if(data.infoList.length > 0) {
300-
infoControl.current.addTo(mapObj);
301-
}
302295
resetLayersKey.current++;
303296

304297
zoomControlWithHome.current = Leaflet.control.zoom({
@@ -348,7 +341,6 @@ function TheMap({data}) {
348341
zoomControlWithHome.current.addTo(mapObj);
349342

350343
return ()=>{
351-
infoControl.current?.remove();
352344
zoomControlWithHome.current?.remove();
353345
};
354346
}, [data]);
@@ -359,6 +351,17 @@ function TheMap({data}) {
359351

360352
return (
361353
<>
354+
{data.infoList.length > 0 && (
355+
<EmptyPanelMessage text={data.infoList.join(' ')} style={{
356+
position: 'absolute',
357+
top: 0,
358+
left: 0,
359+
width: '100%',
360+
height: '100%',
361+
zIndex: 1000,
362+
pointerEvents: 'none',
363+
}} />
364+
)}
362365
{data.selectedSRID === 4326 &&
363366
<LayersControl position="topright">
364367
<LayersControl.BaseLayer checked name={gettext('Empty')}>
@@ -436,25 +439,47 @@ export function GeometryViewer({rows, columns, column}) {
436439

437440
const mapRef = React.useRef();
438441
const contentRef = React.useRef();
439-
const data = parseData(rows, columns, column);
440442
const queryToolCtx = React.useContext(QueryToolContext);
441443

444+
const currentColumnKey = useMemo(() => column?.key, [column]);
445+
446+
const data = React.useMemo(() => {
447+
if (!currentColumnKey) {
448+
const hasGeometryColumn = columns.some(c => c.cell === 'geometry' || c.cell === 'geography');
449+
return {
450+
'geoJSONs': [],
451+
'selectedSRID': 0,
452+
'getPopupContent': undefined,
453+
'infoList': hasGeometryColumn
454+
? [gettext('Query complete. Use the Geometry Viewer button in the Data Output tab to visualize results.')]
455+
: [gettext('No spatial data found. At least one geometry or geography column is required for visualization.')],
456+
};
457+
}
458+
return parseData(rows, columns, column);
459+
}, [rows, columns, column, currentColumnKey]);
460+
442461
useEffect(()=>{
443462
let timeoutId;
444463
const contentResizeObserver = new ResizeObserver(()=>{
445464
clearTimeout(timeoutId);
446-
if(queryToolCtx.docker.isTabVisible(PANELS.GEOMETRY)) {
465+
if(queryToolCtx?.docker?.isTabVisible(PANELS.GEOMETRY)) {
447466
timeoutId = setTimeout(function () {
448467
mapRef.current?.invalidateSize();
449468
}, 100);
450469
}
451470
});
452-
contentResizeObserver.observe(contentRef.current);
453-
}, []);
471+
if(contentRef.current) {
472+
contentResizeObserver.observe(contentRef.current);
473+
}
474+
return () => {
475+
clearTimeout(timeoutId);
476+
contentResizeObserver.disconnect();
477+
};
478+
}, [queryToolCtx]);
454479

455-
// Dyanmic CRS is not supported. Use srid as key and recreate the map on change
480+
// Dynamic CRS is not supported. Use srid and column key as key and recreate the map on change
456481
return (
457-
<StyledBox ref={contentRef} width="100%" height="100%" key={data.selectedSRID}>
482+
<StyledBox ref={contentRef} width="100%" height="100%" key={`${data.selectedSRID}-${currentColumnKey || 'none'}`}>
458483
<MapContainer
459484
crs={data.selectedSRID === 4326 ? CRS.EPSG3857 : CRS.Simple}
460485
zoom={2} center={[20, 100]}

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

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

878878
const isDataChangedRef = useRef(false);
879+
const lastGvSelectionRef = useRef({
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,
887+
});
888+
879889
useEffect(()=>{
880890
isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted));
881891
}, [dataChangeStore]);
@@ -1460,30 +1470,103 @@ export function ResultSet() {
14601470
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows);
14611471
}, [columns, selectedRows.size]);
14621472

1473+
const openGeometryViewerTab = React.useCallback((column, rowsData) => {
1474+
layoutDocker.openTab({
1475+
id: PANELS.GEOMETRY,
1476+
title: gettext('Geometry Viewer'),
1477+
content: <GeometryViewer rows={rowsData} columns={columns} column={column}/>,
1478+
closable: true,
1479+
}, PANELS.MESSAGES, 'after-tab', true);
1480+
}, [layoutDocker, columns]);
1481+
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.
14631486
useEffect(()=>{
14641487
const renderGeometries = (column)=>{
1488+
const defaultSel = { geometryColumnKey: column?.key, rowIndices: [], columnIndices: new Set(), rangeStartIdx: null, rangeEndIdx: null, cellIdx: null };
14651489
let selRowsData = rows;
1466-
if(selectedRows.size != 0) {
1467-
selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r)));
1490+
1491+
if(selectedRows.size > 0) {
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 };
14681501
} else if(selectedColumns.size > 0) {
1469-
let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1));
1470-
selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key)));
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) };
14711506
} else if(selectedRange.current) {
1507+
// Cell range selected — plot the rows within the range
14721508
let [,, startRowIdx, endRowIdx] = getRangeIndexes();
1473-
selRowsData = rows.slice(startRowIdx, endRowIdx+1);
1509+
selRowsData = rows.slice(startRowIdx, endRowIdx + 1);
1510+
lastGvSelectionRef.current = { ...defaultSel, type: 'range', rangeStartIdx: startRowIdx, rangeEndIdx: endRowIdx };
14741511
} else if(selectedCell.current?.[0]) {
1512+
// Single cell selected — plot only that row
1513+
const cellIdx = rows.indexOf(selectedCell.current[0]);
14751514
selRowsData = [selectedCell.current[0]];
1515+
lastGvSelectionRef.current = { ...defaultSel, type: 'cell', cellIdx: cellIdx >= 0 ? cellIdx : null };
1516+
} else {
1517+
// No selection — plot all rows
1518+
lastGvSelectionRef.current = { ...defaultSel, type: 'all' };
14761519
}
1477-
layoutDocker.openTab({
1478-
id: PANELS.GEOMETRY,
1479-
title:gettext('Geometry Viewer'),
1480-
content: <GeometryViewer rows={selRowsData} columns={columns} column={column} />,
1481-
closable: true,
1482-
}, PANELS.MESSAGES, 'after-tab', true);
1520+
1521+
openGeometryViewerTab(column, selRowsData);
14831522
};
14841523
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
14851524
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
1486-
}, [rows, columns, selectedRows.size, selectedColumns.size]);
1525+
}, [openGeometryViewerTab, eventBus, rows, columns, selectedRows, selectedColumns]);
1526+
1527+
// Auto-update Geometry Viewer when rows/columns change
1528+
useEffect(()=>{
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+
// Re-apply row selection — plot only previously selected rows if indices are still valid
1541+
if(lastSel.type === 'rows' && lastSel.rowIndices.length > 0) {
1542+
if(lastSel.rowIndices.every(idx => idx < rows.length)) {
1543+
selRowsData = lastSel.rowIndices.map(idx => rows[idx]);
1544+
}
1545+
// Re-apply column selection — filter each row to only the previously selected columns
1546+
} else if(lastSel.type === 'columns' && lastSel.columnIndices.size > 0) {
1547+
let selectedCols = _.filter(columns, (_c, i) => lastSel.columnIndices.has(i + 1));
1548+
if(selectedCols.length > 0) {
1549+
selRowsData = _.map(rows, (r) => _.pick(r, _.map(selectedCols, (c) => c.key)));
1550+
}
1551+
// Re-apply range selection — plot the previously selected row range if bounds are still valid
1552+
} else if(lastSel.type === 'range' && lastSel.rangeStartIdx != null) {
1553+
if(lastSel.rangeStartIdx < rows.length && lastSel.rangeEndIdx < rows.length) {
1554+
selRowsData = rows.slice(lastSel.rangeStartIdx, lastSel.rangeEndIdx + 1);
1555+
}
1556+
// Re-apply single cell selection — plot the row of the previously selected cell if still valid
1557+
} else if(lastSel.type === 'cell' && lastSel.cellIdx != null) {
1558+
if(lastSel.cellIdx < rows.length) {
1559+
selRowsData = [rows[lastSel.cellIdx]];
1560+
}
1561+
}
1562+
// If any validation fails above, selRowsData remains as all rows (default)
1563+
openGeometryViewerTab(matchedGeomCol, selRowsData);
1564+
} else {
1565+
// Previously plotted geometry column not found - clear GV
1566+
openGeometryViewerTab(null, []);
1567+
}
1568+
}
1569+
}, [rows, columns, layoutDocker]);
14871570

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

0 commit comments

Comments
 (0)