Skip to content

Commit cc8ce7e

Browse files
authored
Fix blank Query Tool screen caused by malformed runtime locale (#10066)
The Query History panel formats entry dates/times with Date.prototype.toLocaleDateString()/toLocaleTimeString(). On runtimes whose default locale (derived from the OS/environment) is malformed, these throw "RangeError: Incorrect locale information provided". Because getDateFormatted()/getTimeFormatted() are called during render (via getGroups/getGroupHeader/getDatePrefix), the uncaught exception unmounts the whole SQL editor React tree, leaving the user with a blank white screen and losing any unsaved query work. Guard both helpers and fall back to a moment-based format (moment uses its own locale data and does not depend on the broken Intl default) so the editor keeps working instead of crashing. Closes #7596
1 parent f6961bc commit cc8ce7e

3 files changed

Lines changed: 105 additions & 4 deletions

File tree

docs/en_US/release_notes_9_16.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Bug fixes
4242
*********
4343

4444
| `Issue #6308 <https://github.com/pgadmin-org/pgadmin4/issues/6308>`_ - Fix the infinite loading spinner after an idle database connection is silently dropped, by detecting stale connections and offering a reconnect dialog.
45+
| `Issue #7596 <https://github.com/pgadmin-org/pgadmin4/issues/7596>`_ - Fix the Query Tool turning into a blank white screen when the runtime has a malformed default locale, by guarding the Query History date/time formatting against the resulting RangeError.
4546
| `Issue #9091 <https://github.com/pgadmin-org/pgadmin4/issues/9091>`_ - Fix the Query Tool re-prompting for an unsaved password in a loop and rejecting the re-entered password, by caching the entered password on the server manager when the primary connection is already established.
4647
| `Issue #9595 <https://github.com/pgadmin-org/pgadmin4/issues/9595>`_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script.
4748
| `Issue #9677 <https://github.com/pgadmin-org/pgadmin4/issues/9677>`_ - Fix the Unlogged table toggle in table properties not generating any ALTER TABLE ... SET LOGGED/UNLOGGED statement.

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,27 @@ export const QuerySources = {
124124
},
125125
};
126126

127-
function getDateFormatted(date) {
128-
return date.toLocaleDateString();
127+
// On some runtimes the default locale (derived from the OS/environment) is
128+
// malformed, which makes Date.prototype.toLocaleDateString/toLocaleTimeString
129+
// throw "RangeError: Incorrect locale information provided". As these are
130+
// called while rendering the Query History panel, an uncaught throw unmounts
131+
// the whole SQL editor and the user sees a blank white screen, losing any
132+
// unsaved work. Fall back to a moment-based format (moment uses its own
133+
// locale data and does not depend on the broken Intl default). See #7596.
134+
export function getDateFormatted(date) {
135+
try {
136+
return date.toLocaleDateString();
137+
} catch {
138+
return moment(date).format('L');
139+
}
129140
}
130141

131-
function getTimeFormatted(time) {
132-
return time.toLocaleTimeString();
142+
export function getTimeFormatted(time) {
143+
try {
144+
return time.toLocaleTimeString();
145+
} catch {
146+
return moment(time).format('LTS');
147+
}
133148
}
134149

135150
class QueryHistoryUtils {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/////////////////////////////////////////////////////////////
2+
//
3+
// pgAdmin 4 - PostgreSQL Tools
4+
//
5+
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
6+
// This software is released under the PostgreSQL Licence
7+
//
8+
//////////////////////////////////////////////////////////////
9+
10+
// Mock url_for
11+
jest.mock('sources/url_for', () => ({
12+
__esModule: true,
13+
default: jest.fn((endpoint) => `/mock/${endpoint}`),
14+
}));
15+
16+
// Mock the QueryToolComponent to avoid importing all its dependencies
17+
jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx', () => {
18+
const React = require('react');
19+
return {
20+
QueryToolContext: React.createContext(null),
21+
QueryToolConnectionContext: React.createContext(null),
22+
QueryToolEventsContext: React.createContext(null),
23+
};
24+
});
25+
26+
// Mock CodeMirror
27+
jest.mock('../../../pgadmin/static/js/components/ReactCodeMirror', () => ({
28+
__esModule: true,
29+
default: ({ value }) => value,
30+
}));
31+
32+
import { getDateFormatted, getTimeFormatted } from '../../../pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx';
33+
34+
describe('QueryHistory date/time formatting', () => {
35+
it('formats a date using the native locale formatter', () => {
36+
const date = new Date(2025, 0, 15);
37+
expect(getDateFormatted(date)).toBe(date.toLocaleDateString());
38+
});
39+
40+
it('formats a time using the native locale formatter', () => {
41+
const time = new Date(2025, 0, 15, 10, 30, 45);
42+
expect(getTimeFormatted(time)).toBe(time.toLocaleTimeString());
43+
});
44+
45+
// Regression test for #7596: a malformed default locale makes
46+
// toLocaleDateString/toLocaleTimeString throw "RangeError: Incorrect
47+
// locale information provided". The helpers must not propagate the throw
48+
// (which would white-screen the SQL editor) and must return a usable
49+
// string instead.
50+
describe('when the runtime locale is broken', () => {
51+
let dateSpy, timeSpy;
52+
53+
beforeEach(() => {
54+
dateSpy = jest.spyOn(Date.prototype, 'toLocaleDateString')
55+
.mockImplementation(() => {
56+
throw new RangeError('Incorrect locale information provided');
57+
});
58+
timeSpy = jest.spyOn(Date.prototype, 'toLocaleTimeString')
59+
.mockImplementation(() => {
60+
throw new RangeError('Incorrect locale information provided');
61+
});
62+
});
63+
64+
afterEach(() => {
65+
dateSpy.mockRestore();
66+
timeSpy.mockRestore();
67+
});
68+
69+
it('does not throw and returns a non-empty date string', () => {
70+
const date = new Date(2025, 0, 15);
71+
let result;
72+
expect(() => { result = getDateFormatted(date); }).not.toThrow();
73+
expect(typeof result).toBe('string');
74+
expect(result.length).toBeGreaterThan(0);
75+
});
76+
77+
it('does not throw and returns a non-empty time string', () => {
78+
const time = new Date(2025, 0, 15, 10, 30, 45);
79+
let result;
80+
expect(() => { result = getTimeFormatted(time); }).not.toThrow();
81+
expect(typeof result).toBe('string');
82+
expect(result.length).toBeGreaterThan(0);
83+
});
84+
});
85+
});

0 commit comments

Comments
 (0)