From d5d83c6d2e1660b675dc2f6722456d7c3b81b514 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Wed, 10 Jun 2026 16:22:10 +0100 Subject: [PATCH 1/2] Warn before opening a very large JSON value in the data grid (#9868) Opening a huge JSON/JSONB cell in the Query Tool's cell editor parses, pretty-prints and renders the entire document on the main thread. For pathologically large values (e.g. a jsonb object with 100k keys) this blocks the UI thread and pgAdmin becomes completely unresponsive, with no chance for the user to back out. Guard the JSON editor: when the raw cell value exceeds a size threshold, render nothing until the user confirms via a warning dialog. If they cancel, the editor is closed without doing the expensive work. Small values are unaffected and open immediately as before. The editor already uses commitOnOutsideClick: false, so the confirm dialog does not dismiss the editor. The size-threshold logic is a small, separately tested helper. Closes #9868 --- docs/en_US/release_notes_9_16.rst | 1 + .../components/QueryToolDataGrid/Editors.jsx | 30 ++++++++++++- .../QueryToolDataGrid/json_editor_warning.js | 21 +++++++++ .../sqleditor/json_editor_warning.spec.js | 45 +++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/json_editor_warning.js create mode 100644 web/regression/javascript/sqleditor/json_editor_warning.spec.js diff --git a/docs/en_US/release_notes_9_16.rst b/docs/en_US/release_notes_9_16.rst index 4f0740cdff0..cdc1b92797c 100644 --- a/docs/en_US/release_notes_9_16.rst +++ b/docs/en_US/release_notes_9_16.rst @@ -43,6 +43,7 @@ Bug fixes | `Issue #9595 `_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script. | `Issue #9677 `_ - Fix the Unlogged table toggle in table properties not generating any ALTER TABLE ... SET LOGGED/UNLOGGED statement. | `Issue #9828 `_ - Fix tool calls failing against OpenAI-compatible providers that emit empty/null name, arguments, or id fields in streaming continuation deltas. + | `Issue #9868 `_ - Warn before opening a very large JSON/JSONB value in the data grid cell editor, which could freeze pgAdmin, and let the user choose whether to proceed. | `Issue #9875 `_ - Fixed an issue where EXPLAIN and EXPLAIN ANALYZE failed to execute when blank lines separated clauses in the SQL query. | `Issue #9810 `_ - Use the ServerManager's passfile for the credential gate in connect() so the check matches the passfile actually used for the connection, and warn on conflicting passfile/passexec settings. | `Issue #9892 `_ - Fix blank difference counts on the top-level group rows in Schema Diff. diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx index a7fbb3f9ef4..a33885a5404 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx @@ -15,6 +15,7 @@ import CloseIcon from '@mui/icons-material/Close'; import gettext from 'sources/gettext'; import JSONBigNumber from 'json-bignumber'; import JsonEditor from '../../../../../../static/js/components/JsonEditor'; +import { shouldWarnForLargeJSON } from './json_editor_warning'; import PropTypes from 'prop-types'; import { RowInfoContext } from '.'; import { usePgAdmin } from '../../../../../../static/js/PgAdminProvider'; @@ -388,7 +389,7 @@ export function CheckboxEditor({row, column, onRowChange, onClose}) { } CheckboxEditor.propTypes = EditorPropTypes; -export function JsonTextEditor({row, column, onRowChange, onClose}) { +function JsonTextEditorContent({row, column, onRowChange, onClose}) { const pgAdmin = usePgAdmin(); @@ -475,4 +476,31 @@ export function JsonTextEditor({row, column, onRowChange, onClose}) { ); } +JsonTextEditorContent.propTypes = EditorPropTypes; + +// Wraps the JSON editor with a guard that warns before opening a very large +// value. Rendering/parsing a huge JSON document blocks the main thread and can +// freeze pgAdmin, so for large cells we render nothing until the user confirms +// (#9868). The editor uses commitOnOutsideClick: false, so showing the confirm +// dialog does not dismiss the editor. +export function JsonTextEditor(props) { + const { row, column, onClose } = props; + const pgAdmin = usePgAdmin(); + const [proceed, setProceed] = React.useState( + () => !shouldWarnForLargeJSON(row[column.key]) + ); + + React.useEffect(() => { + if (proceed) return; + pgAdmin.Browser.notifier.confirm( + gettext('Large data'), + gettext('This cell contains a large amount of data. Opening it in the JSON editor may make pgAdmin slow or unresponsive. Do you want to continue?'), + () => setProceed(true), + () => onClose(false), + ); + }, []); + + if (!proceed) return null; + return ; +} JsonTextEditor.propTypes = EditorPropTypes; diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/json_editor_warning.js b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/json_editor_warning.js new file mode 100644 index 00000000000..f5fb97718a3 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/json_editor_warning.js @@ -0,0 +1,21 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2026, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +// Opening a very large JSON/JSONB value in the cell editor parses, pretty- +// prints and renders the whole document on the main thread, which can make +// pgAdmin slow or completely unresponsive. Above this size (in characters) we +// warn the user and let them decide whether to proceed. See issue #9868. +export const LARGE_JSON_WARNING_LENGTH = 1024 * 1024; + +// Returns true when the raw cell value is large enough that opening it in the +// JSON editor warrants a confirmation prompt. Only string values are measured; +// the cheap length check avoids serializing the value just to size it. +export function shouldWarnForLargeJSON(value, threshold = LARGE_JSON_WARNING_LENGTH) { + return typeof value === 'string' && value.length > threshold; +} diff --git a/web/regression/javascript/sqleditor/json_editor_warning.spec.js b/web/regression/javascript/sqleditor/json_editor_warning.spec.js new file mode 100644 index 00000000000..ee9dfc294e4 --- /dev/null +++ b/web/regression/javascript/sqleditor/json_editor_warning.spec.js @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2026, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { + LARGE_JSON_WARNING_LENGTH, + shouldWarnForLargeJSON, +} from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/json_editor_warning'; + +describe('QueryToolDataGrid json_editor_warning', () => { + describe('shouldWarnForLargeJSON', () => { + it('does not warn for small string values', () => { + expect(shouldWarnForLargeJSON('{"a":1}')).toBe(false); + expect(shouldWarnForLargeJSON('')).toBe(false); + }); + + it('warns once a string value exceeds the threshold (#9868)', () => { + const big = 'x'.repeat(LARGE_JSON_WARNING_LENGTH + 1); + expect(shouldWarnForLargeJSON(big)).toBe(true); + }); + + it('does not warn exactly at the threshold', () => { + const atLimit = 'x'.repeat(LARGE_JSON_WARNING_LENGTH); + expect(shouldWarnForLargeJSON(atLimit)).toBe(false); + }); + + it('respects a custom threshold', () => { + expect(shouldWarnForLargeJSON('abcd', 3)).toBe(true); + expect(shouldWarnForLargeJSON('abc', 3)).toBe(false); + }); + + it('does not warn for non-string values', () => { + expect(shouldWarnForLargeJSON(null)).toBe(false); + expect(shouldWarnForLargeJSON(undefined)).toBe(false); + expect(shouldWarnForLargeJSON(12345)).toBe(false); + expect(shouldWarnForLargeJSON([1, 2, 3])).toBe(false); + expect(shouldWarnForLargeJSON({ a: 1 })).toBe(false); + }); + }); +}); From 4b57462e7bb622a2b67584312a7c251c5f1bb374 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Wed, 10 Jun 2026 17:35:52 +0100 Subject: [PATCH 2/2] Keep the JSON editor open when continuing past the large-data warning notifier.confirm() fires its cancel callback whenever the dialog closes, including when the user clicks OK. The large-JSON guard passed () => onClose(false) as the cancel callback, so confirming "continue" also closed the cell editor and the JSON editor never opened. Track the confirmation in the effect closure so the editor is only closed on an actual cancel, and add a regression test for the continue path. Closes #9868 --- .../components/QueryToolDataGrid/Editors.jsx | 15 +++- .../sqleditor/json_editor_warning.spec.js | 89 +++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx index a33885a5404..b717f3eed7e 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx @@ -492,11 +492,22 @@ export function JsonTextEditor(props) { React.useEffect(() => { if (proceed) return; + // notifier.confirm() fires its cancel callback whenever the dialog + // closes, including when the user clicks OK. Track the confirmation so + // we only close the cell editor on an actual cancel and not after the + // user chose to continue (#9868). + let confirmed = false; pgAdmin.Browser.notifier.confirm( gettext('Large data'), gettext('This cell contains a large amount of data. Opening it in the JSON editor may make pgAdmin slow or unresponsive. Do you want to continue?'), - () => setProceed(true), - () => onClose(false), + () => { + confirmed = true; + setProceed(true); + }, + () => { + if (confirmed) return; + onClose(false); + }, ); }, []); diff --git a/web/regression/javascript/sqleditor/json_editor_warning.spec.js b/web/regression/javascript/sqleditor/json_editor_warning.spec.js index ee9dfc294e4..3935751e9e4 100644 --- a/web/regression/javascript/sqleditor/json_editor_warning.spec.js +++ b/web/regression/javascript/sqleditor/json_editor_warning.spec.js @@ -7,11 +7,31 @@ // ////////////////////////////////////////////////////////////// +import { render, screen, act } from '@testing-library/react'; + import { LARGE_JSON_WARNING_LENGTH, shouldWarnForLargeJSON, } from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/json_editor_warning'; +// Stub the heavy JSON editor so the wrapper can render without CodeMirror. +jest.mock('../../../pgadmin/static/js/components/JsonEditor', () => ({ + __esModule: true, + default: () =>
, +})); + +// Mock the QueryToolDataGrid index so importing Editors does not pull in the +// whole data grid; Editors only needs RowInfoContext from it. +jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid', () => { + const ReactActual = require('react'); + return { RowInfoContext: ReactActual.createContext() }; +}); + +import Theme from 'sources/Theme'; +import { JsonTextEditor } from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors'; +import { RowInfoContext } from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid'; +import { PgAdminProvider } from '../../../pgadmin/static/js/PgAdminProvider'; + describe('QueryToolDataGrid json_editor_warning', () => { describe('shouldWarnForLargeJSON', () => { it('does not warn for small string values', () => { @@ -42,4 +62,73 @@ describe('QueryToolDataGrid json_editor_warning', () => { expect(shouldWarnForLargeJSON({ a: 1 })).toBe(false); }); }); + + describe('JsonTextEditor large-value guard', () => { + const KEY = 'col'; + let confirmArgs; + let onClose; + + const renderEditor = (value) => { + const column = { key: KEY, idx: 0, can_edit: true }; + const pgAdmin = { + Browser: { + notifier: { + confirm: (title, text, onOk, onCancel) => { + confirmArgs = { title, text, onOk, onCancel }; + }, + error: jest.fn(), + }, + }, + }; + return render( + + + null }}> + + + + + ); + }; + + beforeEach(() => { + confirmArgs = undefined; + onClose = jest.fn(); + }); + + it('opens the editor immediately for small values without confirming', () => { + renderEditor('{"a":1}'); + expect(confirmArgs).toBeUndefined(); + expect(screen.getByTestId('json-editor')).toBeInTheDocument(); + }); + + it('warns for large values and waits before opening the editor', () => { + renderEditor('x'.repeat(LARGE_JSON_WARNING_LENGTH + 1)); + expect(confirmArgs).toBeDefined(); + expect(screen.queryByTestId('json-editor')).not.toBeInTheDocument(); + }); + + it('closes the editor when the warning is cancelled', () => { + renderEditor('x'.repeat(LARGE_JSON_WARNING_LENGTH + 1)); + act(() => confirmArgs.onCancel()); + expect(onClose).toHaveBeenCalledWith(false); + }); + + it('opens the editor and does not close it when the user continues (#9868)', () => { + renderEditor('x'.repeat(LARGE_JSON_WARNING_LENGTH + 1)); + // notifier.confirm() fires the cancel callback as well as the OK + // callback when the user clicks OK; the guard must keep the editor open. + act(() => { + confirmArgs.onOk(); + confirmArgs.onCancel(); + }); + expect(onClose).not.toHaveBeenCalled(); + expect(screen.getByTestId('json-editor')).toBeInTheDocument(); + }); + }); });