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..b717f3eed7e 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,42 @@ 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; + // 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?'), + () => { + confirmed = true; + setProceed(true); + }, + () => { + if (confirmed) return; + 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..3935751e9e4 --- /dev/null +++ b/web/regression/javascript/sqleditor/json_editor_warning.spec.js @@ -0,0 +1,134 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2026, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +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', () => { + 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); + }); + }); + + 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(); + }); + }); +});