Skip to content

Commit afa5e4c

Browse files
committed
new module name expl_pgsql
1 parent f956529 commit afa5e4c

7 files changed

Lines changed: 163 additions & 81 deletions

File tree

web/pgadmin/static/js/components/ReactCodeMirror/index.jsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import gettext from 'sources/gettext';
1919
import { PgIconButton } from '../Buttons';
2020
import { copyToClipboard } from '../../clipboard';
2121
import { useDelayedCaller } from '../../custom_hooks';
22-
import epFormatSQL from '../../../../tools/ep/static/js/ExplainPostgreSQL/formatSQL';
22+
import explPgsqlFormatSQL from '../../../../tools/expl_pgsql/static/js/ExplainPostgreSQL/formatSQL';
23+
import Loader from '../Loader';
2324

2425
import Editor from './components/Editor';
2526
import CustomPropTypes from '../../custom_prop_types';
@@ -69,9 +70,10 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu
6970
const [[showFind, isReplace, findKey], setShowFind] = useState([false, false, false]);
7071
const [showGoto, setShowGoto] = useState(false);
7172
const [showCopy, setShowCopy] = useState(false);
73+
const [loading, setLoading] = useState(false);
7274
const preferences = usePreferences().getPreferencesForModule('sqleditor');
7375
const editorPrefs = usePreferences().getPreferencesForModule('editor');
74-
const epPrefs = usePreferences().getPreferencesForModule('ep');
76+
const explPgsqlPrefs = usePreferences().getPreferencesForModule('expl_pgsql');
7577

7678
const formatSQL = async (view)=>{
7779
let selection = true, sql = view.getSelection();
@@ -98,12 +100,22 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu
98100
selection = false;
99101
}
100102
let formattedSql;
101-
if (epPrefs.explain_postgresql_format) {
103+
if (explPgsqlPrefs.explain_postgresql_format) {
104+
let loadingTimeout;
102105
try {
103-
formattedSql = await epFormatSQL(sql);
106+
loadingTimeout = setTimeout(() => {
107+
setLoading(true);
108+
}, 500);
109+
formattedSql = await explPgsqlFormatSQL(sql);
104110
} catch (e) {
105111
console.error('Error formatting SQL using Explain PostgreSQL API:', e);
106112
formattedSql = format(sql,formatPrefs);
113+
} finally {
114+
if (loading) {
115+
setLoading(false);
116+
} else {
117+
clearTimeout(loadingTimeout);
118+
}
107119
}
108120
} else {
109121
formattedSql = format(sql,formatPrefs);
@@ -207,6 +219,7 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu
207219
<Root className={[className].join(' ')} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} >
208220
<Editor currEditor={currEditorWrap} customKeyMap={finalCustomKeyMap} {...props} />
209221
{showCopy && <CopyButton editor={editor.current} />}
222+
{loading && <Loader message={gettext('Loading...')} autoEllipsis />}
210223
<FindDialog key={findKey} editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} />
211224
<GotoDialog editor={editor.current} show={showGoto} onClose={closeGoto} />
212225
</Root>

web/pgadmin/tools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def register(self, app, options):
3434
from .debugger import blueprint as module
3535
app.register_blueprint(module)
3636

37-
from .ep import blueprint as module
37+
from .expl_pgsql import blueprint as module
3838
app.register_blueprint(module)
3939

4040
from .erd import blueprint as module
Lines changed: 113 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,43 +19,50 @@
1919
from pgadmin.utils.ajax import make_json_response
2020
from pgadmin.user_login_check import pga_login_required
2121

22-
MODULE_NAME = 'ep'
22+
MODULE_NAME = 'expl_pgsql'
2323

24-
class EPModule(PgAdminModule):
24+
25+
class ExplPgsqlModule(PgAdminModule):
2526
"""Explain PostgreSQL configuration module for pgAdmin."""
2627

2728
LABEL = gettext('Explain PostgreSQL')
28-
29+
2930
def register_preferences(self):
3031
"""
3132
Register preferences for Explain PostgreSQL.
3233
"""
3334

35+
self.explain_module = self.preference.register(
36+
'Explain PostgreSQL', 'explain_postgresql',
37+
gettext("Explain Plan"), 'boolean', False,
38+
category_label=gettext('Configuration'),
39+
help_str=gettext('Analyze query plan via Explain PostgreSQL API')
40+
)
41+
3442
self.explain_postgresql_api = self.preference.register(
3543
'Explain PostgreSQL', 'explain_postgresql_api',
36-
gettext("Explain PostgreSQL API"), 'text', 'https://explain.tensor.ru',
44+
gettext("Explain PostgreSQL API"), 'text',
45+
'https://explain.tensor.ru',
3746
category_label=gettext('Configuration'),
38-
help_str=gettext('Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)'),
47+
help_str=gettext(
48+
'Explain PostgreSQL API endpoint '
49+
'(e.g. https://explain.tensor.ru)'
50+
),
3951
allow_blanks=False
4052
)
4153

4254
self.explain_postgresql_private = self.preference.register(
4355
'Explain PostgreSQL', 'explain_postgresql_private',
4456
gettext("Private Plans"), 'boolean', False,
4557
category_label=gettext('Configuration'),
46-
help_str=gettext('Hide plans from public access on Explain PostgreSQL')
47-
)
48-
49-
self.explain_module = self.preference.register(
50-
'Explain PostgreSQL', 'explain_postgresql',
51-
gettext("Explain Plan"), 'boolean', True,
52-
category_label=gettext('Configuration'),
53-
help_str=gettext('Analyze query plan via Explain PostgreSQL API')
58+
help_str=gettext(
59+
'Hide plans from public access on Explain PostgreSQL'
60+
)
5461
)
5562

5663
self.explain_postgresql_format = self.preference.register(
5764
'Explain PostgreSQL', 'explain_postgresql_format',
58-
gettext("Format SQL"), 'boolean', True,
65+
gettext("Format SQL"), 'boolean', False,
5966
category_label=gettext('Configuration'),
6067
help_str=gettext('Format SQL using Explain PostgreSQL API')
6168
)
@@ -65,109 +72,154 @@ def get_exposed_url_endpoints(self):
6572
Returns the list of URLs exposed to the client.
6673
"""
6774
return [
68-
'ep.explain_postgresql',
69-
'ep.explain_postgresql_format',
75+
'expl_pgsql.status',
76+
'expl_pgsql.explain',
77+
'expl_pgsql.formatSQL',
7078
]
7179

7280

7381
# Initialise the module
74-
blueprint = EPModule(MODULE_NAME, __name__, static_url_path='/static')
82+
blueprint = ExplPgsqlModule(MODULE_NAME, __name__, static_url_path='/static')
83+
84+
85+
@blueprint.route("/status", methods=["GET"], endpoint='status')
86+
@pga_login_required
87+
def get_status():
88+
"""
89+
Get the status of the Explain PostgreSQL configuration.
90+
Indicates whether the analysis of query plans
91+
via the Explain PostgreSQL API is currently enabled
92+
"""
93+
94+
return make_json_response(
95+
success=1,
96+
data={
97+
'enabled': get_preference_value('explain_postgresql'),
98+
}
99+
)
100+
75101

76102
@blueprint.route(
77-
'/explain_postgresql_format',
78-
methods=["POST"], endpoint='explain_postgresql_format'
103+
'/formatSQL',
104+
methods=["POST"], endpoint='formatSQL'
79105
)
80106
@pga_login_required
81-
def explain_postgresql_format():
107+
def formatSQL():
82108
"""
83109
This method is used to send sql to explain postgresql beatifier api.
84-
85110
"""
86111

87112
data = request.get_json(silent=True)
88113
if not isinstance(data, dict):
89114
return make_json_response(
90115
success=0,
91116
errormsg="Invalid JSON payload. Expected an object/dictionary.",
92-
info=gettext('JSON payload must be an object, not null, array, or scalar value'),
117+
info=gettext(
118+
'JSON payload must be an object,'
119+
' not null, array, or scalar value'
120+
),
93121
)
94-
122+
95123
explain_postgresql_api = get_preference_value('explain_postgresql_api')
96-
124+
97125
# Validate the API URL to prevent SSRF
98126
if not is_valid_url(explain_postgresql_api):
99127
return make_json_response(
100128
success=0,
101-
errormsg="Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed.",
102-
info=gettext('The provided API endpoint is not valid. Only HTTP/HTTPS URLs are permitted.')
129+
errormsg=gettext(
130+
'Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed.'
131+
),
132+
info=gettext(
133+
'The provided API endpoint is not valid. '
134+
'Only HTTP/HTTPS URLs are permitted.'
135+
)
103136
)
104137

105-
is_error, data = send_post_request(explain_postgresql_api + '/beautifier-api', data)
138+
api_url = explain_postgresql_api + '/beautifier-api'
139+
is_error, data = send_post_request(api_url, data)
106140
if is_error:
107-
return make_json_response(success=0, errormsg=data,
108-
info=gettext('Failed to post data to the Explain Postgresql API'),
109-
)
141+
return make_json_response(
142+
success=0,
143+
errormsg=data,
144+
info=gettext('Failed to post data to the Explain Postgresql API'),
145+
)
110146

111147
return make_json_response(success=1, data=data)
112148

113149

114150
@blueprint.route(
115-
'/explain_postgresql',
116-
methods=["POST"], endpoint='explain_postgresql'
151+
'/explain',
152+
methods=["POST"], endpoint='explain'
117153
)
118154
@pga_login_required
119-
def explain_postgresql():
155+
def explain():
120156
"""
121157
This method is used to send plan to explain postgresql api.
122-
123158
"""
124159

125160
data = request.get_json(silent=True)
126161
if not isinstance(data, dict):
127162
return make_json_response(
128163
success=0,
129164
errormsg="Invalid JSON payload. Expected an object/dictionary.",
130-
info=gettext('JSON payload must be an object, not null, array, or scalar value'),
165+
info=gettext(
166+
'JSON payload must be an object, '
167+
'not null, array, or scalar value'
168+
),
131169
)
132-
170+
133171
explain_postgresql_api = get_preference_value('explain_postgresql_api')
134-
172+
135173
# Validate the API URL to prevent SSRF
136174
if not is_valid_url(explain_postgresql_api):
137175
return make_json_response(
138176
success=0,
139-
errormsg="Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed.",
140-
info=gettext('The provided API endpoint is not valid. Only HTTP/HTTPS URLs are permitted.')
177+
errormsg=gettext(
178+
'Invalid API endpoint URL. Only HTTP/HTTPS URLs are allowed.'
179+
),
180+
info=gettext(
181+
'The provided API endpoint is not valid. '
182+
'Only HTTP/HTTPS URLs are permitted.'
183+
)
141184
)
142-
143-
explain_postgresql_private = get_preference_value('explain_postgresql_private')
185+
186+
pref_name = 'explain_postgresql_private'
187+
explain_postgresql_private = get_preference_value(pref_name)
144188
data['private'] = explain_postgresql_private
145189

146-
is_error, response_data = send_post_request(explain_postgresql_api + '/explain', data)
190+
api_url = explain_postgresql_api + '/explain'
191+
is_error, response_data = send_post_request(api_url, data)
147192
if is_error:
148-
return make_json_response(success=0, errormsg=response_data,
149-
info=gettext('Failed to post data to the Explain Postgresql API'),
150-
)
193+
return make_json_response(
194+
success=0,
195+
errormsg=response_data,
196+
info=gettext('Failed to post data to the Explain Postgresql API'),
197+
)
151198

152199
# response_data should be a relative path from 302 Location header
153200
if not response_data.startswith('/'):
154-
return make_json_response(success=0, errormsg="Unexpected response format from API")
155-
return make_json_response(success=1, data=explain_postgresql_api + response_data)
201+
return make_json_response(
202+
success=0,
203+
errormsg='Unexpected response format from API'
204+
)
205+
res_data = explain_postgresql_api + response_data
206+
return make_json_response(success=1, data=res_data)
156207

157208

158209
def is_valid_url(url):
159210
"""
160-
Validate that a URL is safe to use (HTTP/HTTPS only, localhost and private IP ranges are allowed).
161-
211+
Validate that a URL is safe to use
212+
(HTTP/HTTPS only, localhost and private IP ranges are allowed).
213+
162214
Args:
163215
url: The URL to validate
164-
216+
165217
Returns:
166218
bool: True if URL is valid, False otherwise
167219
"""
168220
if not url:
169221
return False
170-
222+
171223
try:
172224
parsed = urlparse(url)
173225

@@ -184,7 +236,7 @@ def is_valid_url(url):
184236
return False
185237

186238

187-
def send_post_request(url_api, data, parse=False):
239+
def send_post_request(url_api, data):
188240
data = json.dumps(data).encode('utf-8')
189241
headers = {
190242
"Content-Type": "application/json; charset=utf-8",
@@ -197,13 +249,11 @@ def send_post_request(url_api, data, parse=False):
197249
if (response.code == 302):
198250
return False, response.headers["Location"]
199251
response_data = response.read().decode('utf-8')
200-
if (parse):
201-
return False, json.loads(response_data)
202-
else:
203-
return False, response_data
252+
return False, response_data
204253
except Exception as e:
205254
return True, str(e)
206255

256+
207257
class No302HTTPErrorProcessor(urllib.request.HTTPErrorProcessor):
208258

209259
def http_response(self, request, response):
@@ -222,6 +272,7 @@ def http_response(self, request, response):
222272

223273
https_response = http_response
224274

275+
225276
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
226277
"""
227278
A redirect handler that prevents automatic redirects by returning None
@@ -233,14 +284,18 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
233284
"""
234285
return None
235286

236-
# Build opener without HTTPRedirectHandler so No302HTTPErrorProcessor can handle 302 responses
287+
288+
# Build opener without HTTPRedirectHandler
289+
# so No302HTTPErrorProcessor can handle 302 responses
237290
no302opener = urllib.request.build_opener(
238291
No302HTTPErrorProcessor(),
239-
NoRedirectHandler(), # Explicitly add NoRedirectHandler to prevent automatic redirects
292+
# Explicitly add NoRedirectHandler to prevent automatic redirects
293+
NoRedirectHandler(),
240294
urllib.request.HTTPHandler(),
241295
urllib.request.HTTPSHandler()
242296
)
243297

298+
244299
def get_preference_value(name):
245300
"""
246301
Get a preference value, returning None if empty or not set.

web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js renamed to web/pgadmin/tools/expl_pgsql/static/js/ExplainPostgreSQL/formatSQL.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default async function formatSQL(sql) {
55
return new Promise((resolve, reject) => {
66
const api = getApiInstance();
77
api.post(
8-
url_for('ep.explain_postgresql_format'),
8+
url_for('expl_pgsql.formatSQL'),
99
JSON.stringify({
1010
query_src: sql,
1111
}))

0 commit comments

Comments
 (0)