1) Port query tool to React. Fixes #6131

2) Added status bar to the Query Tool. Fixes #3253
3) Ensure that row numbers should be visible in view when scrolling horizontally. Fixes #3989
4) Allow removing a single query history. Refs #4113
5) Partially fixed Macros usability issues. Ref #6969
6) Fixed an issue where the Query tool opens on minimum size if the user opens multiple query tool Window quickly. Fixes #6725
7) Relocate GIS Viewer Button to the Left Side of the Results Table. Fixes #6830
8) Fixed an issue where the connection bar is not visible. Fixes #7188
9) Fixed an issue where an Empty message popup after running a query. Fixes #7260
10) Ensure that Autocomplete should work after changing the connection. Fixes #7262
11) Fixed an issue where the copy and paste row does not work if the first column contains no data. Fixes #7294
This commit is contained in:
Aditya Toshniwal
2022-04-07 17:36:56 +05:30
committed by Akshay Joshi
parent bf8e569bde
commit b5b9ee46a1
213 changed files with 11134 additions and 18830 deletions

View File

@@ -1,617 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""A blueprint module implementing the datagrid frame."""
import simplejson as json
import pickle
import random
from threading import Lock
from flask import Response, url_for, session, request, make_response
from werkzeug.useragents import UserAgent
from flask import current_app as app, render_template
from flask_babel import gettext
from flask_security import login_required, current_user
from pgadmin.tools.sqleditor.command import ObjectRegistry, SQLFilter
from pgadmin.tools.sqleditor import check_transaction_status
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_json_response, bad_request, \
internal_server_error, unauthorized
from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD
from pgadmin.model import Server, User
from pgadmin.utils.driver import get_driver
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
from pgadmin.utils.preferences import Preferences
from pgadmin.settings import get_setting
from pgadmin.browser.utils import underscore_unescape
from pgadmin.utils.exception import ObjectGone
from pgadmin.tools.sqleditor.utils.macros import get_user_macros
from pgadmin.utils.constants import MIMETYPE_APP_JS, UNAUTH_REQ
MODULE_NAME = 'datagrid'
query_tool_close_session_lock = Lock()
class DataGridModule(PgAdminModule):
"""
class DataGridModule(PgAdminModule)
A module class for Edit Grid derived from PgAdminModule.
"""
LABEL = "Data Grid"
def get_own_menuitems(self):
return {}
def get_own_javascripts(self):
return [{
'name': 'pgadmin.datagrid',
'path': url_for('datagrid.index') + "datagrid",
'when': None
}]
def get_panels(self):
return []
def get_exposed_url_endpoints(self):
"""
Returns:
list: URL endpoints for backup module
"""
return [
'datagrid.initialize_datagrid',
'datagrid.initialize_query_tool',
'datagrid.initialize_query_tool_with_did',
'datagrid.filter_validate',
'datagrid.filter',
'datagrid.panel',
'datagrid.close',
'datagrid.update_query_tool_connection'
]
def on_logout(self, user):
"""
This is a callback function when user logout from pgAdmin
:param user:
:return:
"""
with query_tool_close_session_lock:
if 'gridData' in session:
for trans_id in session['gridData']:
close_query_tool_session(trans_id)
# Delete all grid data from session variable
del session['gridData']
blueprint = DataGridModule(MODULE_NAME, __name__, static_url_path='/static')
@blueprint.route("/")
@login_required
def index():
return bad_request(
errormsg=gettext('This URL cannot be requested directly.')
)
@blueprint.route("/css/datagrid.css")
def datagrid_css():
return make_response(
render_template('datagrid/css/datagrid.css'),
200, {'Content-Type': 'text/css'}
)
@blueprint.route("/filter", endpoint='filter')
@login_required
def show_filter():
return render_template(MODULE_NAME + '/filter.html')
@blueprint.route(
'/initialize/datagrid/<int:trans_id>/<int:cmd_type>/<obj_type>/'
'<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
methods=["PUT", "POST"],
endpoint="initialize_datagrid"
)
@login_required
def initialize_datagrid(trans_id, cmd_type, obj_type, sgid, sid, did, obj_id):
"""
This method is responsible for creating an asynchronous connection.
After creating the connection it will instantiate and initialize
the object as per the object type. It will also create a unique
transaction id and store the information into session variable.
Args:
cmd_type: Contains value for which menu item is clicked.
obj_type: Contains type of selected object for which data grid to
be render
sgid: Server group Id
sid: Server Id
did: Database Id
obj_id: Id of currently selected object
"""
if request.data:
filter_sql = json.loads(request.data, encoding='utf-8')
else:
filter_sql = request.args or request.form
# Create asynchronous connection using random connection id.
conn_id = str(random.randint(1, 9999999))
try:
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
# default_conn is same connection which is created when user connect to
# database from tree
default_conn = manager.connection(did=did)
conn = manager.connection(did=did, conn_id=conn_id,
auto_reconnect=False,
use_binary_placeholder=True,
array_to_string=True)
except (ConnectionLost, SSHTunnelConnectionLost):
raise
except Exception as e:
app.logger.error(e)
return internal_server_error(errormsg=str(e))
status, msg = default_conn.connect()
if not status:
app.logger.error(msg)
return internal_server_error(errormsg=str(msg))
status, msg = conn.connect()
if not status:
app.logger.error(msg)
return internal_server_error(errormsg=str(msg))
try:
# if object type is partition then it is nothing but a table.
if obj_type == 'partition':
obj_type = 'table'
# Get the object as per the object type
command_obj = ObjectRegistry.get_object(
obj_type, conn_id=conn_id, sgid=sgid, sid=sid,
did=did, obj_id=obj_id, cmd_type=cmd_type,
sql_filter=filter_sql
)
except ObjectGone:
raise
except Exception as e:
app.logger.error(e)
return internal_server_error(errormsg=str(e))
if 'gridData' not in session:
sql_grid_data = dict()
else:
sql_grid_data = session['gridData']
# Use pickle to store the command object which will be used later by the
# sql grid module.
sql_grid_data[str(trans_id)] = {
# -1 specify the highest protocol version available
'command_obj': pickle.dumps(command_obj, -1)
}
# Store the grid dictionary into the session variable
session['gridData'] = sql_grid_data
return make_json_response(
data={
'conn_id': conn_id
}
)
@blueprint.route(
'/panel/<int:trans_id>',
methods=["POST"],
endpoint='panel'
)
def panel(trans_id):
"""
This method calls index.html to render the data grid.
Args:
trans_id: unique transaction id
"""
url_params = None
if request.args:
url_params = {k: v for k, v in request.args.items()}
if request.form:
url_params['title'] = request.form['title']
if 'sql_filter' in request.form:
url_params['sql_filter'] = request.form['sql_filter']
if 'query_url' in request.form:
url_params['query_url'] = request.form['query_url']
# We need client OS information to render correct Keyboard shortcuts
user_agent = UserAgent(request.headers.get('User-Agent'))
"""
Animations and transitions are not automatically GPU accelerated and by
default use browser's slow rendering engine. We need to set 'translate3d'
value of '-webkit-transform' property in order to use GPU. After applying
this property under linux, Webkit calculates wrong position of the
elements so panel contents are not visible. To make it work, we need to
explicitly set '-webkit-transform' property to 'none' for .ajs-notifier,
.ajs-message, .ajs-modal classes.
This issue is only with linux runtime application and observed in Query
tool and debugger. When we open 'Open File' dialog then whole Query tool
panel content is not visible though it contains HTML element in back end.
The port number should have already been set by the runtime if we're
running in desktop mode.
"""
is_linux_platform = False
from sys import platform as _platform
if "linux" in _platform:
is_linux_platform = True
# Fetch the server details
bgcolor = None
fgcolor = None
s = Server.query.filter_by(id=url_params['sid']).first()
if s and s.bgcolor:
# If background is set to white means we do not have to change
# the title background else change it as per user specified
# background
if s.bgcolor != '#ffffff':
bgcolor = s.bgcolor
fgcolor = s.fgcolor or 'black'
layout = get_setting('SQLEditor/Layout')
macros = get_user_macros()
return render_template(
"datagrid/index.html",
_=gettext,
uniqueId=trans_id,
is_desktop_mode=app.PGADMIN_RUNTIME,
is_linux=is_linux_platform,
title=underscore_unescape(url_params['title']),
url_params=json.dumps(url_params),
client_platform=user_agent.platform,
bgcolor=bgcolor,
fgcolor=fgcolor,
layout=layout,
requirejs=True,
basejs=True,
macros=macros
)
@blueprint.route(
'/initialize/query_tool/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
methods=["POST"], endpoint='initialize_query_tool_with_did'
)
@blueprint.route(
'/initialize/query_tool/<int:trans_id>/<int:sgid>/<int:sid>',
methods=["POST"], endpoint='initialize_query_tool'
)
@login_required
def initialize_query_tool(trans_id, sgid, sid, did=None):
"""
This method is responsible for instantiating and initializing
the query tool object. It will also create a unique
transaction id and store the information into session variable.
Args:
sgid: Server group Id
sid: Server Id
did: Database Id
"""
connect = True
# Read the data if present. Skipping read may cause connection
# reset error if data is sent from the client
if request.data:
_ = request.data
req_args = request.args
if ('recreate' in req_args and
req_args['recreate'] == '1'):
connect = False
is_error, errmsg, conn_id, version = _init_query_tool(trans_id, connect,
sgid, sid, did)
if is_error:
return errmsg
return make_json_response(
data={
'connId': str(conn_id),
'serverVersion': version,
}
)
def _connect(conn, **kwargs):
"""
Connect the database.
:param conn: Connection instance.
:param kwargs: user, role and password data from user.
:return:
"""
user = None
role = None
password = None
is_ask_password = False
if 'user' in kwargs and 'role' in kwargs:
user = kwargs['user']
role = kwargs['role'] if kwargs['role'] else None
password = kwargs['password'] if kwargs['password'] else None
is_ask_password = True
if user:
status, msg = conn.connect(user=user, role=role,
password=password)
else:
status, msg = conn.connect()
return status, msg, is_ask_password, user, role, password
def _init_query_tool(trans_id, connect, sgid, sid, did, **kwargs):
# Create asynchronous connection using random connection id.
conn_id = str(random.randint(1, 9999999))
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
if did is None:
did = manager.did
try:
command_obj = ObjectRegistry.get_object(
'query_tool', conn_id=conn_id, sgid=sgid, sid=sid, did=did
)
except Exception as e:
app.logger.error(e)
return True, internal_server_error(errormsg=str(e)), '', ''
try:
conn = manager.connection(did=did, conn_id=conn_id,
auto_reconnect=False,
use_binary_placeholder=True,
array_to_string=True)
if connect:
status, msg, is_ask_password, user, role, password = _connect(
conn, **kwargs)
if not status:
app.logger.error(msg)
if is_ask_password:
server = Server.query.filter_by(id=sid).first()
return True, make_json_response(
success=0,
status=428,
result=render_template(
'servers/password.html',
server_label=server.name,
username=user,
errmsg=msg,
_=gettext,
allow_save_password=True if
ALLOW_SAVE_PASSWORD and
session['allow_save_password'] else False,
)
), '', ''
else:
return True, internal_server_error(
errormsg=str(msg)), '', ''
except (ConnectionLost, SSHTunnelConnectionLost) as e:
app.logger.error(e)
raise
except Exception as e:
app.logger.error(e)
return True, internal_server_error(errormsg=str(e)), '', ''
if 'gridData' not in session:
sql_grid_data = dict()
else:
sql_grid_data = session['gridData']
# Set the value of auto commit and auto rollback specified in Preferences
pref = Preferences.module('sqleditor')
command_obj.set_auto_commit(pref.preference('auto_commit').get())
command_obj.set_auto_rollback(pref.preference('auto_rollback').get())
# Use pickle to store the command object which will be used
# later by the sql grid module.
sql_grid_data[str(trans_id)] = {
# -1 specify the highest protocol version available
'command_obj': pickle.dumps(command_obj, -1)
}
# Store the grid dictionary into the session variable
session['gridData'] = sql_grid_data
return False, '', conn_id, manager.version
@blueprint.route(
'/initialize/query_tool/update_connection/<int:trans_id>/'
'<int:sgid>/<int:sid>/<int:did>',
methods=["POST"], endpoint='update_query_tool_connection'
)
def update_query_tool_connection(trans_id, sgid, sid, did):
# Remove transaction Id.
with query_tool_close_session_lock:
data = json.loads(request.data, encoding='utf-8')
if 'gridData' not in session:
return make_json_response(data={'status': True})
grid_data = session['gridData']
# Return from the function if transaction id not found
if str(trans_id) not in grid_data:
return make_json_response(data={'status': True})
connect = True
req_args = request.args
if ('recreate' in req_args and
req_args['recreate'] == '1'):
connect = False
new_trans_id = str(random.randint(1, 9999999))
kwargs = {
'user': data['user'],
'role': data['role'] if 'role' in data else None,
'password': data['password'] if 'password' in data else None
}
is_error, errmsg, conn_id, version = _init_query_tool(
new_trans_id, connect, sgid, sid, did, **kwargs)
if is_error:
return errmsg
else:
try:
# Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = \
check_transaction_status(trans_id)
status, error_msg, new_conn, new_trans_obj, new_session_obj = \
check_transaction_status(new_trans_id)
new_session_obj['primary_keys'] = session_obj[
'primary_keys'] if 'primary_keys' in session_obj else None
new_session_obj['columns_info'] = session_obj[
'columns_info'] if 'columns_info' in session_obj else None
new_session_obj['client_primary_key'] = session_obj[
'client_primary_key'] if 'client_primary_key'\
in session_obj else None
close_query_tool_session(trans_id)
# Remove the information of unique transaction id from the
# session variable.
grid_data.pop(str(trans_id), None)
session['gridData'] = grid_data
except Exception as e:
app.logger.error(e)
return make_json_response(
data={
'connId': str(conn_id),
'serverVersion': version,
'tran_id': new_trans_id
}
)
@blueprint.route('/close/<int:trans_id>', methods=["DELETE"], endpoint='close')
def close(trans_id):
"""
This method is used to close the asynchronous connection
and remove the information of unique transaction id from
the session variable.
Args:
trans_id: unique transaction id
"""
with query_tool_close_session_lock:
if 'gridData' not in session:
return make_json_response(data={'status': True})
grid_data = session['gridData']
# Return from the function if transaction id not found
if str(trans_id) not in grid_data:
return make_json_response(data={'status': True})
try:
close_query_tool_session(trans_id)
# Remove the information of unique transaction id from the
# session variable.
grid_data.pop(str(trans_id), None)
session['gridData'] = grid_data
except Exception as e:
app.logger.error(e)
return internal_server_error(errormsg=str(e))
return make_json_response(data={'status': True})
@blueprint.route(
'/filter/validate/<int:sid>/<int:did>/<int:obj_id>',
methods=["PUT", "POST"], endpoint='filter_validate'
)
@login_required
def validate_filter(sid, did, obj_id):
"""
This method is used to validate the sql filter.
Args:
sid: Server Id
did: Database Id
obj_id: Id of currently selected object
"""
if request.data:
filter_sql = json.loads(request.data, encoding='utf-8')
else:
filter_sql = request.args or request.form
try:
# Create object of SQLFilter class
sql_filter_obj = SQLFilter(sid=sid, did=did, obj_id=obj_id)
# Call validate_filter method to validate the SQL.
status, res = sql_filter_obj.validate_filter(filter_sql)
except ObjectGone:
raise
except Exception as e:
app.logger.error(e)
return internal_server_error(errormsg=str(e))
return make_json_response(data={'status': status, 'result': res})
@blueprint.route("/datagrid.js")
@login_required
def script():
"""render the required javascript"""
return Response(
response=render_template("datagrid/js/datagrid.js", _=gettext),
status=200,
mimetype=MIMETYPE_APP_JS
)
def close_query_tool_session(trans_id):
"""
This function is used to cancel the transaction and release the connection.
:param trans_id: Transaction id
:return:
"""
if 'gridData' in session and str(trans_id) in session['gridData']:
cmd_obj_str = session['gridData'][str(trans_id)]['command_obj']
# Use pickle.loads function to get the command object
cmd_obj = pickle.loads(cmd_obj_str)
# if connection id is None then no need to release the connection
if cmd_obj.conn_id is not None:
manager = get_driver(
PG_DEFAULT_DRIVER).connection_manager(cmd_obj.sid)
if manager is not None:
conn = manager.connection(
did=cmd_obj.did, conn_id=cmd_obj.conn_id)
# Release the connection
if conn.connected():
conn.cancel_transaction(cmd_obj.conn_id, cmd_obj.did)
manager.release(did=cmd_obj.did, conn_id=cmd_obj.conn_id)

View File

@@ -1,370 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
'sources/sqleditor_utils', 'backbone',
'tools/datagrid/static/js/show_data',
'tools/datagrid/static/js/show_query_tool', 'pgadmin.browser.toolbar',
'tools/datagrid/static/js/datagrid_panel_title', 'sources/utils', 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
Backbone, showData, showQueryTool, toolBar, panelTitleFunc, commonUtils
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
var wcDocker = window.wcDocker,
pgBrowser = pgAdmin.Browser;
/* Return back, this has been called more than once */
if (pgAdmin.DataGrid)
return pgAdmin.DataGrid;
pgAdmin.DataGrid =
_.extend(
{
init: function() {
if (this.initialized)
return;
this.initialized = true;
this.title_index = 1;
let self = this;
/* Cache may take time to load for the first time
* Keep trying till available
*/
let cacheIntervalId = setInterval(function() {
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('sqleditor');
clearInterval(cacheIntervalId);
}
},0);
pgBrowser.onPreferencesChange('sqleditor', function() {
self.preferences = pgBrowser.get_preferences_for_module('sqleditor');
});
// Define list of nodes on which view data option appears
var supported_nodes = [
'table', 'view', 'mview',
'foreign_table', 'catalog_object', 'partition',
],
/* Enable/disable View data menu in tools based
* on node selected. if selected node is present
* in supported_nodes, menu will be enabled
* otherwise disabled.
*/
view_menu_enabled = function(obj) {
var isEnabled = (() => {
if (!_.isUndefined(obj) && !_.isNull(obj))
return (_.indexOf(supported_nodes, obj._type) !== -1 ? true : false);
else
return false;
})();
toolBar.enable(gettext('View Data'), isEnabled);
toolBar.enable(gettext('Filtered Rows'), isEnabled);
return isEnabled;
},
/* Enable/disable Query tool menu in tools based
* on node selected. if selected node is present
* in unsupported_nodes, menu will be disabled
* otherwise enabled.
*/
query_tool_menu_enabled = function(obj) {
var isEnabled = (() => {
if (!_.isUndefined(obj) && !_.isNull(obj)) {
if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) {
if (obj._type == 'database' && obj.allowConn) {
return true;
} else if (obj._type != 'database') {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
})();
toolBar.enable(gettext('Query Tool'), isEnabled);
return isEnabled;
};
// Define the nodes on which the menus to be appear
var menus = [{
name: 'query_tool',
module: this,
applies: ['tools'],
callback: 'show_query_tool',
enable: query_tool_menu_enabled,
priority: 1,
label: gettext('Query Tool'),
icon: 'pg-font-icon icon-query_tool',
data:{
applies: 'tools',
data_disabled: gettext('Please select a database from the browser tree to access Query Tool.'),
},
}];
// Create context menu
for (let node_val of supported_nodes) {
menus.push({
name: 'view_all_rows_context_' + node_val,
node: node_val,
module: this,
data: {
mnuid: 3,
},
applies: ['context', 'object'],
callback: 'show_data_grid',
enable: view_menu_enabled,
category: 'view_data',
priority: 101,
label: gettext('All Rows'),
}, {
name: 'view_first_100_rows_context_' + node_val,
node: node_val,
module: this,
data: {
mnuid: 1,
},
applies: ['context', 'object'],
callback: 'show_data_grid',
enable: view_menu_enabled,
category: 'view_data',
priority: 102,
label: gettext('First 100 Rows'),
}, {
name: 'view_last_100_rows_context_' + node_val,
node: node_val,
module: this,
data: {
mnuid: 2,
},
applies: ['context', 'object'],
callback: 'show_data_grid',
enable: view_menu_enabled,
category: 'view_data',
priority: 103,
label: gettext('Last 100 Rows'),
}, {
name: 'view_filtered_rows_context_' + node_val,
node: node_val,
module: this,
data: {
mnuid: 4,
},
applies: ['context', 'object'],
callback: 'show_filtered_row',
enable: view_menu_enabled,
category: 'view_data',
priority: 104,
label: gettext('Filtered Rows...'),
});
}
pgAdmin.Browser.add_menu_category('view_data', gettext('View/Edit Data'), 100, '');
pgAdmin.Browser.add_menus(menus);
// Creating a new pgAdmin.Browser frame to show the data.
var dataGridFrameType = new pgAdmin.Browser.Frame({
name: 'frm_datagrid',
showTitle: true,
isCloseable: true,
isRenamable: true,
isPrivate: true,
url: 'about:blank',
});
// Load the newly created frame
dataGridFrameType.load(pgBrowser.docker);
this.on('pgadmin-datagrid:transaction:created', function(trans_obj) {
this.launch_grid(trans_obj);
});
},
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
const transId = commonUtils.getRandomInt(1, 9999999);
showData.showDataGrid(this, pgBrowser, alertify, data, i, transId);
},
// This is a callback function to show filtered data when user click on menu item.
show_filtered_row: function(data, i) {
const transId = commonUtils.getRandomInt(1, 9999999);
showData.showDataGrid(this, pgBrowser, alertify, data, i, transId, true, this.preferences);
},
// This is a callback function to show query tool when user click on menu item.
show_query_tool: function(url, aciTreeIdentifier) {
const transId = commonUtils.getRandomInt(1, 9999999);
var t = pgBrowser.tree,
i = aciTreeIdentifier || t.selected(),
d = i ? t.itemData(i) : undefined;
//Open query tool with create script if copy_sql_to_query_tool is true else open blank query tool
var preference = pgBrowser.get_preference('sqleditor', 'copy_sql_to_query_tool');
if(preference.value && !d._type.includes('coll-') && (url === '' || url['applies'] === 'tools')){
var stype = d._type.toLowerCase();
var data = {
'script': stype,
data_disabled: gettext('The selected tree node does not support this option.'),
};
pgBrowser.Node.callbacks.show_script(data);
}else{
if(d._type.includes('coll-')){
url = '';
}
showQueryTool.showQueryTool(this, pgBrowser, url, aciTreeIdentifier, transId);
}
},
launch_grid: function(trans_id, panel_url, is_query_tool, panel_title, sURL=null, sql_filter=null) {
let queryToolForm = `
<form id="queryToolForm" action="${panel_url}" method="post">
<input id="title" name="title" hidden />`;
if(sURL){
queryToolForm +=`<input name="query_url" value="${sURL}" hidden />`;
}
if(sql_filter) {
queryToolForm +=`<textarea name="sql_filter" hidden>${sql_filter}</textarea>`;
}
/* Escape backslashes as it is stripped by back end */
queryToolForm +=`
</form>
<script>
document.getElementById("title").value = "${_.escape(panel_title.replace('\\', '\\\\'))}";
document.getElementById("queryToolForm").submit();
</script>
`;
var browser_preferences = pgBrowser.get_preferences_for_module('browser');
var open_new_tab = browser_preferences.new_browser_tab_open;
if (open_new_tab && open_new_tab.includes('qt')) {
var newWin = window.open('', '_blank');
if(newWin) {
newWin.document.write(queryToolForm);
newWin.document.title = panel_title;
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function() {
pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
return false;
}
} else {
/* On successfully initialization find the dashboard panel,
* create new panel and add it to the dashboard panel.
*/
var propertiesPanel = pgBrowser.docker.findPanels('properties');
var queryToolPanel = pgBrowser.docker.addPanel('frm_datagrid', wcDocker.DOCK.STACKED, propertiesPanel[0]);
queryToolPanel.trans_id = trans_id;
showQueryTool._set_dynamic_tab(pgBrowser, browser_preferences['dynamic_tabs']);
// Set panel title and icon
panelTitleFunc.setQueryToolDockerTitle(queryToolPanel, is_query_tool, _.unescape(panel_title));
queryToolPanel.focus();
// Listen on the panel closed event.
if (queryToolPanel.isVisible()) {
queryToolPanel.on(wcDocker.EVENT.CLOSED, function() {
$.ajax({
url: url_for('datagrid.close', {'trans_id': trans_id}),
method: 'DELETE',
});
});
}
queryToolPanel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
queryToolPanel.trigger(wcDocker.EVENT.RESIZED);
});
commonUtils.registerDetachEvent(queryToolPanel);
// Listen on the panelRename event.
queryToolPanel.on(wcDocker.EVENT.RENAME, function(panel_data) {
var temp_title = panel_data.$titleText[0].textContent;
var is_dirty_editor = queryToolPanel.is_dirty_editor ? queryToolPanel.is_dirty_editor : false;
var title = queryToolPanel.is_dirty_editor ? panel_data.$titleText[0].textContent.replace(/.$/, '') : temp_title;
alertify.prompt('', title,
// We will execute this function when user clicks on the OK button
function(evt, value) {
// Remove the leading and trailing white spaces.
value = value.trim();
if(value) {
var is_file = false;
if(panel_data.$titleText[0].innerHTML.includes('File - ')) {
is_file = true;
}
var selected_item = pgBrowser.tree.selected();
var panel_titles = '';
if(is_query_tool) {
panel_titles = panelTitleFunc.getPanelTitle(pgBrowser, selected_item, value);
} else {
panel_titles = showData.generateDatagridTitle(pgBrowser, selected_item, value);
}
// Set title to the selected tab.
if (is_dirty_editor) {
panel_titles = panel_titles + ' *';
}
panelTitleFunc.setQueryToolDockerTitle(queryToolPanel, is_query_tool, _.unescape(panel_titles), is_file);
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function(evt) { evt.cancel = false; }
).set({'title': gettext('Rename Panel')});
});
var openQueryToolURL = function(j) {
// add spinner element
let $spinner_el =
$(`<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>`).appendTo($(j).data('embeddedFrame').$container);
let init_poller_id = setInterval(function() {
var frameInitialized = $(j).data('frameInitialized');
if (frameInitialized) {
clearInterval(init_poller_id);
var frame = $(j).data('embeddedFrame');
if (frame) {
frame.onLoaded(()=>{
$spinner_el.remove();
});
frame.openHTML(queryToolForm);
}
}
}, 100);
};
openQueryToolURL(queryToolPanel);
}
return true;
},
},
Backbone.Events);
return pgAdmin.DataGrid;
});

View File

@@ -1,507 +0,0 @@
{% extends "base.html" %}
{% block title %}{{title}}{% endblock %}
{% block body %}
<style>
body {padding: 0px;}
{% if is_desktop_mode and is_linux %}
.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform: none;}
.alertify-notifier{-webkit-transform: none;}
.alertify-notifier .ajs-message{-webkit-transform: none;}
.alertify .ajs-dialog.ajs-shake{-webkit-animation-name: none;}
.sql-editor-busy-icon.fa-pulse{-webkit-animation: none;}
{% endif %}
</style>
{% block css_link %}
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
{% endblock %}
<div id="main-editor_panel">
<div class="sql-editor">
<div id="btn-toolbar" class="editor-toolbar" role="toolbar" aria-label="">
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-show-query-tool" type="button" class="btn btn-sm btn-primary-icon btn-show-query-tool"
title=""
aria-label="show query tool"
tabindex="0">
<i class="pg-font-icon icon-query_tool" aria-hidden="true" role="img"></i>
</button>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-load-file" type="button" class="btn btn-sm btn-primary-icon btn-load-file"
title=""
accesskey=""
tabindex="0">
<i class="fa fa-folder-open sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-save-file" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
disabled>
<i class="fa fa-save sql-icon-lg" aria-hidden="true" tabindex="0" role="img"></i>
</button>
<button id="btn-file-menu-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" disabled
tabindex="0" aria-label="file menu">
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-file-menu-save" href="#" tabindex="0">
<span>{{ _('Save') }}</span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-file-menu-save-as" href="#" tabindex="0">
<span>{{ _('Save As') }}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-save-data" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
tabindex="0" disabled>
<i class="pg-font-icon icon-save_data_changes sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-find" type="button" class="btn btn-sm btn-primary-icon" aria-label="{{ _('Find') }}" title="{{ _('Find (Ctrl/Cmd+F)') }}">
<i class="fa fa-search sql-icon-lg" aria-hidden="true" tabindex="0" role="img"></i>
</button>
<button id="btn-find-menu-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
title=""
accesskey=""
tabindex="0">
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-find-menu-find" href="#" tabindex="0">
<span> {{ _('Find') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+F)') }}
{% else %}
{{ _(' (Ctrl+F)') }}{%- endif %}</span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-find-menu-find-next" href="#" tabindex="0">
<span> {{ _('Find Next') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+G)') }}
{% else %}
{{ _(' (Ctrl+G)') }}{%- endif %}</span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-find-menu-find-previous" href="#" tabindex="0">
<span> {{ _('Find Previous') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+Shift+G)') }}
{% else %}
{{ _(' (Ctrl+Shift+G)') }}{%- endif %}</span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-find-menu-find-persistent" href="#" tabindex="0">
<span>{{ _('Persistent Find') }}</span>
</a>
</li>
<li class="dropdown-divider"></li>
<li>
<a class="dropdown-item" id="btn-find-menu-replace" href="#" tabindex="0">
<span> {{ _('Replace') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+Option+F)') }}
{% else %}
{{ _(' (Ctrl+Shift+F)') }}{%- endif %}</span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-find-menu-replace-all" href="#" tabindex="0">
<span>{{ _('Replace All') }}</span>
</a>
</li>
<li class="dropdown-divider"></li>
<li>
<a class="dropdown-item" id="btn-find-menu-jump" href="#" tabindex="0">
<span>{{ _('Jump (Alt+G)') }}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-copy-row" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
tabindex="0" disabled>
<i class="fa fa-copy sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-copy-row-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
tabindex="0" aria-label="copy row">
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-copy-with-header" href="#" tabindex="0">
<i class="copy-with-header fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Copy with headers') }} </span>
</a>
</li>
</ul>
<button id="btn-paste-row" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
tabindex="0" disabled>
<i class="fa fa-paste sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-delete-row" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
tabindex="0" disabled>
<i class="fa fa-trash-alt sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-edit-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
aria-label="{{ _('Edit') }}" title="{{ _('Edit') }}" tabindex="0">
<i class="fa fa-edit sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-indent-code" href="#" tabindex="0">
<span> {{ _('Indent Selection (Tab)') }} </span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-unindent-code" href="#" tabindex="0">
<span> {{ _('Unindent Selection (Shift+Tab)') }} </span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-comment-line" href="#" tabindex="0">
<span> {{ _('Inline Comment Selection') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+/)') }}
{% else %}
{{ _(' (Ctrl+/)') }}{%- endif %}</span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-uncomment-line" href="#" tabindex="0">
<span> {{ _('Inline Uncomment Selection') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+.)') }}
{% else %}
{{ _(' (Ctrl+.)') }}{%- endif %}</span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-toggle-comment-block" href="#" tabindex="0">
<span> {{ _('Block Comment/Uncomment Selection') }}{% if client_platform == 'macos' -%}
{{ _(' (Shift+Cmd+/)') }}
{% else %}
{{ _(' (Shift+Ctrl+/)') }}{%- endif %}</span>
</a>
</li>
<li class="dropdown-divider"></li>
<li>
<a class="dropdown-item" id="btn-format-sql" href="#" tabindex="0">
<span>{{ _('Format SQL') }}{% if client_platform == 'macos' -%}
{{ _(' (Shift+Cmd+K)') }}
{% else %}
{{ _(' (Shift+Ctrl+K)') }}{%- endif %}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-filter" type="button" class="btn btn-secondary"
title=""
accesskey=""
tabindex="0" disabled>
<i class="fa fa-filter sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-filter-dropdown" type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
title=""
accesskey=""
disabled tabindex="0">
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a id="btn-filter-menu" class="dropdown-item" href="#" tabindex="0">{{ _('Sort/Filter') }}</a>
</li>
<li>
<a id="btn-include-filter" class="dropdown-item" href="#" tabindex="0">{{ _('Filter by Selection') }}</a>
</li>
<li>
<a id="btn-exclude-filter" class="dropdown-item" href="#" tabindex="0">{{ _('Exclude by Selection') }}</a>
</li>
<li>
<a id="btn-remove-filter" class="dropdown-item" href="#" tabindex="0">{{ _('Remove Sort/Filter') }}</a>
</li>
</ul>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<select id="btn-rows-limit" class="limit form-control form-control-sm" disabled
title=""
accesskey=""
tabindex="0">
<option value="-1">{{ _('No limit') }}</option>
<option value="1000">{{ _('1000 rows') }}</option>
<option value="500">{{ _('500 rows') }}</option>
<option value="100">{{ _('100 rows') }}</option>
</select>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-cancel-query" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
tabindex="0" disabled >
<i class="fa fa-stop sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-flash" data-test-selector="execute-refresh-button" type="button" class="btn btn-sm btn-primary-icon" style="width: 32px;"
title=""
tabindex="0" data-click-counter="0" disabled>
<i class="fa fa-play sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-query-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
accesskey=""
title=""
tabindex="0">
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-auto-commit" href="#" tabindex="0">
<i class="auto-commit fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Auto commit?') }} </span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-auto-rollback" href="#" tabindex="0">
<i class="auto-rollback fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Auto rollback on error?') }} </span>
</a>
</li>
</ul>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-explain" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
tabindex="0" disabled>
<i class="fa fa-hand-pointer sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-explain-analyze" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey="" disabled>
<i class="fa fa-list-alt sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-explain-options-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
tabindex="0" aria-label="explain">
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-explain-verbose" href="#" tabindex="0">
<i class="explain-verbose fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Verbose') }} </span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-explain-costs" href="#" tabindex="0">
<i class="explain-costs fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Costs') }} </span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-explain-buffers" href="#" tabindex="0">
<i class="explain-buffers fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Buffers') }} </span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-explain-timing" href="#" tabindex="0">
<i class="explain-timing fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Timing') }} </span>
</a>
</li>
<li data-min-ver="100000">
<a class="dropdown-item" id="btn-explain-summary" href="#" tabindex="0">
<i class="explain-summary fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Summary') }} </span>
</a>
</li>
<li data-min-ver="120000">
<a class="dropdown-item" id="btn-explain-settings" href="#" tabindex="0">
<i class="explain-settings fa fa-check visibility-hidden" aria-hidden="true" role="img"></i>
<span> {{ _('Settings') }} </span>
</a>
</li>
</ul>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-commit" type="button" class="btn btn-sm btn-primary-icon"
title=""
accesskey=""
tabindex="0" disabled>
<i class="pg-font-icon icon-commit sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<button id="btn-rollback" type="button" class="btn btn-sm btn-primary-icon"
title=""
tabindex="0" disabled>
<i class="pg-font-icon icon-rollback sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
</div>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-clear-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
title=""
accesskey=""
tabindex="0">
<i class="fa fa-eraser sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-clear" href="#" tabindex="0">
<span> {{ _('Clear Query Window') }} </span>
</a>
</li>
<li>
<a class="dropdown-item" id="btn-clear-history" href="#" tabindex="0">
<span> {{ _('Clear History') }} </span>
</a>
</li>
</ul>
</div>
<div class="btn-group" role="group" aria-label="">
<button id="btn-save-results-to-file" type="button" class="btn btn-sm btn-primary-icon"
title=""
tabindex="0" disabled>
<i class="fa fa-download sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
</div>
<div class="btn-group mr-1 user_macros" role="group" aria-label="">
<button id="btn-macro-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
aria-label="{{ _('Macros') }}" title="{{ _('Macros') }}" tabindex="0">
<i class="fa fa-scroll sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-manage-macros" href="#" tabindex="0">
<span> {{ _('Manage Macros...') }} </span>
</a>
</li>
{% if macros|length > 0 %}
<li class="dropdown-divider"></li>
{% endif %}
{% for i in macros %}
<li>
<a class="dropdown-item btn-macro" data-macro-id="{{ i.id }}" href="#" tabindex="0">
<span> {{ _(i.name)|e }} </span>
<span> ({{ i.key_label }}) </span>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="connection_status_wrapper d-flex">
<div id="btn-conn-status"
role="status"
class="connection_status d-flex justify-content-center align-items-center" data-container="body"
data-toggle="popover" data-placement="bottom"
data-content=""
data-panel-visible="visible"
accesskey=""
tabindex="0">
<i class="pg-font-icon icon-disconnected obtaining-conn d-flex m-auto" aria-hidden="true"
title="" role="img">
</i>
</div>
<div class="connection-info btn-group mr-1" role="group" aria-label="" style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% endif %}; color: {% if fgcolor %}{{ fgcolor }}{% endif %};">
<div class="connection-data" data-toggle="dropdown">
<div class="editor-title" aria-haspopup="true" aria-expanded="false"
style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% endif %}; color: {% if fgcolor %}{{ fgcolor }}{% endif %};">&nbsp;
</div>
<span class="conn-info-dd dropdown-toggle dropdown-toggle-split"
aria-haspopup="true" aria-expanded="false"></span>
</div>
<ul class="dropdown-menu" id="connections-list">
</ul>
</div>
</div>
<div id="editor-panel" tabindex="0">
<div id="main_loader" class="pg-sp-container sql-editor-busy-fetching">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon sql-editor-busy-icon"></div>
</div>
<div class="row"><div class="col-12 pg-sp-text sql-editor-busy-text">{{ _('Loading...') }}</div></div>
</div>
</div>
</div>
<div class="sql-editor-busy-text-status d-none"></div>
</div>
</div>
{% endblock %}
{% block init_script %}
require(['sources/generated/browser_nodes', 'sources/generated/codemirror', 'sources/generated/slickgrid'], function() {
require(['sources/generated/sqleditor'], function(ctx) {
var $ = pgAdmin.SqlEditor.jquery,
S = pgAdmin.SqlEditor.S,
editorPanel = $('.sql-editor');
// Register unload event on window close.
/* If opened in new tab, close the connection only on tab/window close and
* not on refresh attempt because the user may cancel the reload
*/
if(window.opener) {
$(window).on('unload', function(ev) {
$.ajax({
method: 'DELETE',
url: "{{ url_for('datagrid.index') }}" + "close/" + {{ uniqueId }}
});
});
} else {
$(window).on('beforeunload', function(ev) {
$.ajax({
method: 'DELETE',
url: "{{ url_for('datagrid.index') }}" + "close/" + {{ uniqueId }}
});
});
}
// Get the controller object from pgAdmin.SqlEditor
var sqlEditorController = pgAdmin.SqlEditor.create(editorPanel);
// Listen on events to show/hide loading-icon and change messages.
{% if script_type_url %}
var script_type_url = '{{ script_type_url }}';
{% else %}
var script_type_url = '';
{% endif %}
// Start the query tool.
sqlEditorController.start(
{{ uniqueId }},
{{ url_params|safe}},
'{{ layout|safe }}',
{{ macros|safe }}
);
// If opening from schema diff, set the generated script to the SQL Editor
var schema_ddl_diff = (window.opener !== null) ? window.opener.pgAdmin.ddl_diff : (window.parent !== null) ? window.parent.pgAdmin.ddl_diff : window.top.pgAdmin.ddl_diff;
sqlEditorController.set_value_to_editor(schema_ddl_diff);
if (window.opener !== null) window.opener.pgAdmin.ddl_diff = '';
else if (window.parent !== null) window.parent.pgAdmin.ddl_diff = '';
else if (window.top !== null) window.top.pgAdmin.ddl_diff = '';
});
});
{% endblock %}

View File

@@ -1,134 +0,0 @@
{
"data_grid_init_query_tool": [
{
"name": "Datagrid init query tool",
"url": "/datagrid/initialize/query_tool/",
"is_positive_test": true,
"mocking_required": false,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_query_tool_close": [
{
"name": "Datagrid query tool close",
"url": "/datagrid/close/",
"is_positive_test": true,
"mocking_required": false,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_validate_filter": [
{
"name": "Datagrid validate filter",
"url": "/datagrid/filter/validate/",
"is_positive_test": true,
"mocking_required": false,
"test_data": "id = 1",
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Datagrid validate filter",
"url": "/datagrid/filter/validate/",
"is_positive_test": false,
"mocking_required": true,
"test_data": "id = 1",
"mock_data": {
"function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_scalar",
"return_value": "(False, 'Mocked Internal Server Error while validate filter')"
},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_update_connection": [
{
"name": "Datagrid update connection positive",
"url": "/datagrid/initialize/query_tool/update_connection/",
"is_positive_test": true,
"mocking_required": false,
"is_create_role": false,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Datagrid update connection with new user",
"url": "/datagrid/initialize/query_tool/update_connection/",
"is_positive_test": true,
"mocking_required": false,
"is_create_role": true,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_panel": [
{
"name": "Datagrid Panel",
"url": "/datagrid/panel/",
"is_positive_test": true,
"mocking_required": false,
"test_data": {},
"mock_data": {
},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_initialize": [
{
"name": "Datagrid Initialize",
"url": "/datagrid/initialize/datagrid/",
"is_positive_test": true,
"mocking_required": false,
"test_data": "id=1",
"mock_data": {
},
"expected_data": {
"status_code": 200
}
},{
"name": "Datagrid Initialize",
"url": "/datagrid/initialize/datagrid/",
"is_positive_test": true,
"mocking_required": false,
"test_data": null,
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Datagrid Initialize",
"url": "/datagrid/initialize/datagrid/",
"is_positive_test": false,
"mocking_required": true,
"test_data": "id=1",
"mock_data": {
"function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict",
"return_value": "(False, 'Mocked Internal Server Error while initialize datagrid.')"
},
"expected_data": {
"status_code": 500
}
}
]
}

View File

@@ -1,73 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.utils.exception import ExecuteError
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from . import utils as data_grid_utils
class DatagridInitQueryToolTestCase(BaseTestGenerator):
"""
This will init query-tool connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_init_query_tool',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
self.trans_id = str(random.randint(1, 9999999))
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
def init_query_tool(self):
response = self.tester.post(
self.url + str(self.trans_id) + '/' + str(self.sgid) + '/' + str(
self.sid) + '/' + str(self.did),
content_type='html/json'
)
return response
def runTest(self):
""" This function will init query tool connection."""
if self.is_positive_test:
response = self.init_query_tool()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -1,90 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.utils.exception import ExecuteError
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from . import utils as data_grid_utils
class DatagridPanelTestCase(BaseTestGenerator):
"""
This will data grid panel.
"""
scenarios = utils.generate_scenarios(
'data_grid_panel',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.trans_id = str(random.randint(1, 9999999))
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize querty tool.")
def panel(self):
query_param = \
'?is_query_tool={0}&sgid={1}&sid={2}&server_type={3}' \
'&did={4}&title={5}'.format(True, self.sgid, self.sid,
self.server_information['type'],
self.did, 'Query panel')
response = self.tester.post(
self.url + str(self.trans_id) + query_param,
data=json.dumps(self.test_data),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.panel()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
with patch(self.mock_data["function_name"],
return_value=eval(self.mock_data["return_value"])):
response = self.panel()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -1,78 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridQueryToolCloseTestCase(BaseTestGenerator):
"""
This will close query-tool connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_query_tool_close',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.trans_id = str(random.randint(1, 9999999))
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize querty tool.")
def close_connection(self):
response = self.tester.delete(
self.url + str(self.trans_id),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.close_connection()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -1,121 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from pgadmin.browser.server_groups.servers.roles.tests import \
utils as roles_utils
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridUpdateConnectionTestCase(BaseTestGenerator):
"""
This will update query-tool connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_update_connection',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
self.trans_id = str(random.randint(1, 9999999))
self.roles = None
if self.is_create_role:
data = roles_utils.get_role_data(self.server['db_password'])
self.role_name = data['rolname']
self.role_password = data['rolpassword']
roles_utils.create_role_with_password(
self.server, self.role_name, self.role_password)
if not self.is_positive_test or self.is_create_role:
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize querty tool.")
self.test_data = {
"database": self.did,
"server": self.sid,
}
if self.server_information['type'] == 'ppas':
self.test_data['password'] = 'enterprisedb'
self.test_data['user'] = 'enterprisedb'
else:
self.test_data['password'] = 'postgres'
self.test_data['user'] = 'postgres'
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
def update_connection(self, user_data=None):
if user_data:
response = self.tester.post(
self.url + str(self.trans_id) + '/' + str(self.sgid) +
'/' + str(self.sid) + '/' + str(self.did),
data=json.dumps(user_data),
content_type='html/json'
)
else:
response = self.tester.post(
self.url + str(self.trans_id) + '/' + str(self.sgid) + '/' +
str(self.sid) + '/' + str(self.did),
data=json.dumps(self.test_data),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
user_data = dict()
if self.is_create_role:
user_data['user'] = self.role_name
user_data['password'] = self.role_password
user_data['role'] = None
response = self.update_connection(user_data=user_data)
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
response = self.update_connection()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -1,92 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridValidateFilterTestCase(BaseTestGenerator):
"""
This will validate filter connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_validate_filter',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.schema_id = parent_node_dict['schema'][-1]["schema_id"]
self.schema_name = parent_node_dict['schema'][-1]["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise ExecuteError("Could not find the schema to add a table.")
self.table_name = "table_for_wizard%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
def validate_filter(self):
response = self.tester.post(
self.url + str(self.sid) + '/' + str(self.did) + '/' +
str(self.table_id),
data=json.dumps(self.test_data),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.validate_filter()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
with patch(self.mock_data["function_name"],
return_value=eval(self.mock_data["return_value"])):
response = self.validate_filter()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -1,109 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridInitializeTestCase(BaseTestGenerator):
"""
This will Initialize datagrid
"""
scenarios = utils.generate_scenarios(
'data_grid_initialize',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.schema_id = parent_node_dict['schema'][-1]["schema_id"]
self.schema_name = parent_node_dict['schema'][-1]["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise ExecuteError("Could not find the schema to add a table.")
self.table_name = "table_for_wizard%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
self.trans_id = str(random.randint(1, 9999999))
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize query tool.")
def initialize_datagrid(self):
if self.test_data:
response = self.tester.post(
self.url + str(self.trans_id) + '/4/table/' +
str(self.sgid) + '/' + str(self.sid) + '/' +
str(self.did) + '/' + str(self.table_id),
data=json.dumps(self.test_data),
content_type='html/json'
)
else:
response = self.tester.post(
self.url + str(self.trans_id) + '/4/table/' +
str(self.sgid) + '/' + str(self.sid) + '/' +
str(self.did) + '/' + str(self.table_id),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.initialize_datagrid()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
with patch(self.mock_data["function_name"],
return_value=eval(self.mock_data["return_value"])):
response = self.initialize_datagrid()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -1,33 +0,0 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import os
import json
file_name = os.path.basename(__file__)
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
with open(CURRENT_PATH + "/datagrid_test_data.json") as data_file:
test_cases = json.load(data_file)
def _init_query_tool(self, trans_id, server_group, server_id, db_id):
QUERY_TOOL_INIT_URL = '/datagrid/initialize/query_tool'
qt_init = self.tester.post(
'{0}/{1}/{2}/{3}/{4}'.format(
QUERY_TOOL_INIT_URL,
trans_id,
server_group,
server_id,
db_id
),
follow_redirects=True
)
assert qt_init.status_code == 200
qt_init = json.loads(qt_init.data.decode('utf-8'))
return qt_init

View File

@@ -14,8 +14,8 @@ define([
'alertify', 'sources/pgadmin', 'pgadmin.browser',
'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform',
'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils',
'tools/datagrid/static/js/show_query_tool', 'sources/utils',
'pgadmin.authenticate.kerberos', 'tools/datagrid/static/js/datagrid_panel_title',
'tools/sqleditor/static/js/show_query_tool', 'sources/utils',
'pgadmin.authenticate.kerberos', 'tools/sqleditor/static/js/sqleditor_title',
'wcdocker', 'pgadmin.browser.frame',
], function(
gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,

View File

@@ -6,8 +6,8 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import {generateTitle} from '../../../datagrid/static/js/datagrid_panel_title';
import {_set_dynamic_tab} from '../../../datagrid/static/js/show_query_tool';
import {generateTitle} from '../../../sqleditor/static/js/sqleditor_title';
import {_set_dynamic_tab} from '../../../sqleditor/static/js/show_query_tool';
function setFocusToDebuggerEditor(editor, command) {
const TAB = 9;

View File

@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import {getPanelTitle} from 'tools/datagrid/static/js/datagrid_panel_title';
import {getPanelTitle} from 'tools/sqleditor/static/js/sqleditor_title';
import {getRandomInt, registerDetachEvent} from 'sources/utils';
import Notify from '../../../../static/js/helpers/Notifier';

View File

@@ -22,7 +22,7 @@ import FloatingNote from './FloatingNote';
import {setPanelTitle} from '../../erd_module';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import {showERDSqlTool} from 'tools/datagrid/static/js/show_query_tool';
import {showERDSqlTool} from 'tools/sqleditor/static/js/show_query_tool';
import 'wcdocker';
import Theme from '../../../../../../static/js/Theme';
import TableSchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui';
@@ -579,7 +579,7 @@ export default class BodyWidget extends React.Component {
let sqlId = `erd${this.props.params.trans_id}`;
localStorage.setItem(sqlId, sqlScript);
showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgWindow.pgAdmin.DataGrid, this.props.alertify);
showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgWindow.pgAdmin.Tools.SQLEditor, this.props.alertify);
})
.catch((error)=>{
this.handleAxiosCatch(error);

View File

@@ -18,7 +18,7 @@ import {retrieveAncestorOfTypeServer} from 'sources/tree/tree_utils';
import pgWindow from 'sources/window';
import Notify from '../../../../static/js/helpers/Notifier';
import {generateTitle, refresh_db_node} from 'tools/datagrid/static/js/datagrid_panel_title';
import {generateTitle, refresh_db_node} from 'tools/sqleditor/static/js/sqleditor_title';
export function setPanelTitle(psqlToolPanel, panelTitle) {

View File

@@ -15,8 +15,7 @@ import Backbone from 'backbone';
import Slick from 'sources/../bundle/slickgrid';
import pgAdmin from 'sources/pgadmin';
import {setPGCSRFToken} from 'sources/csrf';
import {generateScript} from 'tools/datagrid/static/js/show_query_tool';
import 'pgadmin.sqleditor';
import 'pgadmin.tools.sqleditor';
import pgWindow from 'sources/window';
import _ from 'underscore';
import Notify from '../../../../static/js/helpers/Notifier';
@@ -27,6 +26,7 @@ import { SchemaDiffSelect2Control, SchemaDiffHeaderView,
import { handleDependencies, selectDependenciesForGroup,
selectDependenciesForAll, selectDependencies } from './schema_diff_dependency';
import { generateScript } from '../../../sqleditor/static/js/show_query_tool';
var wcDocker = window.wcDocker;
@@ -252,6 +252,7 @@ export default class SchemaDiffUI {
let data = res.data;
let server_data = {};
if (data) {
let sqlId = `schema${self.trans_id}`;
server_data['sgid'] = data.gid;
server_data['sid'] = data.sid;
server_data['stype'] = data.type;
@@ -259,13 +260,13 @@ export default class SchemaDiffUI {
server_data['user'] = data.user;
server_data['did'] = self.model.get('target_did');
server_data['database'] = data.database;
server_data['sql_id'] = sqlId;
if (_.isUndefined(generated_script)) {
generated_script = script_header + 'BEGIN;' + '\n' + self.model.get('diff_ddl') + '\n' + 'END;';
}
pgWindow.pgAdmin.ddl_diff = generated_script;
generateScript(server_data, pgWindow.pgAdmin.DataGrid, Alertify);
localStorage.setItem(sqlId, generated_script);
generateScript(server_data, pgWindow.pgAdmin.Tools.SQLEditor, Alertify);
}
$('#diff_fetching_data').find('.schema-diff-busy-text').text('');

View File

@@ -9,7 +9,7 @@
import gettext from 'sources/gettext';
import {Dialog} from 'sources/alertify/dialog';
import {getPanelTitle} from 'tools/datagrid/static/js/datagrid_panel_title';
import {getPanelTitle} from 'tools/sqleditor/static/js/sqleditor_title';
import {retrieveAncestorOfTypeDatabase} from 'sources/tree/tree_utils';
export default class SearchObjectsDialog extends Dialog {

View File

@@ -11,16 +11,21 @@
import os
import pickle
import re
import random
from urllib.parse import unquote
from threading import Lock
import simplejson as json
from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT
from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT,\
ALLOW_SAVE_PASSWORD
from werkzeug.user_agent import UserAgent
from flask import Response, url_for, render_template, session, current_app
from flask import request, jsonify
from flask import request
from flask_babel import gettext
from flask_security import login_required, current_user
from pgadmin.misc.file_manager import Filemanager
from pgadmin.tools.sqleditor.command import QueryToolCommand
from pgadmin.tools.sqleditor.command import QueryToolCommand, ObjectRegistry, \
SQLFilter
from pgadmin.tools.sqleditor.utils.constant_definition import ASYNC_OK, \
ASYNC_EXECUTION_ABORTED, \
CONNECTION_STATUS_MESSAGE_MAPPING, TX_STATUS_INERROR
@@ -33,7 +38,8 @@ from pgadmin.utils.ajax import make_json_response, bad_request, \
success_return, internal_server_error
from pgadmin.utils.driver import get_driver
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost, \
CryptKeyMissing
CryptKeyMissing, ObjectGone
from pgadmin.browser.utils import underscore_unescape
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete
from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
@@ -48,10 +54,13 @@ from pgadmin.utils.constants import MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, ERROR_FETCHING_DATA
from pgadmin.model import Server, ServerGroup
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.settings import get_setting
from pgadmin.utils.preferences import Preferences
MODULE_NAME = 'sqleditor'
TRANSACTION_STATUS_CHECK_FAILED = gettext("Transaction status check failed.")
_NODES_SQL = 'nodes.sql'
sqleditor_close_session_lock = Lock()
class SqlEditorModule(PgAdminModule):
@@ -73,13 +82,6 @@ class SqlEditorModule(PgAdminModule):
url=url_for('help.static', filename='index.html'))
]}
def get_own_javascripts(self):
return [{
'name': 'pgadmin.sqleditor',
'path': url_for('sqleditor.index') + "sqleditor",
'when': None
}]
def get_panels(self):
return []
@@ -89,6 +91,15 @@ class SqlEditorModule(PgAdminModule):
list: URL endpoints for sqleditor module
"""
return [
'sqleditor.initialize_viewdata',
'sqleditor.initialize_sqleditor',
'sqleditor.initialize_sqleditor_with_did',
'sqleditor.filter_validate',
'sqleditor.filter',
'sqleditor.panel',
'sqleditor.close',
'sqleditor.update_sqleditor_connection',
'sqleditor.view_data_start',
'sqleditor.query_tool_start',
'sqleditor.poll',
@@ -117,6 +128,7 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.get_macros',
'sqleditor.set_macros',
'sqleditor.get_new_connection_data',
'sqleditor.get_new_connection_servers',
'sqleditor.get_new_connection_database',
'sqleditor.get_new_connection_user',
'sqleditor._check_server_connection_status',
@@ -125,6 +137,20 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.connect_server_with_user',
]
def on_logout(self, user):
"""
This is a callback function when user logout from pgAdmin
:param user:
:return:
"""
with sqleditor_close_session_lock:
if 'gridData' in session:
for trans_id in session['gridData']:
close_sqleditor_session(trans_id)
# Delete all grid data from session variable
del session['gridData']
def register_preferences(self):
register_query_tool_preferences(self)
@@ -140,6 +166,480 @@ def index():
)
@blueprint.route("/filter", endpoint='filter')
@login_required
def show_filter():
return render_template(MODULE_NAME + '/filter.html')
@blueprint.route(
'/initialize/viewdata/<int:trans_id>/<int:cmd_type>/<obj_type>/'
'<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
methods=["PUT", "POST"],
endpoint="initialize_viewdata"
)
@login_required
def initialize_viewdata(trans_id, cmd_type, obj_type, sgid, sid, did, obj_id):
"""
This method is responsible for creating an asynchronous connection.
After creating the connection it will instantiate and initialize
the object as per the object type. It will also create a unique
transaction id and store the information into session variable.
Args:
cmd_type: Contains value for which menu item is clicked.
obj_type: Contains type of selected object for which data grid to
be render
sgid: Server group Id
sid: Server Id
did: Database Id
obj_id: Id of currently selected object
"""
if request.data:
filter_sql = json.loads(request.data, encoding='utf-8')
else:
filter_sql = request.args or request.form
# Create asynchronous connection using random connection id.
conn_id = str(random.randint(1, 9999999))
try:
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
# default_conn is same connection which is created when user connect to
# database from tree
default_conn = manager.connection(did=did)
conn = manager.connection(did=did, conn_id=conn_id,
auto_reconnect=False,
use_binary_placeholder=True,
array_to_string=True)
except (ConnectionLost, SSHTunnelConnectionLost):
raise
except Exception as e:
current_app.logger.error(e)
return internal_server_error(errormsg=str(e))
status, msg = default_conn.connect()
if not status:
current_app.logger.error(msg)
return internal_server_error(errormsg=str(msg))
status, msg = conn.connect()
if not status:
current_app.logger.error(msg)
return internal_server_error(errormsg=str(msg))
try:
# if object type is partition then it is nothing but a table.
if obj_type == 'partition':
obj_type = 'table'
# Get the object as per the object type
command_obj = ObjectRegistry.get_object(
obj_type, conn_id=conn_id, sgid=sgid, sid=sid,
did=did, obj_id=obj_id, cmd_type=cmd_type,
sql_filter=filter_sql
)
except ObjectGone:
raise
except Exception as e:
current_app.logger.error(e)
return internal_server_error(errormsg=str(e))
if 'gridData' not in session:
sql_grid_data = dict()
else:
sql_grid_data = session['gridData']
# Use pickle to store the command object which will be used later by the
# sql grid module.
sql_grid_data[str(trans_id)] = {
# -1 specify the highest protocol version available
'command_obj': pickle.dumps(command_obj, -1)
}
# Store the grid dictionary into the session variable
session['gridData'] = sql_grid_data
return make_json_response(
data={
'conn_id': conn_id
}
)
@blueprint.route(
'/panel/<int:trans_id>',
methods=["POST"],
endpoint='panel'
)
def panel(trans_id):
"""
This method calls index.html to render the data grid.
Args:
trans_id: unique transaction id
"""
params = None
if request.args:
params = {k: v for k, v in request.args.items()}
close_url = ''
if request.form:
params['title'] = underscore_unescape(request.form['title'])
close_url = request.form['close_url']
if 'sql_filter' in request.form:
params['sql_filter'] = request.form['sql_filter']
if 'query_url' in request.form:
params['query_url'] = request.form['query_url']
params['trans_id'] = trans_id
# We need client OS information to render correct Keyboard shortcuts
params['client_platform'] = UserAgent(request.headers.get('User-Agent'))\
.platform
params['is_linux'] = False
from sys import platform as _platform
if "linux" in _platform:
params['is_linux'] = True
# Fetch the server details
params['bgcolor'] = None
params['fgcolor'] = None
s = Server.query.filter_by(id=params['sid']).first()
if s and s.bgcolor:
# If background is set to white means we do not have to change
# the title background else change it as per user specified
# background
if s.bgcolor != '#ffffff':
params['bgcolor'] = s.bgcolor
params['fgcolor'] = s.fgcolor or 'black'
params['server_name'] = s.name
params['username'] = s.username
params['layout'] = get_setting('SQLEditor/Layout')
params['macros'] = get_user_macros()
params['is_desktop_mode'] = current_app.PGADMIN_RUNTIME
return render_template(
"sqleditor/index.html",
close_url=close_url,
params=json.dumps(params),
requirejs=True,
basejs=True,
)
@blueprint.route(
'/initialize/sqleditor/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
methods=["POST"], endpoint='initialize_sqleditor_with_did'
)
@blueprint.route(
'/initialize/sqleditor/<int:trans_id>/<int:sgid>/<int:sid>',
methods=["POST"], endpoint='initialize_sqleditor'
)
@login_required
def initialize_sqleditor(trans_id, sgid, sid, did=None):
"""
This method is responsible for instantiating and initializing
the query tool object. It will also create a unique
transaction id and store the information into session variable.
Args:
sgid: Server group Id
sid: Server Id
did: Database Id
"""
connect = True
# Read the data if present. Skipping read may cause connection
# reset error if data is sent from the client
if request.data:
_ = request.data
req_args = request.args
if ('recreate' in req_args and
req_args['recreate'] == '1'):
connect = False
is_error, errmsg, conn_id, version = _init_sqleditor(
trans_id, connect, sgid, sid, did)
if is_error:
return errmsg
return make_json_response(
data={
'connId': str(conn_id),
'serverVersion': version,
}
)
def _connect(conn, **kwargs):
"""
Connect the database.
:param conn: Connection instance.
:param kwargs: user, role and password data from user.
:return:
"""
user = None
role = None
password = None
is_ask_password = False
if 'user' in kwargs and 'role' in kwargs:
user = kwargs['user']
role = kwargs['role'] if kwargs['role'] else None
password = kwargs['password'] if kwargs['password'] else None
is_ask_password = True
if user:
status, msg = conn.connect(user=user, role=role,
password=password)
else:
status, msg = conn.connect()
return status, msg, is_ask_password, user, role, password
def _init_sqleditor(trans_id, connect, sgid, sid, did, **kwargs):
# Create asynchronous connection using random connection id.
conn_id = str(random.randint(1, 9999999))
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
if did is None:
did = manager.did
try:
command_obj = ObjectRegistry.get_object(
'query_tool', conn_id=conn_id, sgid=sgid, sid=sid, did=did
)
except Exception as e:
current_app.logger.error(e)
return True, internal_server_error(errormsg=str(e)), '', ''
try:
conn = manager.connection(did=did, conn_id=conn_id,
auto_reconnect=False,
use_binary_placeholder=True,
array_to_string=True)
if connect:
status, msg, is_ask_password, user, role, password = _connect(
conn, **kwargs)
if not status:
current_app.logger.error(msg)
if is_ask_password:
server = Server.query.filter_by(id=sid).first()
return True, make_json_response(
success=0,
status=428,
result=render_template(
'servers/password.html',
server_label=server.name,
username=user,
errmsg=msg,
_=gettext,
allow_save_password=True if
ALLOW_SAVE_PASSWORD and
session['allow_save_password'] else False,
)
), '', ''
else:
return True, internal_server_error(
errormsg=str(msg)), '', ''
except (ConnectionLost, SSHTunnelConnectionLost) as e:
current_app.logger.error(e)
raise
except Exception as e:
current_app.logger.error(e)
return True, internal_server_error(errormsg=str(e)), '', ''
if 'gridData' not in session:
sql_grid_data = dict()
else:
sql_grid_data = session['gridData']
# Set the value of auto commit and auto rollback specified in Preferences
pref = Preferences.module('sqleditor')
command_obj.set_auto_commit(pref.preference('auto_commit').get())
command_obj.set_auto_rollback(pref.preference('auto_rollback').get())
# Use pickle to store the command object which will be used
# later by the sql grid module.
sql_grid_data[str(trans_id)] = {
# -1 specify the highest protocol version available
'command_obj': pickle.dumps(command_obj, -1)
}
# Store the grid dictionary into the session variable
session['gridData'] = sql_grid_data
return False, '', conn_id, manager.version
@blueprint.route(
'/initialize/sqleditor/update_connection/<int:trans_id>/'
'<int:sgid>/<int:sid>/<int:did>',
methods=["POST"], endpoint='update_sqleditor_connection'
)
def update_sqleditor_connection(trans_id, sgid, sid, did):
# Remove transaction Id.
with sqleditor_close_session_lock:
data = json.loads(request.data, encoding='utf-8')
if 'gridData' not in session:
return make_json_response(data={'status': True})
grid_data = session['gridData']
# Return from the function if transaction id not found
if str(trans_id) not in grid_data:
return make_json_response(data={'status': True})
connect = True
req_args = request.args
if ('recreate' in req_args and
req_args['recreate'] == '1'):
connect = False
new_trans_id = str(random.randint(1, 9999999))
kwargs = {
'user': data['user'],
'role': data['role'] if 'role' in data else None,
'password': data['password'] if 'password' in data else None
}
is_error, errmsg, conn_id, version = _init_sqleditor(
new_trans_id, connect, sgid, sid, did, **kwargs)
if is_error:
return errmsg
else:
try:
# Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = \
check_transaction_status(trans_id)
status, error_msg, new_conn, new_trans_obj, new_session_obj = \
check_transaction_status(new_trans_id)
new_session_obj['primary_keys'] = session_obj[
'primary_keys'] if 'primary_keys' in session_obj else None
new_session_obj['columns_info'] = session_obj[
'columns_info'] if 'columns_info' in session_obj else None
new_session_obj['client_primary_key'] = session_obj[
'client_primary_key'] if 'client_primary_key'\
in session_obj else None
close_sqleditor_session(trans_id)
# Remove the information of unique transaction id from the
# session variable.
grid_data.pop(str(trans_id), None)
session['gridData'] = grid_data
except Exception as e:
current_app.logger.error(e)
return make_json_response(
data={
'connId': str(conn_id),
'serverVersion': version,
'trans_id': new_trans_id
}
)
@blueprint.route('/close/<int:trans_id>', methods=["DELETE"], endpoint='close')
def close(trans_id):
"""
This method is used to close the asynchronous connection
and remove the information of unique transaction id from
the session variable.
Args:
trans_id: unique transaction id
"""
with sqleditor_close_session_lock:
if 'gridData' not in session:
return make_json_response(data={'status': True})
grid_data = session['gridData']
# Return from the function if transaction id not found
if str(trans_id) not in grid_data:
return make_json_response(data={'status': True})
try:
close_sqleditor_session(trans_id)
# Remove the information of unique transaction id from the
# session variable.
grid_data.pop(str(trans_id), None)
session['gridData'] = grid_data
except Exception as e:
current_app.logger.error(e)
return internal_server_error(errormsg=str(e))
return make_json_response(data={'status': True})
@blueprint.route(
'/filter/validate/<int:sid>/<int:did>/<int:obj_id>',
methods=["PUT", "POST"], endpoint='filter_validate'
)
@login_required
def validate_filter(sid, did, obj_id):
"""
This method is used to validate the sql filter.
Args:
sid: Server Id
did: Database Id
obj_id: Id of currently selected object
"""
if request.data:
filter_sql = json.loads(request.data, encoding='utf-8')
else:
filter_sql = request.args or request.form
try:
# Create object of SQLFilter class
sql_filter_obj = SQLFilter(sid=sid, did=did, obj_id=obj_id)
# Call validate_filter method to validate the SQL.
status, res = sql_filter_obj.validate_filter(filter_sql)
except ObjectGone:
raise
except Exception as e:
current_app.logger.error(e)
return internal_server_error(errormsg=str(e))
return make_json_response(data={'status': status, 'result': res})
def close_sqleditor_session(trans_id):
"""
This function is used to cancel the transaction and release the connection.
:param trans_id: Transaction id
:return:
"""
if 'gridData' in session and str(trans_id) in session['gridData']:
cmd_obj_str = session['gridData'][str(trans_id)]['command_obj']
# Use pickle.loads function to get the command object
cmd_obj = pickle.loads(cmd_obj_str)
# if connection id is None then no need to release the connection
if cmd_obj.conn_id is not None:
manager = get_driver(
PG_DEFAULT_DRIVER).connection_manager(cmd_obj.sid)
if manager is not None:
conn = manager.connection(
did=cmd_obj.did, conn_id=cmd_obj.conn_id)
# Release the connection
if conn.connected():
conn.cancel_transaction(cmd_obj.conn_id, cmd_obj.did)
manager.release(did=cmd_obj.did, conn_id=cmd_obj.conn_id)
def check_transaction_status(trans_id):
"""
This function is used to check the transaction id
@@ -286,8 +786,6 @@ def start_view_data(trans_id):
'filter_applied': filter_applied,
'limit': limit, 'can_edit': can_edit,
'can_filter': can_filter, 'sql': sql,
'info_notifier_timeout':
blueprint.info_notifier_timeout.get() * 1000
}
)
@@ -1345,7 +1843,7 @@ def start_query_download_tool(trans_id):
errormsg=TRANSACTION_STATUS_CHECK_FAILED
)
data = request.values if request.values else None
data = request.values if request.values else request.json
if data is None:
return make_json_response(
status=410,
@@ -1534,8 +2032,12 @@ def _check_server_connection_status(sgid, sid=None):
'/new_connection_dialog/<int:sgid>/<int:sid>',
methods=["GET"], endpoint='get_new_connection_data'
)
@blueprint.route(
'/new_connection_dialog',
methods=["GET"], endpoint='get_new_connection_servers'
)
@login_required
def get_new_connection_data(sgid, sid=None):
def get_new_connection_data(sgid=None, sid=None):
"""
This method is used to get required data for get new connection.
:extract_sql_from_network_parameters,
@@ -1822,7 +2324,9 @@ def connect_server(sid, usr=None):
)
view = SchemaDiffRegistry.get_node_view('server')
return view.connect(server.servergroup_id, sid, user_name=user)
return view.connect(
server.servergroup_id, sid, user_name=user, resp_json=True
)
@blueprint.route(
@@ -1886,7 +2390,8 @@ def clear_query_history(trans_id):
status, error_msg, conn, trans_obj, session_ob = \
check_transaction_status(trans_id)
return QueryHistory.clear(current_user.id, trans_obj.sid, conn.db)
filter = request.json
return QueryHistory.clear(current_user.id, trans_obj.sid, conn.db, filter)
@blueprint.route(

View File

@@ -1,401 +0,0 @@
#main-editor_panel {
position: absolute;
left: 0;
right: 0;
top : 0;
bottom: 0;
}
.sql-editor {
position: absolute;
left: 0;
right: 0;
top : 0;
bottom: 0;
}
.filter-container .CodeMirror-scroll {
min-height: 120px;
}
.filter-container .sql-textarea{
box-shadow: 0.1px 0.1px 3px #000;
margin-bottom: 5px;
}
#filter .btn-group {
margin-right: 2px;
float: right;
}
#filter .btn-group > button {
padding: 3px;
}
#filter .btn-group .btn-primary {
margin: auto !important;
}
.has-select-all table thead tr th:nth-child(1),
.has-select-all table tbody tr td:nth-child(1) {
width: 35px !important;
max-width: 35px !important;
min-width: 35px !important;
}
.sql-status-cell {
max-width: 30px;
}
.btn-circle {
width: 16px;
height: 16px;
text-align: center;
padding: 0;
font-size: 10px;
line-height: 1.428571429;
border-radius: 10px;
cursor: auto;
}
.visibility-hidden {
visibility: hidden;
}
.sql-editor-mark {
border-bottom: 2px dotted red;
}
.CodeMirror {
min-height: 100%;
height: 100%;
}
#output-panel {
height: 100% !important;
}
.sql-editor-explain {
height: 100%;
width: 100%;
overflow: auto;
}
.sqleditor-hint {
padding-left: 20px;
}
.CodeMirror-hint .fa::before {
padding-right: 7px;
}
h2 {
font-size: 10pt;
border-bottom: 1px dotted gray;
}
ul {
margin-left: 0;
padding: 0;
cursor: default;
}
li {
padding: 0 0 0 0px;
list-style: none;
margin: 0;
}
#datagrid {
background: white;
outline: 0;
font-size: 9pt;
}
#datagrid .slick-header-column.ui-state-default {
height: 32px !important;
}
#datagrid .grid-header label {
display: inline-block;
font-weight: bold;
margin: auto auto auto 6px;
}
.grid-header .ui-icon {
margin: 4px 4px auto 6px;
background-color: transparent;
border-color: transparent;
}
.slick-row .cell-actions {
text-align: left;
}
/* Slick.Editors.Text, Slick.Editors.Date */
#datagrid .slick-header > input.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Checkbox */
#datagrid .slick-header > input.editor-checkbox {
margin: 0;
height: 100%;
padding: 0;
border: 0;
}
.slick-row.selected .cell-selection {
background-color: transparent; /* show default selected row background */
}
#datagrid .slick-header .ui-state-default,
#datagrid .slick-header .ui-widget-content.ui-state-default,
#datagrid .slick-header .ui-widget-header .ui-state-default {
background: none;
}
#datagrid .slick-header .slick-header-column .column-name {
font-weight: bold;
display: block;
}
.column-description {
display: table-cell;
}
.long_text_editor {
margin-left: 5px;
font-size: 12px !important;
padding: 1px 7px;
}
/* Slick.Editors.Text, Slick.Editors.Date */
input.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Text, Slick.Editors.Date */
textarea.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Checkbox */
input.editor-checkbox {
margin: 0;
height: 100%;
padding: 0;
border: 0;
}
/* remove outlined border on focus */
input.editor-checkbox:focus {
outline: none;
}
.slick-cell span[data-cell-type="row-header-selector"] {
display: block;
text-align: center;
}
/*
SlickGrid, To fix the issue of width misalignment between Column Header &
actual Column in Mozilla Firefox browser
Ref: https://github.com/mleibman/SlickGrid/issues/742
*/
.slickgrid, .slickgrid *, .slick-header-column {
box-sizing: content-box;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
-ms-box-sizing: content-box;
}
.select-all-icon {
margin-left: 9px;
margin-right: 4px;
vertical-align: bottom;
position: absolute;
bottom: 4px;
right: 0;
}
/* Style for text editor */
.pg_buttons {
text-align:right;
}
#datagrid .slick-row .slick-cell {
white-space: pre;
}
.connection_status {
font-size: 1rem;
width: 40px;
}
.ajs-body .warn-header {
font-size: 13px;
font-weight: bold;
line-height: 3em;
}
.ajs-body .warn-body {
font-size: 13px;
}
.ajs-body .warn-footer {
font-size: 13px;
line-height: 3em;
}
/* For Filter status bar */
.data_sorting_dialog .pg-prop-status-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 5;
}
.data_sorting_dialog .CodeMirror-gutter-wrapper {
left: -30px !important;
}
.data_sorting_dialog .CodeMirror-gutters {
left: 0px !important;
}
.data_sorting_dialog .custom_height_css_class {
height: 100px;
}
.data_sorting_dialog .data_sorting {
padding: 10px 0px;
}
.connection-status-hide {
display: none !important;
}
/* For geometry data viewer panel */
.sql-editor-geometry-viewer{
width: 100%;
height: 100%;
}
.geometry-viewer-container {
width: 100%;
height: 100%;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAANElEQVQoU2O8e/fuf2VlZUYGLAAkB5bApggmBteJrAiZjWI0SAJkIrKVxCvAawVeRxLyJgB+Ajc1cwux9wAAAABJRU5ErkJggg==);
/* Let's keep the background as fff irrespective of theme
* make geometry viewer look clean
*/
background-color: #fff;
}
/* For geometry column button */
.div-view-geometry-column, .editable-column-header-icon {
float: right;
height: 100%;
display: flex;
display: -webkit-flex;
align-items: center;
padding-right: 6px;
}
/* For leaflet popup */
.leaflet-popup-content-wrapper {
border-radius: 2px;
}
.leaflet-popup-content {
margin: 5px;
padding: 10px 10px 0;
overflow-y: scroll;
overflow-x: hidden;
}
/* For geometry viewer property table */
.view-geometry-property-table {
table-layout: fixed;
white-space: nowrap;
padding: 0;
}
.view-geometry-property-table th {
overflow: hidden;
text-overflow: ellipsis;
}
.view-geometry-property-table td {
overflow: hidden;
text-overflow: ellipsis;
}
/* For geometry viewer info control */
.geometry-viewer-info-control {
padding: 5px;
background: white;
border: 2px solid rgba(0, 0, 0, 0.2);
background-clip: padding-box;
border-radius: 2px;
}
.geometry-viewer-info-control i{
margin: 0 0 0 4px;
}
.hide-vertical-scrollbar {
overflow-y: hidden;
}
/* Macros */
.macro-tab {
top: 0px !important;
}
.macro-tab .tab-pane {
padding: 0px !important;
}
.macro_dialog .CodeMirror {
overflow-y: auto;
resize: vertical;
}
.macro_dialog .sql-cell > div {
overflow-y: auto;
resize: vertical;
}
.macro_dialog .CodeMirror-cursor {
width: 1px !important;
height: 18px !important;
}
.macro_dialog .pg-prop-status-bar {
z-index: 1;
}
.new-connection-dialog-style {
width: 100% !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,424 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import * as showViewData from './show_view_data';
import * as showQueryTool from './show_query_tool';
import * as toolBar from 'pgadmin.browser.toolbar';
import * as panelTitleFunc from './sqleditor_title';
import * as commonUtils from 'sources/utils';
import $ from 'jquery';
import url_for from 'sources/url_for';
import _ from 'lodash';
import alertify from 'pgadmin.alertifyjs';
var wcDocker = window.wcDocker;
import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'pgadmin.browser';
import 'pgadmin.file_manager';
import 'pgadmin.tools.user_management';
import gettext from 'sources/gettext';
import React from 'react';
import ReactDOM from 'react-dom';
import QueryToolComponent from './components/QueryToolComponent';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
export function setPanelTitle(queryToolPanel, panelTitle) {
queryToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
}
export default class SQLEditor {
static instance;
static getInstance(...args) {
if(!SQLEditor.instance) {
SQLEditor.instance = new SQLEditor(...args);
}
return SQLEditor.instance;
}
SUPPORTED_NODES = [
'table', 'view', 'mview',
'foreign_table', 'catalog_object', 'partition',
];
/* Enable/disable View data menu in tools based
* on node selected. if selected node is present
* in supportedNodes, menu will be enabled
* otherwise disabled.
*/
viewMenuEnabled(obj) {
var isEnabled = (() => {
if (!_.isUndefined(obj) && !_.isNull(obj))
return (_.indexOf(this.SUPPORTED_NODES, obj._type) !== -1 ? true : false);
else
return false;
})();
toolBar.enable(gettext('View Data'), isEnabled);
toolBar.enable(gettext('Filtered Rows'), isEnabled);
return isEnabled;
}
/* Enable/disable Query tool menu in tools based
* on node selected. if selected node is present
* in unsupported_nodes, menu will be disabled
* otherwise enabled.
*/
queryToolMenuEnabled(obj) {
var isEnabled = (() => {
if (!_.isUndefined(obj) && !_.isNull(obj)) {
if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) {
if (obj._type == 'database' && obj.allowConn) {
return true;
} else if (obj._type != 'database') {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
})();
toolBar.enable(gettext('Query Tool'), isEnabled);
return isEnabled;
}
init() {
if(this.initialized)
return;
this.initialized = true;
let self = this;
/* Cache may take time to load for the first time
* Keep trying till available
*/
let cacheIntervalId = setInterval(function() {
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('sqleditor');
clearInterval(cacheIntervalId);
}
},0);
pgBrowser.onPreferencesChange('sqleditor', function() {
self.preferences = pgBrowser.get_preferences_for_module('sqleditor');
});
// Define the nodes on which the menus to be appear
var menus = [{
name: 'query_tool',
module: this,
applies: ['tools'],
callback: 'showQueryTool',
enable: self.queryToolMenuEnabled,
priority: 1,
label: gettext('Query Tool'),
icon: 'pg-font-icon icon-query_tool',
data:{
applies: 'tools',
data_disabled: gettext('Please select a database from the browser tree to access Query Tool.'),
},
}];
// Create context menu
for (const supportedNode of self.SUPPORTED_NODES) {
menus.push({
name: 'view_all_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 3,
},
applies: ['context', 'object'],
callback: 'showViewData',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 101,
label: gettext('All Rows'),
}, {
name: 'view_first_100_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 1,
},
applies: ['context', 'object'],
callback: 'showViewData',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 102,
label: gettext('First 100 Rows'),
}, {
name: 'view_last_100_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 2,
},
applies: ['context', 'object'],
callback: 'showViewData',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 103,
label: gettext('Last 100 Rows'),
}, {
name: 'view_filtered_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 4,
},
applies: ['context', 'object'],
callback: 'showFilteredRow',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 104,
label: gettext('Filtered Rows...'),
});
}
pgBrowser.add_menu_category('view_data', gettext('View/Edit Data'), 100, '');
pgBrowser.add_menus(menus);
// Creating a new pgAdmin.Browser frame to show the data.
var frame = new pgAdmin.Browser.Frame({
name: 'frm_sqleditor',
showTitle: true,
isCloseable: true,
isRenamable: true,
isPrivate: true,
url: 'about:blank',
});
// Load the newly created frame
frame.load(pgBrowser.docker);
}
// This is a callback function to show data when user click on menu item.
showViewData(data, i) {
const transId = commonUtils.getRandomInt(1, 9999999);
showViewData.showViewData(this, pgBrowser, alertify, data, i, transId);
}
// This is a callback function to show filtered data when user click on menu item.
showFilteredRow(data, i) {
const transId = commonUtils.getRandomInt(1, 9999999);
showViewData.showViewData(this, pgBrowser, alertify, data, i, transId, true, this.preferences);
}
// This is a callback function to show query tool when user click on menu item.
showQueryTool(url, treeIdentifier) {
const transId = commonUtils.getRandomInt(1, 9999999);
var t = pgBrowser.tree,
i = treeIdentifier || t.selected(),
d = i ? t.itemData(i) : undefined;
//Open query tool with create script if copy_sql_to_query_tool is true else open blank query tool
var preference = pgBrowser.get_preference('sqleditor', 'copy_sql_to_query_tool');
if(preference.value && !d._type.includes('coll-') && (url === '' || url['applies'] === 'tools')){
var stype = d._type.toLowerCase();
var data = {
'script': stype,
data_disabled: gettext('The selected tree node does not support this option.'),
};
pgBrowser.Node.callbacks.show_script(data);
} else {
if(d._type.includes('coll-')){
url = '';
}
showQueryTool.showQueryTool(this, pgBrowser, url, treeIdentifier, transId);
}
}
onPanelRename(queryToolPanel, panelData, is_query_tool) {
var temp_title = panelData.$titleText[0].textContent;
var is_dirty_editor = queryToolPanel.is_dirty_editor ? queryToolPanel.is_dirty_editor : false;
var title = queryToolPanel.is_dirty_editor ? panelData.$titleText[0].textContent.replace(/.$/, '') : temp_title;
alertify.prompt('', title,
// We will execute this function when user clicks on the OK button
function(evt, value) {
// Remove the leading and trailing white spaces.
value = value.trim();
if(value) {
var is_file = false;
if(panelData.$titleText[0].innerHTML.includes('File - ')) {
is_file = true;
}
var selected_item = pgBrowser.tree.selected();
var panel_titles = '';
if(is_query_tool) {
panel_titles = panelTitleFunc.getPanelTitle(pgBrowser, selected_item, value);
} else {
panel_titles = showViewData.generateDatagridTitle(pgBrowser, selected_item, value);
}
// Set title to the selected tab.
if (is_dirty_editor) {
panel_titles = panel_titles + ' *';
}
panelTitleFunc.setQueryToolDockerTitle(queryToolPanel, is_query_tool, _.unescape(panel_titles), is_file);
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function(evt) { evt.cancel = false; }
).set({'title': gettext('Rename Panel')});
}
openQueryToolPanel(trans_id, is_query_tool, panel_title, closeUrl, queryToolForm) {
let self = this;
var browser_preferences = pgBrowser.get_preferences_for_module('browser');
var propertiesPanel = pgBrowser.docker.findPanels('properties');
var queryToolPanel = pgBrowser.docker.addPanel('frm_sqleditor', wcDocker.DOCK.STACKED, propertiesPanel[0]);
queryToolPanel.trans_id = trans_id;
showQueryTool._set_dynamic_tab(pgBrowser, browser_preferences['dynamic_tabs']);
// Set panel title and icon
panelTitleFunc.setQueryToolDockerTitle(queryToolPanel, is_query_tool, _.unescape(panel_title));
queryToolPanel.focus();
// Listen on the panel closed event.
if (queryToolPanel.isVisible()) {
queryToolPanel.on(wcDocker.EVENT.CLOSED, function() {
$.ajax({
url: closeUrl,
method: 'DELETE',
});
});
}
queryToolPanel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
queryToolPanel.trigger(wcDocker.EVENT.RESIZED);
});
commonUtils.registerDetachEvent(queryToolPanel);
// Listen on the panelRename event.
queryToolPanel.on(wcDocker.EVENT.RENAME, function(panelData) {
self.onPanelRename(queryToolPanel, panelData, is_query_tool);
});
var openQueryToolURL = function(j) {
// add spinner element
let $spinner_el =
$(`<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>`).appendTo($(j).data('embeddedFrame').$container);
let init_poller_id = setInterval(function() {
var frameInitialized = $(j).data('frameInitialized');
if (frameInitialized) {
clearInterval(init_poller_id);
var frame = $(j).data('embeddedFrame');
if (frame) {
frame.onLoaded(()=>{
$spinner_el.remove();
});
frame.openHTML(queryToolForm);
}
}
}, 100);
};
openQueryToolURL(queryToolPanel);
}
launch(trans_id, panel_url, is_query_tool, panel_title, sURL=null, sql_filter=null) {
const self = this;
let closeUrl = url_for('sqleditor.close', {'trans_id': trans_id});
let queryToolForm = `
<form id="queryToolForm" action="${panel_url}" method="post">
<input id="title" name="title" hidden />
<input id="conn_title" name="conn_title" hidden />
<input name="close_url" value="${closeUrl}" hidden />`;
if(sURL){
queryToolForm +=`<input name="query_url" value="${sURL}" hidden />`;
}
if(sql_filter) {
queryToolForm +=`<textarea name="sql_filter" hidden>${sql_filter}</textarea>`;
}
/* Escape backslashes as it is stripped by back end */
queryToolForm +=`
</form>
<script>
document.getElementById("title").value = "${_.escape(panel_title.replace('\\', '\\\\'))}";
document.getElementById("queryToolForm").submit();
</script>
`;
var browser_preferences = pgBrowser.get_preferences_for_module('browser');
var open_new_tab = browser_preferences.new_browser_tab_open;
if (open_new_tab && open_new_tab.includes('qt')) {
var newWin = window.open('', '_blank');
if(newWin) {
newWin.document.write(queryToolForm);
newWin.document.title = panel_title;
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function() {
pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
return false;
}
} else {
/* On successfully initialization find the dashboard panel,
* create new panel and add it to the dashboard panel.
*/
self.openQueryToolPanel(trans_id, is_query_tool, panel_title, closeUrl, queryToolForm);
}
return true;
}
setupPreferencesWorker() {
if (window.location == window.parent?.location) {
/* Sync the local preferences with the main window if in new tab */
setInterval(()=>{
if(pgWindow?.pgAdmin) {
if(pgAdmin.Browser.preference_version() < pgWindow.pgAdmin.Browser.preference_version()){
pgAdmin.Browser.preferences_cache = pgWindow.pgAdmin.Browser.preferences_cache;
pgAdmin.Browser.preference_version(pgWindow.pgAdmin.Browser.preference_version());
pgAdmin.Browser.triggerPreferencesChange('browser');
pgAdmin.Browser.triggerPreferencesChange('sqleditor');
}
}
}, 1000);
}
}
loadComponent(container, params) {
let panel = null;
let selectedNodeInfo = pgWindow.pgAdmin.Browser.tree.getTreeNodeHierarchy(
pgWindow.pgAdmin.Browser.tree.selected()
);
_.each(pgWindow.pgAdmin.Browser.docker.findPanels('frm_sqleditor'), function(p) {
if (p.trans_id == params.trans_id) {
panel = p;
}
});
this.setupPreferencesWorker();
ReactDOM.render(
<ModalProvider>
<QueryToolComponent params={params} pgWindow={pgWindow} pgAdmin={pgAdmin} panel={panel} selectedNodeInfo={selectedNodeInfo}/>
</ModalProvider>,
container
);
}
}

View File

@@ -0,0 +1,646 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useCallback, useRef, useMemo, useState, useEffect} from 'react';
import _ from 'lodash';
import Layout, { LayoutHelper } from '../../../../../static/js/helpers/Layout';
import EventBus from '../../../../../static/js/helpers/EventBus';
import Query from './sections/Query';
import { ConnectionBar } from './sections/ConnectionBar';
import { ResultSet } from './sections/ResultSet';
import { StatusBar } from './sections/StatusBar';
import { MainToolBar } from './sections/MainToolBar';
import { Messages } from './sections/Messages';
import Theme from 'sources/Theme';
import getApiInstance, {parseApiError} from '../../../../../static/js/api_instance';
import url_for from 'sources/url_for';
import { PANELS, QUERY_TOOL_EVENTS, CONNECTION_STATUS } from './QueryToolConstants';
import { useInterval } from '../../../../../static/js/custom_hooks';
import { Box } from '@material-ui/core';
import { getDatabaseLabel, getTitle, setQueryToolDockerTitle } from '../sqleditor_title';
import gettext from 'sources/gettext';
import NewConnectionDialog from './dialogs/NewConnectionDialog';
import { evalFunc } from '../../../../../static/js/utils';
import { Notifications } from './sections/Notifications';
import MacrosDialog from './dialogs/MacrosDialog';
import Notifier from '../../../../../static/js/helpers/Notifier';
import FilterDialog from './dialogs/FilterDialog';
import { QueryHistory } from './sections/QueryHistory';
import * as showQueryTool from '../show_query_tool';
import * as commonUtils from 'sources/utils';
import * as Kerberos from 'pgadmin.authenticate.kerberos';
import PropTypes from 'prop-types';
import { retrieveNodeName } from '../show_view_data';
import 'wcdocker';
import { useModal } from '../../../../../static/js/helpers/ModalProvider';
export const QueryToolContext = React.createContext();
export const QueryToolConnectionContext = React.createContext();
export const QueryToolEventsContext = React.createContext();
function fetchConnectionStatus(api, transId) {
return api.get(url_for('sqleditor.connection_status', {trans_id: transId}));
}
function initConnection(api, params, passdata) {
return api.post(url_for('NODE-server.connect_id', params), passdata);
}
function setPanelTitle(panel, title, qtState, dirty=false) {
if(title) {
title =title.split('\\').pop().split('/').pop();
} else if(qtState.current_file) {
title = qtState.current_file.split('\\').pop().split('/').pop();
} else {
title = qtState.params.title || 'Untitled';
}
title = title + (dirty ? '*': '');
if (qtState.is_new_tab) {
window.document.title = title;
} else {
setQueryToolDockerTitle(panel, true, title, qtState.current_file ? true : false);
}
}
export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedNodeInfo, panel, eventBusObj}) {
const containerRef = React.useRef(null);
const forceClose = React.useRef(false);
const [qtState, _setQtState] = useState({
preferences: {
browser: {}, sqleditor: {},
},
is_new_tab: window.location == window.parent?.location,
current_file: null,
obtaining_conn: true,
connected: false,
connection_status: null,
connection_status_msg: '',
params: {
...params,
is_query_tool: params.is_query_tool == 'true' ? true : false,
node_name: retrieveNodeName(selectedNodeInfo),
},
connection_list: [{
sgid: params.sgid,
sid: params.sid,
did: params.did,
user: params.username,
role: null,
title: _.unescape(params.title),
conn_title: getTitle(
pgAdmin, null, selectedNodeInfo, true, params.server_name, params.database_name || getDatabaseLabel(selectedNodeInfo),
params.username, params.is_query_tool == 'true' ? true : false),
server_name: params.server_name,
database_name: params.database_name || getDatabaseLabel(selectedNodeInfo),
is_selected: true,
}],
});
const setQtState = (state)=>{
_setQtState((prev)=>({...prev,...evalFunc(null, state, prev)}));
};
const eventBus = useRef(eventBusObj || (new EventBus()));
const docker = useRef(null);
const api = useMemo(()=>getApiInstance(), []);
const modal = useModal();
/* Connection status poller */
let pollTime = qtState.preferences.sqleditor.connection_status_fetch_time > 0 ?
qtState.preferences.sqleditor.connection_status_fetch_time*1000 : -1;
/* No need to poll when the query is executing. Query poller will the txn status */
if(qtState.connection_status === CONNECTION_STATUS.TRANSACTION_STATUS_ACTIVE && qtState.connected) {
pollTime = -1;
}
useInterval(async ()=>{
try {
let {data: respData} = await fetchConnectionStatus(api, qtState.params.trans_id);
if(respData.data) {
setQtState({
connected: true,
connection_status: respData.data.status,
});
} else {
setQtState({
connected: false,
connection_status: null,
connection_status_msg: gettext('An unexpected error occurred - ensure you are logged into the application.')
});
}
if(respData.data.notifies) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.PUSH_NOTICE, respData.data.notifies);
}
} catch (error) {
console.error(error);
setQtState({
connected: false,
connection_status: null,
connection_status_msg: parseApiError(error),
});
}
}, pollTime);
let defaultLayout = {
dockbox: {
mode: 'vertical',
children: [
{
mode: 'horizontal',
children: [
{
tabs: [
LayoutHelper.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: <Query />}),
LayoutHelper.getPanel({id: PANELS.HISTORY, title: 'Query History', content: <QueryHistory />,
cached: undefined}),
],
},
{
size: 75,
tabs: [
LayoutHelper.getPanel({
id: PANELS.SCRATCH, title: gettext('Scratch Pad'),
closable: true,
content: <textarea style={{
border: 0,
height: '100%',
width: '100%',
resize: 'none'
}}/>
}),
]
}
]
},
{
mode: 'horizontal',
children: [
{
tabs: [
LayoutHelper.getPanel({
id: PANELS.DATA_OUTPUT, title: 'Data output', content: <ResultSet />,
}),
LayoutHelper.getPanel({
id: PANELS.MESSAGES, title:'Messages', content: <Messages />,
}),
LayoutHelper.getPanel({
id: PANELS.NOTIFICATIONS, title:'Notifications', content: <Notifications />,
}),
],
}
]
},
]
},
};
const reflectPreferences = useCallback(()=>{
setQtState({preferences: {
browser: pgWindow.pgAdmin.Browser.get_preferences_for_module('browser'),
sqleditor: pgWindow.pgAdmin.Browser.get_preferences_for_module('sqleditor'),
}});
}, []);
const getSQLScript = ()=>{
// Fetch the SQL for Scripts (eg: CREATE/UPDATE/DELETE/SELECT)
// Call AJAX only if script type url is present
if(qtState.params.is_query_tool && qtState.params.query_url) {
api.get(qtState.params.query_url)
.then((res)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, res.data);
})
.catch((err)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
} else if(qtState.params.sql_id) {
let sqlValue = localStorage.getItem(qtState.params.sql_id);
localStorage.removeItem(qtState.params.sql_id);
if(sqlValue) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, sqlValue);
}
}
};
const initializeQueryTool = ()=>{
let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
let baseUrl = '';
if(qtState.params.is_query_tool) {
let endpoint = 'sqleditor.initialize_sqleditor';
if(qtState.params.did) {
endpoint = 'sqleditor.initialize_sqleditor_with_did';
}
baseUrl = url_for(endpoint, {
...selectedConn,
trans_id: qtState.params.trans_id,
});
} else {
baseUrl = url_for('sqleditor.initialize_viewdata', {
...qtState.params,
});
}
api.post(baseUrl, qtState.params.is_query_tool ? null : JSON.stringify(qtState.params.sql_filter))
.then(()=>{
setQtState({
connected: true,
obtaining_conn: false,
});
if(!qtState.params.is_query_tool) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
}
}).catch((err)=>{
if(err.response?.request?.responseText?.search('Ticket expired') !== -1) {
Kerberos.fetch_ticket()
.then(()=>{
initializeQueryTool();
})
.catch((kberr)=>{
setQtState({
connected: false,
obtaining_conn: false,
});
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, kberr);
});
}
setQtState({
connected: false,
obtaining_conn: false,
});
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
};
useEffect(()=>{
getSQLScript();
initializeQueryTool();
eventBus.current.registerListener(QUERY_TOOL_EVENTS.FOCUS_PANEL, (panelId)=>{
LayoutHelper.focus(docker.current, panelId);
});
eventBus.current.registerListener(QUERY_TOOL_EVENTS.SET_CONNECTION_STATUS, (status)=>{
setQtState({connection_status: status});
});
eventBus.current.registerListener(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL, ()=>{
panel.off(window.wcDocker.EVENT.CLOSING);
panel.close();
});
reflectPreferences();
pgWindow.pgAdmin.Browser.onPreferencesChange('sqleditor', function() {
reflectPreferences();
});
/* WC docker events */
panel?.on(window.wcDocker.EVENT.CLOSING, function() {
if(!forceClose.current) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.WARN_SAVE_DATA_CLOSE);
} else {
panel.close();
}
});
pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:select_file', (fileName)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName);
}, pgAdmin);
pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', (fileName)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, fileName);
}, pgAdmin);
}, []);
useEffect(()=>{
const pushHistory = (h)=>{
api.post(
url_for('sqleditor.add_query_history', {
'trans_id': qtState.params.trans_id,
}),
JSON.stringify(h),
).catch((error)=>{console.error(error);});
};
eventBus.current.registerListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
return ()=>{eventBus.current.deregisterListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);};
}, [qtState.params.trans_id]);
const handleApiError = (error, handleParams)=>{
if(error.response && pgAdmin.Browser?.UserManagement?.isPgaLoginRequired(error.response)) {
return pgAdmin.Browser.UserManagement.pgaLogin();
}
if(error.response?.status == 503 && error.response.data?.info == 'CONNECTION_LOST') {
// We will display re-connect dialog, no need to display error message again
modal.confirm(
gettext('Connection Warning'),
<p>
<span>{gettext('The application has lost the database connection:')}</span>
<br/><span>{gettext(' If the connection was idle it may have been forcibly disconnected.')}</span>
<br/><span>{gettext(' The application server or database server may have been restarted.')}</span>
<br/><span>{gettext(' The user session may have timed out.')}</span>
<br />
<span>{gettext('Do you want to continue and establish a new session')}</span>
</p>,
function() {
handleParams?.connectionLostCallback?.();
}, null,
gettext('Continue'),
gettext('Cancel')
);
} else if(handleParams?.checkTransaction && error.response?.data.info == 'DATAGRID_TRANSACTION_REQUIRED') {
let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
initConnection(api, {
'gid': selectedConn.sgid,
'sid': selectedConn.sid,
'did': selectedConn.did,
'role': selectedConn.role,
}).then(()=>{
initializeQueryTool();
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
} else {
let msg = parseApiError(error);
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, msg, true);
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.MESSAGES);
Notifier.error(msg);
}
};
useEffect(()=>{
const fileDone = (fileName, success=true)=>{
if(success) {
setQtState({
current_file: fileName,
});
setPanelTitle(panel, fileName, {...qtState, current_file: fileName});
}
};
const events = [
[QUERY_TOOL_EVENTS.TRIGGER_LOAD_FILE, ()=>{
let fileParams = {
'supported_types': ['*', 'sql'], // file types allowed
'dialog_type': 'select_file', // open select file dialog
};
pgAdmin.FileManager.init();
pgAdmin.FileManager.show_dialog(fileParams);
}],
[QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, (isSaveAs=false)=>{
if(!isSaveAs && qtState.current_file) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, qtState.current_file);
} else {
let fileParams = {
'supported_types': ['*', 'sql'],
'dialog_type': 'create_file',
'dialog_title': 'Save File',
'btn_primary': 'Save',
};
pgAdmin.FileManager.init();
pgAdmin.FileManager.show_dialog(fileParams);
}
}],
[QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileDone],
[QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileDone],
[QUERY_TOOL_EVENTS.QUERY_CHANGED, (isDirty)=>{
if(qtState.params.is_query_tool) {
setPanelTitle(panel, null, qtState, isDirty);
}
}],
[QUERY_TOOL_EVENTS.HANDLE_API_ERROR, handleApiError],
];
events.forEach((e)=>{
eventBus.current.registerListener(e[0], e[1]);
});
return ()=>{
events.forEach((e)=>{
eventBus.current.deregisterListener(e[0], e[1]);
});
};
}, [qtState]);
useEffect(()=>{
/* Fire query change so that title changes to latest */
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE);
}, [qtState.params.title]);
const updateQueryToolConnection = useCallback((connectionData, isNew=false)=>{
setQtState((prev)=>{
let newConnList = [...prev.connection_list];
if(isNew) {
newConnList.push(connectionData);
}
for (const connItem of newConnList) {
if(connectionData.sid == connItem.sid
&& connectionData.did == connItem.did
&& connectionData.user == connItem.user
&& connectionData.role == connItem.role) {
connItem.is_selected = true;
} else {
connItem.is_selected = false;
}
}
return {
connection_list: newConnList,
};
});
setQtState((prev)=>{
return {
params: {
...prev.params,
sid: connectionData.sid,
did: connectionData.did,
title: connectionData.title,
},
obtaining_conn: true,
connected: false,
};
});
return api.post(url_for('sqleditor.update_sqleditor_connection', {
trans_id: qtState.params.trans_id,
sgid: connectionData.sgid,
sid: connectionData.sid,
did: connectionData.did
}), connectionData)
.then(({data: respData})=>{
setQtState((prev)=>{
return {
params: {
...prev.params,
trans_id: respData.data.trans_id,
},
connected: respData.data.trans_id ? true : false,
obtaining_conn: false,
};
});
let msg = `${connectionData['server_name']}/${connectionData['database_name']} - Database connected`;
Notifier.success(msg);
});
}, [qtState.params.trans_id]);
const onNewConnClick = useCallback(()=>{
const onClose = ()=>LayoutHelper.close(docker.current, 'new-conn');
LayoutHelper.openDialog(docker.current, {
id: 'new-conn',
title: gettext('Add new connection'),
content: <NewConnectionDialog onSave={(_isNew, data)=>{
let connectionData = {
sgid: 0,
sid: data.sid,
did: data.did,
user: data.user,
role: data.role && null,
title: getTitle(pgAdmin, qtState.preferences.browser, null, false, data.server_name, data.database_name, data.user, true),
conn_title: getTitle(pgAdmin, null, null, true, data.server_name, data.database_name, data.user, true),
server_name: data.server_name,
database_name: data.database_name,
is_selected: true,
};
updateQueryToolConnection(connectionData, true);
onClose();
return Promise.resolve();
}}
onClose={onClose}/>
});
}, [qtState.preferences.browser]);
const onNewQueryToolClick = ()=>{
const transId = commonUtils.getRandomInt(1, 9999999);
let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
let parentData = {
server_group: {
_id: selectedConn.sgid || 0,
},
server: {
_id: selectedConn.sid,
server_type: qtState.params.server_type,
},
database: {
_id: selectedConn.did,
label: selectedConn.database_name,
},
};
const gridUrl = showQueryTool.generateUrl(transId, parentData, null);
const title = getTitle(pgAdmin, qtState.preferences.browser, null, false, selectedConn.server_name, selectedConn.database_name, selectedConn.user);
showQueryTool.launchQueryTool(pgWindow.pgAdmin.Tools.SQLEditor, transId, gridUrl, title, '');
};
const onManageMacros = useCallback(()=>{
const onClose = ()=>LayoutHelper.close(docker.current, 'manage-macros');
LayoutHelper.openDialog(docker.current, {
id: 'manage-macros',
title: gettext('Manage Macros'),
content: <MacrosDialog onSave={(newMacros)=>{
setQtState((prev)=>{
return {
params: {
...prev.params,
macros: newMacros,
},
};
});
}}
onClose={onClose}/>
}, 850, 500);
}, [qtState.preferences.browser]);
const onFilterClick = useCallback(()=>{
const onClose = ()=>LayoutHelper.close(docker.current, 'filter-dialog');
LayoutHelper.openDialog(docker.current, {
id: 'filter-dialog',
title: gettext('Sort/Filter options'),
content: <FilterDialog onSave={()=>{
onClose();
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
}}
onClose={onClose}/>
}, 700, 400);
}, [qtState.preferences.browser]);
const onResetLayout = useCallback(()=>{
docker.current?.resetLayout();
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.QUERY);
}, []);
const queryToolContextValue = React.useMemo(()=>({
docker: docker.current,
api: api,
modal: modal,
params: qtState.params,
preferences: qtState.preferences,
}), [qtState.params, qtState.preferences]);
const queryToolConnContextValue = React.useMemo(()=>({
connected: qtState.connected,
obtainingConn: qtState.obtaining_conn,
connectionStatus: qtState.connection_status,
}), [qtState]);
/* Push only those things in context which do not change frequently */
return (
<QueryToolContext.Provider value={queryToolContextValue}>
<QueryToolConnectionContext.Provider value={queryToolConnContextValue}>
<QueryToolEventsContext.Provider value={eventBus.current}>
<Theme>
<Box width="100%" height="100%" display="flex" flexDirection="column" flexGrow="1" tabIndex="0" ref={containerRef}>
<ConnectionBar
connected={qtState.connected}
connecting={qtState.obtaining_conn}
connectionStatus={qtState.connection_status}
connectionStatusMsg={qtState.connection_status_msg}
connectionList={qtState.connection_list}
onConnectionChange={(connectionData)=>updateQueryToolConnection(connectionData)}
onNewConnClick={onNewConnClick}
onNewQueryToolClick={onNewQueryToolClick}
onResetLayout={onResetLayout}
docker={docker.current}
/>
<MainToolBar
containerRef={containerRef}
onManageMacros={onManageMacros}
onFilterClick={onFilterClick}
/>
<Layout
getLayoutInstance={(obj)=>docker.current=obj}
defaultLayout={defaultLayout}
layoutId="SQLEditor/Layout"
savedLayout={params.layout}
/>
<StatusBar />
</Box>
</Theme>
</QueryToolEventsContext.Provider>
</QueryToolConnectionContext.Provider>
</QueryToolContext.Provider>
);
}
QueryToolComponent.propTypes = {
params:PropTypes.shape({
trans_id: PropTypes.number.isRequired,
sgid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
sid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
did: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
server_type: PropTypes.string,
title: PropTypes.string.isRequired,
bgcolor: PropTypes.string,
fgcolor: PropTypes.string,
is_query_tool: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
username: PropTypes.string,
server_name: PropTypes.string,
database_name: PropTypes.string,
layout: PropTypes.string,
}),
pgWindow: PropTypes.object.isRequired,
pgAdmin: PropTypes.object.isRequired,
selectedNodeInfo: PropTypes.object,
panel: PropTypes.object,
eventBusObj: PropTypes.objectOf(EventBus),
};

View File

@@ -0,0 +1,97 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
export const QUERY_TOOL_EVENTS = {
TRIGGER_STOP_EXECUTION: 'TRIGGER_STOP_EXECUTION',
TRIGGER_EXECUTION: 'TRIGGER_EXECUTION',
TRIGGER_LOAD_FILE: 'TRIGGER_LOAD_FILE',
TRIGGER_SAVE_FILE: 'TRIGGER_SAVE_FILE',
TRIGGER_SAVE_DATA: 'TRIGGER_SAVE_DATA',
TRIGGER_DELETE_ROWS: 'TRIGGER_DELETE_ROWS',
TRIGGER_COPY_DATA: 'TRIGGER_COPY_DATA',
TRIGGER_ADD_ROWS: 'TRIGGER_ADD_ROWS',
TRIGGER_RENDER_GEOMETRIES: 'TRIGGER_RENDER_GEOMETRIES',
TRIGGER_SAVE_RESULTS: 'TRIGGER_SAVE_RESULTS',
TRIGGER_SAVE_RESULTS_END: 'TRIGGER_SAVE_RESULTS_END',
TRIGGER_PASTE_ROWS: 'TRIGGER_PASTE_ROWS',
TRIGGER_QUERY_CHANGE: 'TRIGGER_QUERY_CHANGE',
TRIGGER_INCLUDE_EXCLUDE_FILTER: 'TRIGGER_INCLUDE_EXCLUDE_FILTER',
TRIGGER_REMOVE_FILTER: 'TRIGGER_REMOVE_FILTER',
TRIGGER_SET_LIMIT: 'TRIGGER_SET_LIMIT',
TRIGGER_FORMAT_SQL: 'TRIGGER_FORMAT_SQL',
COPY_DATA: 'COPY_DATA',
SET_LIMIT_VALUE: 'SET_LIMIT_VALUE',
SET_CONNECTION_STATUS: 'SET_CONNECTION_STATUS',
EXECUTION_START: 'EXECUTION_START',
EXECUTION_END: 'EXECUTION_END',
STOP_QUERY: 'STOP_QUERY',
CURSOR_ACTIVITY: 'CURSOR_ACTIVITY',
SET_MESSAGE: 'SET_MESSAGE',
ROWS_FETCHED: 'ROWS_FETCHED',
SELECTED_ROWS_COLS_CHANGED: 'SELECTED_ROWS_COLS_CHANGED',
DATAGRID_CHANGED: 'DATAGRID_CHANGED',
HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR',
FOCUS_PANEL: 'FOCUS_PANEL',
LOAD_FILE: 'LOAD_FILE',
LOAD_FILE_DONE: 'LOAD_FILE_DONE',
SAVE_FILE: 'SAVE_FILE',
SAVE_FILE_DONE: 'SAVE_FILE_DONE',
QUERY_CHANGED: 'QUERY_CHANGED',
API_ERROR: 'API_ERROR',
SAVE_DATA_DONE: 'SAVE_DATA_DONE',
TASK_START: 'TASK_START',
TASK_END: 'TASK_END',
RENDER_GEOMETRIES: 'RENDER_GEOMETRIES',
PUSH_NOTICE: 'PUSH_NOTICE',
PUSH_HISTORY: 'PUSH_HISTORY',
HANDLE_API_ERROR: 'HANDLE_API_ERROR',
SET_FILTER_INFO: 'SET_FILTER_INFO',
FETCH_MORE_ROWS: 'FETCH_MORE_ROWS',
EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE',
EDITOR_EXEC_CMD: 'EDITOR_EXEC_CMD',
EDITOR_SET_SQL: 'EDITOR_SET_SQL',
COPY_TO_EDITOR: 'COPY_TO_EDITOR',
WARN_SAVE_DATA_CLOSE: 'WARN_SAVE_DATA_CLOSE',
WARN_SAVE_TEXT_CLOSE: 'WARN_SAVE_TEXT_CLOSE',
WARN_TXN_CLOSE: 'WARN_TXN_CLOSE',
RESET_LAYOUT: 'RESET_LAYOUT',
FORCE_CLOSE_PANEL: 'FORCE_CLOSE_PANEL',
};
export const CONNECTION_STATUS = {
TRANSACTION_STATUS_IDLE: 0,
TRANSACTION_STATUS_ACTIVE: 1,
TRANSACTION_STATUS_INTRANS: 2,
TRANSACTION_STATUS_INERROR: 3,
TRANSACTION_STATUS_UNKNOWN: 4,
};
export const CONNECTION_STATUS_MESSAGE = {
[CONNECTION_STATUS.TRANSACTION_STATUS_IDLE]: gettext('The session is idle and there is no current transaction.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_ACTIVE]: gettext('A command is currently in progress.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_INTRANS]: gettext('The session is idle in a valid transaction block.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_INERROR]: gettext('The session is idle in a failed transaction block.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_UNKNOWN]: gettext('The connection with the server is bad.')
};
export const PANELS = {
QUERY: 'id-query',
MESSAGES: 'id-messages',
SCRATCH: 'id-scratch',
DATA_OUTPUT: 'id-dataoutput',
EXPLAIN: 'id-explain',
GEOMETRY: 'id-geometry',
NOTIFICATIONS: 'id-notifications',
HISTORY: 'id-history',
};

View File

@@ -0,0 +1,99 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import JSONBigNumber from 'json-bignumber';
import _ from 'lodash';
import * as clipboard from '../../../../../../static/js/clipboard';
import { CSVToArray } from '../../../../../../static/js/utils';
export default class CopyData {
constructor(options) {
this.CSVOptions = {
field_separator: '\t',
quote_char: '"',
quoting: 'strings',
...options,
};
}
setCSVOptions(options) {
this.CSVOptions = {
...this.CSVOptions,
...options,
};
}
copyRowsToCsv(rows=[], columns=[], withHeaders=false) {
let csvRows = rows.reduce((prevCsvRows, currRow)=>{
let csvRow = columns.reduce((prevCsvCols, column)=>{
prevCsvCols.push(this.csvCell(currRow[column.key], column));
return prevCsvCols;
}, []).join(this.CSVOptions.field_separator);
prevCsvRows.push(csvRow);
return prevCsvRows;
}, []);
if(withHeaders) {
let csvRow = columns.reduce((prevCsvCols, column)=>{
prevCsvCols.push(this.csvCell(column.name, column, true));
return prevCsvCols;
}, []).join(this.CSVOptions.field_separator);
csvRows.unshift(csvRow);
}
clipboard.copyToClipboard(csvRows.join('\n'));
localStorage.setItem('copied-with-headers', withHeaders);
}
escape(iStr) {
return (this.CSVOptions.quote_char == '"') ?
iStr.replace(/\"/g, '""') : iStr.replace(/\'/g, '\'\'');
}
allQuoteCell(value) {
if (value && _.isObject(value)) {
value = this.CSVOptions.quote_char + JSONBigNumber.stringify(value) + this.CSVOptions.quote_char;
} else if (value) {
value = this.CSVOptions.quote_char + this.escape(value.toString()) + this.CSVOptions.quote_char;
} else if (_.isNull(value) || _.isUndefined(value)) {
value = '';
}
return value;
}
stringQuoteCell(value, column) {
if (value && _.isObject(value)) {
value = this.CSVOptions.quote_char + JSONBigNumber.stringify(value) + this.CSVOptions.quote_char;
} else if (value && column.cell != 'number' && column.cell != 'boolean') {
value = this.CSVOptions.quote_char + this.escape(value.toString()) + this.CSVOptions.quote_char;
} else if (column.cell == 'string' && _.isNull(value)){
value = null;
} else if (_.isNull(value) || _.isUndefined(value)) {
value = '';
}
return value;
}
csvCell(value, column, header=false) {
if (this.CSVOptions.quoting == 'all' || header) {
value = this.allQuoteCell(value);
} else if(this.CSVOptions.quoting == 'strings') {
value = this.stringQuoteCell(value, column);
}
return value;
}
getCopiedRows() {
let copiedText = clipboard.getFromClipboard();
let copiedRows = CSVToArray(copiedText, this.CSVOptions.field_separator, this.CSVOptions.quote_char);
if(localStorage.getItem('copied-with-headers') == 'true') {
copiedRows = copiedRows.slice(1);
}
return copiedRows;
}
}

View File

@@ -0,0 +1,345 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles, Box, Portal } from '@material-ui/core';
import React, {useContext} from 'react';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import CloseIcon from '@material-ui/icons/Close';
import gettext from 'sources/gettext';
import clsx from 'clsx';
import JSONBigNumber from 'json-bignumber';
import JsonEditor from '../../../../../../static/js/components/JsonEditor';
import PropTypes from 'prop-types';
import { RowInfoContext } from '.';
import Notifier from '../../../../../../static/js/helpers/Notifier';
const useStyles = makeStyles((theme)=>({
textEditor: {
position: 'absolute',
zIndex: 1050,
backgroundColor: theme.palette.background.default,
padding: '0.25rem',
fontSize: '12px',
...theme.mixins.panelBorder.all,
left: 0,
// bottom: 0,
top: 0,
'& textarea': {
width: '250px',
height: '80px',
border: 0,
outline: 0,
resize: 'both',
}
},
jsonEditor: {
position: 'absolute',
zIndex: 1050,
backgroundColor: theme.palette.background.default,
...theme.mixins.panelBorder,
padding: '0.25rem',
'& .jsoneditor-div': {
fontSize: '12px',
minWidth: '525px',
minHeight: '300px',
...theme.mixins.panelBorder.all,
outline: 0,
resize: 'both',
overflow: 'auto',
},
'& .jsoneditor': {
height: 'abc',
border: 'none',
'& .ace-jsoneditor .ace_marker-layer .ace_active-line': {
background: theme.palette.primary.light
}
}
},
buttonMargin: {
marginLeft: '0.5rem',
},
textarea: {
resize: 'both'
},
input: {
appearance: 'none',
width: '100%',
height: '100%',
verticalAlign: 'top',
outline: 'none',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
border: 0,
boxShadow: 'inset 0 0 0 1.5px '+theme.palette.primary.main,
padding: '0 2px',
'::selection': {
background: theme.palette.primary.light,
}
},
check: {
display: 'inline-block',
verticalAlign: 'top',
width: '16px',
height: '16px',
border: '1px solid '+theme.palette.grey[800],
margin: '3px',
textAlign: 'center',
lineHeight: '16px',
'&.checked, &.unchecked': {
background: theme.palette.background.default,
},
'&.checked:after': {
content: '\'\\2713\'',
fontWeight: 'bold',
},
'&.intermediate': {
background: theme.palette.grey[200],
'&:after': {
content: '\'\\003F\'',
fontWeight: 'bold',
},
},
}
}));
function autoFocusAndSelect(input) {
input?.focus();
input?.select();
}
function isValidArray(val) {
val = val?.trim();
return !(val != '' && (val.charAt(0) != '{' || val.charAt(val.length - 1) != '}'));
}
function setEditorPosition(cellEle, editorEle) {
if(!editorEle || !cellEle) {
return;
}
let gridEle = cellEle.closest('.rdg');
let cellRect = cellEle.getBoundingClientRect();
let position = {
left: cellRect.left,
top: cellRect.top - editorEle.offsetHeight + 12,
};
if ((position.left + editorEle.offsetWidth + 10) > gridEle.offsetWidth) {
position.left -= position.left + editorEle.offsetWidth - gridEle.offsetWidth + 10;
}
editorEle.style.left = position.left + 'px';
editorEle.style.top = position.top + 'px';
}
const EditorPropTypes = {
row: PropTypes.object,
column: PropTypes.object,
onRowChange: PropTypes.func,
onClose: PropTypes.func
};
function textColumnFinalVal(columnVal, column) {
if(columnVal === '') {
columnVal = null;
} else if (!column.is_array) {
if (columnVal === '\'\'' || columnVal === '""') {
columnVal = '';
} else if (columnVal === '\\\'\\\'') {
columnVal = '\'\'';
} else if (columnVal === '\\"\\"') {
columnVal = '""';
}
}
return columnVal;
}
export function TextEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const value = row[column.key] ?? '';
const [localVal, setLocalVal] = React.useState(value);
const {getCellElement} = useContext(RowInfoContext);
const onChange = React.useCallback((e)=>{
setLocalVal(e.target.value);
}, []);
const onOK = ()=>{
if(column.is_array && !isValidArray(value)) {
console.error(gettext('Arrays must start with "{" and end with "}"'));
} else {
let columnVal = textColumnFinalVal(localVal, column);
onRowChange({ ...row, [column.key]: columnVal}, true);
onClose();
}
};
return(
<Portal container={document.body}>
<Box ref={(ele)=>{
setEditorPosition(getCellElement(column.idx), ele);
}} className={classes.textEditor}>
<textarea ref={autoFocusAndSelect} className={classes.textarea} value={localVal} onChange={onChange} />
<Box display="flex" justifyContent="flex-end">
<DefaultButton startIcon={<CloseIcon />} onClick={()=>onClose(false)} size="small">
{gettext('Cancel')}
</DefaultButton>
{column.can_edit &&
<>
<PrimaryButton startIcon={<CheckRoundedIcon />} onClick={onOK} size="small" className={classes.buttonMargin}>
{gettext('OK')}
</PrimaryButton>
</>}
</Box>
</Box>
</Portal>
);
}
TextEditor.propTypes = EditorPropTypes;
export function NumberEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const value = row[column.key] ?? '';
const onBlur = ()=>{
if(!column.is_array && isNaN(value)){
Notifier.error(gettext('Please enter a valid number'));
return;
} else if(column.is_array) {
if(!isValidArray(value)) {
Notifier.error(gettext('Arrays must start with "{" and end with "}"'));
return;
}
let checkVal = value.trim().slice(1, -1);
if(checkVal == '') {
checkVal = [];
} else {
checkVal = checkVal.split(',');
}
for (const val of checkVal) {
if(isNaN(val)) {
Notifier.error(gettext('Arrays must start with "{" and end with "}"'));
return;
}
}
}
onClose(column.can_edit ? true : false);
};
const onKeyDown = (e)=>{
if(e.code == 'Tab') {
e.preventDefault();
onBlur();
}
};
return (
<input
className={classes.input}
ref={autoFocusAndSelect}
value={value}
onChange={(e)=>{
if(column.can_edit) {
onRowChange({ ...row, [column.key]: e.target.value });
}
}}
// onBlur={onBlur}
onKeyDown={onKeyDown}
/>
);
}
NumberEditor.propTypes = EditorPropTypes;
export function CheckboxEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const value = row[column.key] ?? null;
const changeValue = ()=>{
if(!column.can_edit) {
return;
}
let newVal = true;
if(value) {
newVal = false;
} else if(value != null && !value) {
newVal = null;
}
onRowChange({ ...row, [column.key]: newVal});
};
const onBlur = ()=>{onClose(true);};
let className = 'checked';
if(!value) {
className = 'unchecked';
} else if(value == null){
className = 'intermediate';
}
return (
<div onClick={changeValue} tabIndex="0" onBlur={onBlur}>
<span className={clsx(classes.check, className)}></span>
</div>
);
}
CheckboxEditor.propTypes = EditorPropTypes;
export function JsonTextEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const {getCellElement} = useContext(RowInfoContext);
const value = React.useMemo(()=>{
let newVal = row[column.key] ?? null;
/* If jsonb or array */
if(column.column_type_internal === 'jsonb' && !Array.isArray(newVal) && newVal != null) {
newVal = JSONBigNumber.stringify(JSONBigNumber.parse(newVal), null, 2);
} else if (Array.isArray(newVal)) {
var temp = newVal.map((ele)=>{
if (typeof ele === 'object') {
return JSONBigNumber.stringify(ele, null, 2);
}
return ele;
});
newVal = '[' + temp.join() + ']';
}
/* set editor content to empty if value is null*/
if (_.isNull(newVal)){
newVal = '';
}
return newVal;
});
const [localVal, setLocalVal] = React.useState(value);
const onChange = React.useCallback((newVal)=>{
setLocalVal(newVal);
}, []);
const onOK = ()=>{
onRowChange({ ...row, [column.key]: localVal}, true);
onClose();
};
return (
<Portal container={document.body}>
<Box ref={(ele)=>{
setEditorPosition(getCellElement(column.idx), ele);
}} className={classes.jsonEditor}>
<JsonEditor
value={localVal}
options={{
onChange: onChange,
onError: (error)=>console.error('Invalid Json: ' + error.message.split(':')[0]),
}}
className={'jsoneditor-div'}
/>
<Box display="flex" justifyContent="flex-end" marginTop="0.25rem">
<DefaultButton startIcon={<CloseIcon />} onClick={()=>onClose(false)} size="small">
{gettext('Cancel')}
</DefaultButton>
{column.can_edit &&
<>
<PrimaryButton startIcon={<CheckRoundedIcon />} onClick={onOK} size="small" className={classes.buttonMargin}>
{gettext('OK')}
</PrimaryButton>
</>}
</Box>
</Box>
</Portal>
);
}
JsonTextEditor.propTypes = EditorPropTypes;

View File

@@ -0,0 +1,73 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import _ from 'lodash';
import { makeStyles } from '@material-ui/core';
import PropTypes from 'prop-types';
import CustomPropTypes from '../../../../../../static/js/custom_prop_types';
const useStyles = makeStyles((theme)=>({
disabledCell: {
opacity: theme.palette.action.disabledOpacity,
}
}));
function NullAndDefaultFormatter({value, column, children}) {
const classes = useStyles();
if (_.isUndefined(value) && column.has_default_val) {
return <span className={classes.disabledCell}>[default]</span>;
} else if ((_.isUndefined(value) && column.not_null) ||
(_.isUndefined(value) || _.isNull(value))) {
return <span className={classes.disabledCell}>[null]</span>;
}
return children;
}
NullAndDefaultFormatter.propTypes = {
value: PropTypes.any,
column: PropTypes.object,
children: CustomPropTypes.children,
};
const FormatterPropTypes = {
row: PropTypes.object,
column: PropTypes.object,
};
export function TextFormatter({row, column}) {
let value = row[column.key];
if(!_.isNull(value) && !_.isUndefined(value)) {
value = value.toString();
}
return (
<NullAndDefaultFormatter value={value} column={column}>
<>{value}</>
</NullAndDefaultFormatter>
);
}
TextFormatter.propTypes = FormatterPropTypes;
export function NumberFormatter({row, column}) {
let value = row[column.key];
return (
<NullAndDefaultFormatter value={value} column={column}>
<div style={{textAlign: 'right'}}>{value}</div>
</NullAndDefaultFormatter>
);
}
NumberFormatter.propTypes = FormatterPropTypes;
export function BinaryFormatter({row, column}) {
let value = row[column.key];
const classes = useStyles();
return (
<NullAndDefaultFormatter value={value} column={column}>
<span className={classes.disabledCell}>[{value}]</span>
</NullAndDefaultFormatter>
);
}
BinaryFormatter.propTypes = FormatterPropTypes;

View File

@@ -0,0 +1,398 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Box, makeStyles } from '@material-ui/core';
import _ from 'lodash';
import React, {useState, useEffect, useCallback, useContext, useRef} from 'react';
import ReactDataGrid, {Row, useRowSelection} from 'react-data-grid';
import LockIcon from '@material-ui/icons/Lock';
import EditIcon from '@material-ui/icons/Edit';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import * as Editors from './Editors';
import * as Formatters from './Formatters';
import clsx from 'clsx';
import { PgIconButton } from '../../../../../../static/js/components/Buttons';
import MapIcon from '@material-ui/icons/Map';
import { QueryToolEventsContext } from '../QueryToolComponent';
import PropTypes, { number } from 'prop-types';
import gettext from 'sources/gettext';
export const ROWNUM_KEY = '$_pgadmin_rownum_key_$';
export const GRID_ROW_SELECT_KEY = '$_pgadmin_gridrowselect_key_$';
const useStyles = makeStyles((theme)=>({
root: {
height: '100%',
color: theme.palette.text.primary,
backgroundColor: theme.otherVars.qtDatagridBg,
fontSize: '12px',
border: 'none',
'--rdg-selection-color': theme.palette.primary.main,
'& .rdg-cell': {
...theme.mixins.panelBorder.right,
...theme.mixins.panelBorder.bottom,
fontWeight: 'abc',
'&[aria-colindex="1"]': {
padding: 0,
}
},
'& .rdg-header-row .rdg-cell': {
padding: 0,
},
'& .rdg-header-row': {
backgroundColor: theme.palette.background.default,
fontWeight: 'normal',
},
'& .rdg-row': {
backgroundColor: theme.palette.background.default,
'&[aria-selected=true]': {
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
'& .rdg-cell:nth-child(1)': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
}
},
}
},
columnHeader: {
padding: '3px 6px',
height: '100%',
display: 'flex',
lineHeight: '16px',
alignItems: 'center',
},
columnName: {
fontWeight: 'bold',
},
editedCell: {
fontWeight: 'bold',
},
deletedRow: {
'&:before': {
content: '" "',
position: 'absolute',
top: '50%',
left: 0,
borderTop: '1px solid ' + theme.palette.error.main,
width: '100%',
}
},
rowNumCell: {
padding: '0px 8px',
},
colHeaderSelected: {
outlineColor: theme.palette.primary.main,
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
colSelected: {
outlineColor: theme.palette.primary.main,
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
}
}));
export const RowInfoContext = React.createContext();
function CustomRow(props) {
const rowRef = useRef();
const rowInfoValue = {
rowIdx: props.rowIdx,
getCellElement: (colIdx)=>{
return rowRef.current.querySelector(`.rdg-cell[aria-colindex="${colIdx+1}"]`);
}
};
return (
<RowInfoContext.Provider value={rowInfoValue}>
<Row ref={rowRef} {...props} />
</RowInfoContext.Provider>
);
}
CustomRow.propTypes = {
rowIdx: number,
};
function SelectAllHeaderRenderer(props) {
const [checked, setChecked] = useState(false);
const eventBus = useContext(QueryToolEventsContext);
const onClick = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
setChecked(!checked);
props.onAllRowsSelectionChange(!checked);
});
};
return <div style={{widht: '100%', height: '100%'}} onClick={onClick}></div>;
}
SelectAllHeaderRenderer.propTypes = {
onAllRowsSelectionChange: PropTypes.func,
};
function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsChange}) {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
const onClick = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
const newSelectedCols = new Set(selectedColumns);
if (newSelectedCols.has(column.idx)) {
newSelectedCols.delete(column.idx);
} else {
newSelectedCols.add(column.idx);
}
onSelectedColumnsChange(newSelectedCols);
});
};
const isSelected = selectedColumns.has(column.idx);
return (
<Box className={clsx(classes.columnHeader, isSelected ? classes.colHeaderSelected : null)} onClick={onClick}>
{(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') &&
<Box>
<PgIconButton title={gettext('View all geometries in this column')} icon={<MapIcon />} size="small" style={{marginRight: '0.25rem'}} onClick={(e)=>{
e.stopPropagation();
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, column);
}}/>
</Box>}
<Box marginRight="auto">
<span className={classes.columnName}>{column.display_name}</span><br/>
<span>{column.display_type}</span>
</Box>
<Box marginLeft="4px">{column.can_edit ?
<EditIcon fontSize="small" style={{fontSize: '0.875rem'}} />:
<LockIcon fontSize="small" style={{fontSize: '0.875rem'}} />
}</Box>
</Box>
);
}
SelectableHeaderRenderer.propTypes = {
column: PropTypes.object,
selectedColumns: PropTypes.objectOf(Set),
onSelectedColumnsChange: PropTypes.func,
};
function setEditorFormatter(col) {
// If grid is editable then add editor else make it readonly
if (col.cell == 'oid' && col.name == 'oid') {
col.editor = null;
col.formatter = Formatters.TextFormatter;
} else if (col.cell == 'Json') {
col.editor = Editors.JsonTextEditor;
col.formatter = Formatters.TextFormatter;
} else if (['number', 'oid'].indexOf(col.cell) != -1 || ['xid', 'real'].indexOf(col.type) != -1) {
col.formatter = Formatters.NumberFormatter;
col.editor = Editors.NumberEditor;
} else if (col.cell == 'boolean') {
col.editor = Editors.CheckboxEditor;
col.formatter = Formatters.TextFormatter;
} else if (col.cell == 'binary') {
// We do not support editing binary data in SQL editor and data grid.
col.editor = null;
col.formatter = Formatters.BinaryFormatter;
} else {
col.editor = Editors.TextEditor;
col.formatter = Formatters.TextFormatter;
}
}
function cellClassGetter(col, classes, isSelected, dataChangeStore, rowKeyGetter){
return (row)=>{
let cellClasses = [];
if(dataChangeStore && rowKeyGetter) {
if(rowKeyGetter(row) in (dataChangeStore?.updated || {})
&& !_.isUndefined(dataChangeStore?.updated[rowKeyGetter(row)]?.data[col.key])
|| rowKeyGetter(row) in (dataChangeStore?.added || {})
) {
cellClasses.push(classes.editedCell);
}
if(rowKeyGetter(row) in (dataChangeStore?.deleted || {})) {
cellClasses.push(classes.deletedRow);
}
}
if(isSelected) {
cellClasses.push(classes.colSelected);
}
return clsx(cellClasses);
};
}
function initialiseColumns(columns, rows, totalRowCount, columnWidthBy) {
let retColumns = [
...columns,
];
const canvas = document.createElement('canvas');
const canvasContext = canvas.getContext('2d');
canvasContext.font = '12px Roboto';
for(const col of retColumns) {
col.width = getTextWidth(col, rows, canvasContext, columnWidthBy);
col.resizable = true;
col.editorOptions = {
commitOnOutsideClick: false,
onCellKeyDown: (e)=>{
/* Do not open the editor */
e.preventDefault();
}
};
setEditorFormatter(col);
}
let rowNumCol = {
key: ROWNUM_KEY, name: '', frozen: true, resizable: false,
minWidth: 45, width: canvasContext.measureText((totalRowCount||'').toString()).width,
};
rowNumCol.cellClass = cellClassGetter(rowNumCol);
retColumns.unshift(rowNumCol);
canvas.remove();
return retColumns;
}
function formatColumns(columns, dataChangeStore, selectedColumns, onSelectedColumnsChange, rowKeyGetter, classes) {
let retColumns = [
...columns,
];
const HeaderRenderer = (props)=>{
return <SelectableHeaderRenderer {...props} selectedColumns={selectedColumns} onSelectedColumnsChange={onSelectedColumnsChange}/>;
};
for(const [idx, col] of retColumns.entries()) {
col.headerRenderer = HeaderRenderer;
col.cellClass = cellClassGetter(col, classes, selectedColumns.has(idx), dataChangeStore, rowKeyGetter);
}
let rowNumCol = retColumns[0];
rowNumCol.headerRenderer = SelectAllHeaderRenderer;
rowNumCol.formatter = ({row})=>{
const {rowIdx} = useContext(RowInfoContext);
const [isRowSelected, onRowSelectionChange] = useRowSelection();
let rowKey = rowKeyGetter(row);
let rownum = rowIdx+1;
if(rowKey in (dataChangeStore?.added || {})) {
rownum = rownum+'+';
} else if(rowKey in (dataChangeStore?.deleted || {})) {
rownum = rownum+'-';
}
return (<div className={classes.rowNumCell} onClick={()=>{
onSelectedColumnsChange(new Set());
onRowSelectionChange({ row: row, checked: !isRowSelected, isShiftClick: false});
}}>
{rownum}
</div>);
};
return retColumns;
}
function getTextWidth(column, rows, canvas, columnWidthBy) {
const dataWidthReducer = (longest, nextRow) => {
let value = nextRow[column.key];
if(_.isNull(value) || _.isUndefined(value)) {
value = '';
}
value = value.toString();
return longest.length > value.length ? longest : value;
};
let columnHeaderLen = column.display_name.length > column.display_type.length ?
canvas.measureText(column.display_name).width : canvas.measureText(column.display_type).width;
/* padding 12, icon-width 15 */
columnHeaderLen += 15 + 12;
if(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') {
columnHeaderLen += 40;
}
let width = columnHeaderLen;
if(typeof(columnWidthBy) == 'number') {
/* padding 16 */
width = 16 + Math.ceil(canvas.measureText(rows.reduce(dataWidthReducer, '')).width);
if(width > columnWidthBy && columnWidthBy > 0) {
width = columnWidthBy;
}
if(width < columnHeaderLen) {
width = columnHeaderLen;
}
}
/* Gracefull */
width += 2;
return width;
}
export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore,
onSelectedCellChange, rowsResetKey, selectedColumns, onSelectedColumnsChange, columnWidthBy, ...props}) {
const classes = useStyles();
const [readyColumns, setColumns] = useState([]);
const eventBus = useContext(QueryToolEventsContext);
const onSelectedColumnsChangeWrapped = (arg)=>{
props.onSelectedRowsChange(new Set());
onSelectedColumnsChange(arg);
};
useEffect(()=>{
if(columns.length > 0 || rows.length > 0) {
let initCols = initialiseColumns(columns, rows, totalRowCount, columnWidthBy);
setColumns(formatColumns(initCols, dataChangeStore, selectedColumns, onSelectedColumnsChangeWrapped, props.rowKeyGetter, classes));
} else {
setColumns([], [], 0);
}
}, [columns, rowsResetKey]);
useEffect(()=>{
setColumns((prevCols)=>{
return formatColumns(prevCols, dataChangeStore, selectedColumns, onSelectedColumnsChangeWrapped, props.rowKeyGetter, classes);
});
}, [dataChangeStore, selectedColumns]);
const onRowClick = useCallback((row, column)=>{
if(column.key === ROWNUM_KEY) {
onSelectedCellChange && onSelectedCellChange(null);
} else {
onSelectedCellChange && onSelectedCellChange([row, column]);
}
}, []);
function handleCopy() {
if (window.isSecureContext) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA);
}
}
return (
<ReactDataGrid
id="datagrid"
columns={readyColumns}
rows={rows}
className={classes.root}
headerRowHeight={40}
rowHeight={25}
mincolumnWidthBy={50}
enableCellSelect={true}
onRowClick={onRowClick}
onCopy={handleCopy}
components={{
rowRenderer: CustomRow,
}}
{...props}
/>
);
}
QueryToolDataGrid.propTypes = {
columns: PropTypes.array,
rows: PropTypes.array,
totalRowCount: PropTypes.number,
dataChangeStore: PropTypes.object,
onSelectedCellChange: PropTypes.func,
onSelectedRowsChange: PropTypes.func,
selectedColumns: PropTypes.objectOf(Set),
onSelectedColumnsChange: PropTypes.func,
rowKeyGetter: PropTypes.func,
rowsResetKey: PropTypes.any,
columnWidthBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { useModalStyles } from '../../../../../../static/js/helpers/ModalProvider';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
import HTMLReactParser from 'html-react-parser';
import PropTypes from 'prop-types';
export default function ConfirmSaveContent({closeModal, text, onDontSave, onSave}) {
const classes = useModalStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof(text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className={classes.footer}>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={()=>{
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<DefaultButton data-test="dont-save" className={classes.margin} startIcon={<DeleteRoundedIcon />} onClick={()=>{
onDontSave?.();
closeModal();
}} >{gettext('Don\'t save')}</DefaultButton>
<PrimaryButton data-test="save" className={classes.margin} startIcon={<CheckRoundedIcon />} onClick={()=>{
onSave?.();
closeModal();
}} autoFocus={true} >{gettext('Save')}</PrimaryButton>
</Box>
</Box>
);
}
ConfirmSaveContent.propTypes = {
closeModal: PropTypes.func,
text: PropTypes.string,
onDontSave: PropTypes.func,
onSave: PropTypes.func
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useModalStyles } from '../../../../../../static/js/helpers/ModalProvider';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import HTMLReactParser from 'html-react-parser';
import { CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import PropTypes from 'prop-types';
export default function ConfirmTransactionContent({closeModal, text, onRollback, onCommit}) {
const classes = useModalStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof(text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className={classes.footer}>
<DefaultButton data-test="cancel" startIcon={<CloseIcon />} onClick={()=>{
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton data-test="rollback" className={classes.margin} startIcon={<RollbackIcon />} onClick={()=>{
onRollback?.();
closeModal();
}} >{gettext('Rollback')}</PrimaryButton>
<PrimaryButton data-test="commit" className={classes.margin} startIcon={<CommitIcon />} onClick={()=>{
onCommit?.();
closeModal();
}} autoFocus={true} >{gettext('Commit')}</PrimaryButton>
</Box>
</Box>
);
}
ConfirmTransactionContent.propTypes = {
closeModal: PropTypes.func,
text: PropTypes.string,
onRollback: PropTypes.func,
onCommit: PropTypes.func
};

View File

@@ -0,0 +1,155 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/core';
import React from 'react';
import SchemaView from '../../../../../../static/js/SchemaView';
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import { QueryToolContext } from '../QueryToolComponent';
import url_for from 'sources/url_for';
import PropTypes from 'prop-types';
class SortingCollection extends BaseUISchema {
constructor(columnOptions) {
super({
name: undefined,
order: 'asc',
});
this.columnOptions = columnOptions;
this.reloadColOptions = 0;
}
setColumnOptions(columnOptions) {
this.columnOptions = columnOptions;
this.reloadColOptions = this.reloadColOptions + 1;
}
get baseFields() {
return [
{
id: 'name', label: gettext('Column'), cell: 'select', controlProps: {
allowClear: false,
}, noEmpty: true, options: this.columnOptions, optionsReloadBasis: this.reloadColOptions
},
{
id: 'order', label: gettext('Order'), cell: 'select', controlProps: {
allowClear: false,
}, options: [
{label: gettext('ASC'), value: 'asc'},
{label: gettext('DESC'), value: 'desc'},
]
},
];
}
}
class FilterSchema extends BaseUISchema {
constructor(columnOptions) {
super({
sql: null,
data_sorting: [],
});
this.sortingCollObj = new SortingCollection(columnOptions);
}
setColumnOptions(columnOptions) {
this.sortingCollObj.setColumnOptions(columnOptions);
}
get baseFields() {
let obj = this;
return [
{
id: 'sql', label: gettext('SQL Filter'), type: 'sql', controlProps: {
options: {
lineWrapping: true,
},
}
},
{
id: 'data_sorting', label: gettext('Data Sorting'), type: 'collection', schema: obj.sortingCollObj,
group: 'temp', uniqueCol: ['name'], canAdd: true, canEdit: false, canDelete: true,
},
];
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
},
}));
export default function FilterDialog({onClose, onSave}) {
const classes = useStyles();
const queryToolCtx = React.useContext(QueryToolContext);
const filterSchemaObj = React.useMemo(()=>new FilterSchema([]));
const getInitData = ()=>{
return new Promise((resolve, reject)=>{
const getFilterData = async ()=>{
try {
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_filter_data', {
'trans_id': queryToolCtx.params.trans_id,
}));
let {column_list: columns, ...filterData} = respData.data.result;
filterSchemaObj.setColumnOptions((columns||[]).map((c)=>({label: c, value: c})));
resolve(filterData);
} catch (error) {
reject(error);
}
};
getFilterData();
});
};
const onSaveClick = (_isNew, changeData)=>{
return new Promise((resolve, reject)=>{
const setFilterData = async ()=>{
try {
let {data: respData} = await queryToolCtx.api.put(url_for('sqleditor.set_filter_data', {
'trans_id': queryToolCtx.params.trans_id,
}), changeData);
if(respData.data.status) {
resolve();
onSave();
} else {
reject(respData.data.result);
}
} catch (error) {
reject(error);
}
};
setFilterData();
});
};
return (<>
<SchemaView
formType={'dialog'}
getInitData={getInitData}
schema={filterSchemaObj}
viewHelperProps={{
mode: 'create',
}}
onSave={onSaveClick}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
formClassName={classes.root}
/>
</>);
}
FilterDialog.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
};

View File

@@ -0,0 +1,189 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/core';
import React from 'react';
import SchemaView from '../../../../../../static/js/SchemaView';
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import { QueryToolContext } from '../QueryToolComponent';
import url_for from 'sources/url_for';
import _ from 'lodash';
import PropTypes from 'prop-types';
class MacrosCollection extends BaseUISchema {
constructor(keyOptions) {
super();
this.keyOptions = keyOptions;
}
get idAttribute() {
return 'mid';
}
get baseFields() {
let obj = this;
return [
{
id: 'id', label: gettext('Key'), cell: 'select', noEmpty: true,
width: 100, options: obj.keyOptions, optionsReloadBasis: obj.keyOptions.length,
controlProps: {
allowClear: false,
}
},
{
id: 'name', label: gettext('Name'), cell: 'text', noEmpty: true,
width: 100,
},
{
id: 'sql', label: gettext('SQL'), cell: 'sql', noEmpty: true,
width: 300, controlProps: {
options: {
foldGutter: false,
lineNumbers: false,
gutters: [],
readOnly: true,
lineWrapping: true,
},
}
},
];
}
}
class MacrosSchema extends BaseUISchema {
constructor(keyOptions) {
super();
this.macrosCollObj = new MacrosCollection(keyOptions);
}
get baseFields() {
let obj = this;
return [
{
id: 'macro', label: '', type: 'collection', schema: obj.macrosCollObj,
canAdd: true, canDelete: true, isFullTab: true, group: 'temp',
},
];
}
validate(state, setError) {
let allKeys = state.macro.map((m)=>m.id.toString());
if(allKeys.length != new Set(allKeys).size) {
setError('macro', gettext('Key must be unique.'));
return true;
}
return false;
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
padding: 0,
},
}));
function getChangedMacros(macrosData, changeData) {
/* For backend, added, removed is changed. Convert all added, removed to changed. */
let changed = [];
for (const m of (changeData.macro.changed || [])) {
let newM = {...m};
if('id' in m) {
/* if key changed, clear prev and add new */
changed.push({id: m.mid, name: null, sql: null});
let em = _.find(macrosData, (d)=>d.mid==m.mid);
newM = {name: em.name, sql: em.sql, ...m};
} else {
newM.id = m.mid;
}
delete newM.mid;
changed.push(newM);
}
for (const m of (changeData.macro.deleted || [])) {
changed.push({id: m.id, name: null, sql: null});
}
for (const m of (changeData.macro.added || [])) {
changed.push(m);
}
return changed;
}
export default function MacrosDialog({onClose, onSave}) {
const classes = useStyles();
const queryToolCtx = React.useContext(QueryToolContext);
const [macrosData, setMacrosData] = React.useState([]);
const [macrosErr, setMacrosErr] = React.useState(null);
React.useEffect(async ()=>{
try {
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_macros', {
'trans_id': queryToolCtx.params.trans_id,
}));
/* Copying id to mid to track key id changes */
setMacrosData(respData.macro.map((m)=>({...m, mid: m.id})));
} catch (error) {
setMacrosErr(error);
}
}, []);
const onSaveClick = (_isNew, changeData)=>{
return new Promise((resolve, reject)=>{
const setMacros = async ()=>{
try {
let changed = getChangedMacros(macrosData, changeData);
let {data: respData} = await queryToolCtx.api.put(url_for('sqleditor.set_macros', {
'trans_id': queryToolCtx.params.trans_id,
}), {changed: changed});
resolve();
onSave(respData.macro?.filter((m)=>Boolean(m.name)));
onClose();
} catch (error) {
reject(error);
}
};
setMacros();
});
};
const keyOptions = macrosData.map((m)=>({
label: m.key_label,
value: m.id,
}));
if(keyOptions.length <= 0) {
return <></>;
}
return (<>
<SchemaView
formType={'dialog'}
getInitData={()=>{
if(macrosErr) {
return Promise.reject(macrosErr);
}
return Promise.resolve({macro: macrosData.filter((m)=>Boolean(m.name))});
}}
schema={new MacrosSchema(keyOptions)}
viewHelperProps={{
mode: 'edit',
}}
onSave={onSaveClick}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
formClassName={classes.root}
/>
</>);
}
MacrosDialog.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
};

View File

@@ -0,0 +1,254 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import SchemaView from '../../../../../../static/js/SchemaView';
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import { QueryToolContext } from '../QueryToolComponent';
import url_for from 'sources/url_for';
import _ from 'lodash';
import { flattenSelectOptions } from '../../../../../../static/js/components/FormComponents';
import PropTypes from 'prop-types';
import ConnectServerContent from '../../../../../../browser/static/js/ConnectServerContent';
class NewConnectionSchema extends BaseUISchema {
constructor(api, params, connectServer) {
super({
sid: null,
did: null,
user: null,
role: null,
server_name: null,
database_name: null,
});
this.flatServers = [];
this.groupedServers = [];
this.dbs = [];
this.params = params;
this.api = api;
this.warningText = gettext('By changing the connection you will lose all your unsaved data for the current connection. <br> Do you want to continue?');
this.connectServer = connectServer;
}
setServerConnected(sid, icon) {
for(const group of this.groupedServers) {
for(const opt of group.options) {
if(opt.value == sid) {
opt.connected = true;
opt.image = icon || 'icon-pg';
break;
}
}
}
}
isServerConnected(sid) {
return _.find(this.flatServers, (s)=>s.value==sid)?.connected;
}
getServerList() {
let obj = this;
if(this.groupedServers?.length != 0) {
return Promise.resolve(this.groupedServers);
}
return new Promise((resolve, reject)=>{
this.api.get(url_for('sqleditor.get_new_connection_servers'))
.then(({data: respData})=>{
let groupedOptions = [];
_.forIn(respData.data.result.server_list, (v, k)=>{
/* initial selection */
_.find(v, (o)=>o.value==obj.params.sid).selected = true;
groupedOptions.push({
label: k,
options: v,
});
});
/* Will be re-used for changing icon when connected */
this.groupedServers = groupedOptions.map((group)=>{
return {
label: group.label,
options: group.options.map((o)=>({...o, selected: false})),
};
});
resolve(groupedOptions);
})
.catch((error)=>{
reject(error);
});
});
}
getOtherOptions(sid, type) {
if(!sid) {
return [];
}
if(!this.isServerConnected(sid)) {
return [];
}
return new Promise((resolve, reject)=>{
this.api.get(url_for(`sqleditor.${type}`, {
'sid': sid,
'sgid': 0,
}))
.then(({data: respData})=>{
resolve(respData.data.result.data);
})
.catch((error)=>{
reject(error);
});
});
}
get baseFields() {
let self = this;
return [
{
id: 'sid', label: gettext('Server'), type: 'select', noEmpty: true,
controlProps: {
allowClear: false,
}, options: ()=>this.getServerList(),
optionsLoaded: (res)=>this.flatServers=flattenSelectOptions(res),
optionsReloadBasis: this.flatServers.map((s)=>s.connected).join(''),
depChange: (state)=>{
/* Once the option is selected get the name */
/* Force sid to null, and set only if connected */
return {
server_name: _.find(this.flatServers, (s)=>s.value==state.sid)?.label,
did: null,
user: null,
role: null,
sid: null,
};
},
deferredDepChange: (state, source, topState, actionObj)=>{
return new Promise((resolve)=>{
let sid = actionObj.value;
if(!_.find(this.flatServers, (s)=>s.value==sid)?.connected) {
this.connectServer(sid, state.user, null, (data)=>{
self.setServerConnected(sid, data.icon);
resolve(()=>({sid: sid}));
});
} else {
resolve(()=>({sid: sid}));
}
});
},
}, {
id: 'did', label: gettext('Database'), deps: ['sid'], noEmpty: true,
controlProps: {
allowClear: false,
},
type: (state)=>({
type: 'select',
options: ()=>this.getOtherOptions(state.sid, 'get_new_connection_database'),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
}),
optionsLoaded: (res)=>this.dbs=res,
depChange: (state)=>{
/* Once the option is selected get the name */
return {database_name: _.find(this.dbs, (s)=>s.value==state.did)?.label};
}
},{
id: 'user', label: gettext('User'), deps: ['sid'], noEmpty: true,
controlProps: {
allowClear: false,
},
type: (state)=>({
type: 'select',
options: ()=>this.getOtherOptions(state.sid, 'get_new_connection_user'),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
}),
},{
id: 'role', label: gettext('Role'), deps: ['sid'],
type: (state)=>({
type: 'select',
options: ()=>this.getOtherOptions(state.sid, 'get_new_connection_role'),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
}),
},{
id: 'server_name', label: '', type: 'text', visible: false,
},{
id: 'database_name', label: '', type: 'text', visible: false,
},
];
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
},
}));
export default function NewConnectionDialog({onClose, onSave}) {
const classes = useStyles();
const [connecting, setConnecting] = useState(false);
const queryToolCtx = React.useContext(QueryToolContext);
const connectServer = async (sid, user, formData, connectCallback) => {
setConnecting(true);
try {
let {data: respData} = await queryToolCtx.api({
method: 'POST',
url: url_for('sqleditor.connect_server', {
'sid': sid,
...(user ? {
'usr': user,
}:{}),
}),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData
});
setConnecting(false);
connectCallback?.(respData.data);
} catch (error) {
queryToolCtx.modal.showModal(gettext('Connect to server'), (closeModal)=>{
return (
<ConnectServerContent
closeModal={()=>{
setConnecting(false);
closeModal();
}}
data={error.response?.data?.result}
onOK={(formData)=>{
connectServer(sid, null, formData, connectCallback);
}}
/>
);
});
}
};
return <SchemaView
formType={'dialog'}
getInitData={()=>Promise.resolve({})}
schema={new NewConnectionSchema(queryToolCtx.api, {
sid: queryToolCtx.params.sid, sgid: 0,
}, connectServer)}
viewHelperProps={{
mode: 'create',
}}
loadingText={connecting ? 'Connecting...' : ''}
onSave={onSave}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
formClassName={classes.root}
/>;
}
NewConnectionDialog.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
};

View File

@@ -0,0 +1,148 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box, CircularProgress, Tooltip } from '@material-ui/core';
import { DefaultButton, PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import { ConnectedIcon, DisonnectedIcon, QueryToolIcon } from '../../../../../../static/js/components/ExternalIcon';
import { QueryToolContext } from '../QueryToolComponent';
import { CONNECTION_STATUS, CONNECTION_STATUS_MESSAGE } from '../QueryToolConstants';
import HourglassEmptyRoundedIcon from '@material-ui/icons/HourglassEmptyRounded';
import QueryBuilderRoundedIcon from '@material-ui/icons/QueryBuilderRounded';
import ErrorOutlineRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
import ReportProblemRoundedIcon from '@material-ui/icons/ReportProblemRounded';
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import RotateLeftRoundedIcon from '@material-ui/icons/RotateLeftRounded';
import PropTypes from 'prop-types';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
},
connectionButton: {
display: 'flex',
width: '450px',
backgroundColor: theme.palette.default.main,
color: theme.palette.default.contrastText,
border: '1px solid ' + theme.palette.default.borderColor,
justifyContent: 'flex-start',
},
viewDataConnTitle: {
marginTop: 'auto',
marginBottom: 'auto',
padding: '0px 4px',
},
menu: {
'& .szh-menu': {
minWidth: '450px',
}
}
}));
function ConnectionStatusIcon({connected, connecting, status}) {
if(connecting) {
return <CircularProgress style={{height: '18px', width: '18px'}} />;
} else if(connected) {
switch (status) {
case CONNECTION_STATUS.TRANSACTION_STATUS_ACTIVE:
return <HourglassEmptyRoundedIcon />;
case CONNECTION_STATUS.TRANSACTION_STATUS_INTRANS:
return <QueryBuilderRoundedIcon />;
case CONNECTION_STATUS.TRANSACTION_STATUS_INERROR:
return <ErrorOutlineRoundedIcon />;
case CONNECTION_STATUS.TRANSACTION_STATUS_UNKNOWN:
return <ReportProblemRoundedIcon />;
default:
return <ConnectedIcon />;
}
} else {
return <DisonnectedIcon />;
}
}
ConnectionStatusIcon.propTypes = {
connected: PropTypes.bool,
connecting: PropTypes.bool,
status: PropTypes.oneOf(Object.values(CONNECTION_STATUS)),
};
export function ConnectionBar({connected, connecting, connectionStatus, connectionStatusMsg,
connectionList, onConnectionChange, onNewConnClick, onNewQueryToolClick, onResetLayout}) {
const classes = useStyles();
const connMenuRef = React.useRef();
const [connDropdownOpen, setConnDropdownOpen] = React.useState(false);
const queryToolCtx = React.useContext(QueryToolContext);
const onConnItemClick = (e)=>{
if(!e.value.is_selected) {
onConnectionChange(e.value);
}
e.keepOpen = false;
};
const connTitle = React.useMemo(()=>_.find(connectionList, (c)=>c.is_selected)?.conn_title, [connectionList]);
return (
<>
<Box className={classes.root}>
<PgButtonGroup size="small">
{queryToolCtx.preferences?.sqleditor?.connection_status &&
<PgIconButton title={CONNECTION_STATUS_MESSAGE[connected ? connectionStatus : -1] ?? connectionStatusMsg}
icon={<ConnectionStatusIcon connecting={connecting} status={connectionStatus} connected={connected}/>}
/>}
<DefaultButton className={classes.connectionButton} ref={connMenuRef}
onClick={queryToolCtx.params.is_query_tool ? ()=>setConnDropdownOpen(true) : undefined}
style={{backgroundColor: queryToolCtx.params.bgcolor, color: queryToolCtx.params.fgcolor}}
>
<Tooltip title={queryToolCtx.params.is_query_tool ? '' : connTitle}>
<Box display="flex" width="100%">
<Box textOverflow="ellipsis" overflow="hidden" marginRight="auto">{connecting && '(Obtaining connection)'}{connTitle}</Box>
{queryToolCtx.params.is_query_tool && <Box><KeyboardArrowDownIcon /></Box>}
</Box>
</Tooltip>
</DefaultButton>
<PgIconButton title="New query tool" icon={<QueryToolIcon />} onClick={onNewQueryToolClick}/>
</PgButtonGroup>
<PgButtonGroup size="small" variant="text" style={{marginLeft: 'auto'}}>
<PgIconButton title="Reset layout" icon={<RotateLeftRoundedIcon />} onClick={onResetLayout}/>
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={connMenuRef}
open={connDropdownOpen}
onClose={()=>{setConnDropdownOpen(false);}}
className={classes.menu}
>
{(connectionList||[]).map((conn)=>{
return (
<PgMenuItem key={conn.conn_title} hasCheck checked={conn.is_selected} value={conn}
onClick={onConnItemClick}>{conn.conn_title}</PgMenuItem>
);
})}
<PgMenuItem onClick={onNewConnClick}>{`< ${gettext('New connection...')} >`}</PgMenuItem>
</PgMenu>
</>
);
}
ConnectionBar.propTypes = {
connected: PropTypes.bool,
connecting: PropTypes.bool,
connectionStatus: PropTypes.oneOf(Object.values(CONNECTION_STATUS)),
connectionStatusMsg: PropTypes.string,
connectionList: PropTypes.array,
onConnectionChange: PropTypes.func,
onNewConnClick: PropTypes.func,
onNewQueryToolClick: PropTypes.func,
onResetLayout: PropTypes.func,
};

View File

@@ -0,0 +1,386 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useRef } from 'react';
import ReactDOMServer from 'react-dom/server';
import { makeStyles } from '@material-ui/styles';
import _ from 'lodash';
import { MapContainer, TileLayer, LayersControl, GeoJSON, useMap } from 'react-leaflet';
import Leaflet, { CRS } from 'leaflet';
import {Geometry as WkxGeometry} from 'wkx';
import {Buffer} from 'buffer';
import gettext from 'sources/gettext';
import Theme from 'sources/Theme';
import clsx from 'clsx';
import PropTypes from 'prop-types';
const useStyles = makeStyles((theme)=>({
mapContainer: {
backgroundColor: theme.palette.background.default,
height: '100%',
width: '100%'
},
table: {
borderSpacing: 0,
width: '100%',
...theme.mixins.panelBorder,
},
tableCell: {
margin: 0,
padding: theme.spacing(0.5),
...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
tableCellHead: {
fontWeight: 'bold',
}
}));
function parseEwkbData(rows, column) {
let key = column.key;
const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB
const maxRenderGeometries = 100000; // render geometries up to 100000
let geometries3D = [],
supportedGeometries = [],
unsupportedRows = [],
geometryItemMap = new Map(),
geometryTotalByteLength = 0,
tooLargeDataSize = false,
tooManyGeometries = false,
infoList = [];
_.every(rows, function (item) {
try {
let value = item[key];
let buffer = Buffer.from(value, 'hex');
let geometry = WkxGeometry.parse(buffer);
if (geometry.hasZ) {
geometries3D.push(geometry);
return true;
}
geometryTotalByteLength += buffer.byteLength;
if (geometryTotalByteLength > maxRenderByteLength) {
tooLargeDataSize = true;
return false;
}
if (supportedGeometries.length >= maxRenderGeometries) {
tooManyGeometries = true;
return false;
}
if (!geometry.srid) {
geometry.srid = 0;
}
supportedGeometries.push(geometry);
geometryItemMap.set(geometry, item);
} catch (e) {
unsupportedRows.push(item);
}
return true;
});
// generate map info content
if (tooLargeDataSize || tooManyGeometries) {
infoList.push(supportedGeometries.length + ' of ' + rows.length + ' geometries rendered.');
}
if (geometries3D.length > 0) {
infoList.push(gettext('3D geometries not rendered.'));
}
if (unsupportedRows.length > 0) {
infoList.push(gettext('Unsupported geometries not rendered.'));
}
return [
supportedGeometries,
geometryItemMap,
infoList
];
}
function parseData(rows, columns, column) {
if (rows.length === 0) {
return {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': ['Empty row.'],
};
}
let mixedSRID = false;
// parse ewkb data
let [
supportedGeometries,
geometryItemMap,
infoList
] = parseEwkbData(rows, column);
if (supportedGeometries.length === 0) {
return {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': infoList,
};
}
// group geometries by SRID
let geometriesGroupBySRID = _.groupBy(supportedGeometries, 'srid');
let SRIDGeometriesPairs = _.toPairs(geometriesGroupBySRID);
if (SRIDGeometriesPairs.length > 1) {
mixedSRID = true;
}
// select the largest group
let selectedPair = _.max(SRIDGeometriesPairs, function (pair) {
return pair[1].length;
});
let selectedSRID = parseInt(selectedPair[0]);
let selectedGeometries = selectedPair[1];
let geoJSONs = _.map(selectedGeometries, function (geometry) {
return geometry.toGeoJSON();
});
let getPopupContent;
if (columns.length >= 3) {
// add popup when geometry has properties
getPopupContent = function (geojson) {
let geometry = selectedGeometries[geoJSONs.indexOf(geojson)];
let row = geometryItemMap.get(geometry);
let retVal = [];
for (const col of columns) {
if(col.key === column.key) {
continue;
}
retVal.push({
'column': col.display_name,
'value': row[col.key],
});
}
return retVal;
};
}
if (mixedSRID) {
infoList.push(gettext('Geometries with non-SRID %s not rendered.', selectedSRID));
}
return {
'geoJSONs': geoJSONs,
'selectedSRID': selectedSRID,
'getPopupContent': getPopupContent,
'infoList': infoList,
};
}
function PopupTable({data}) {
const classes = useStyles();
return (
<table className={classes.table}>
<tbody>
{data.map((row)=>{
return (
<tr key={row.column}>
<td className={clsx(classes.tableCell, classes.tableCellHead)}>{row.column}</td>
<td className={classes.tableCell}>{row.value}</td>
</tr>
);
})}
</tbody>
</table>
);
}
PopupTable.propTypes = {
data: PropTypes.arrayOf({
column: PropTypes.string,
value: PropTypes.string,
}),
};
function GeoJsonLayer({data}) {
const vectorLayerRef = useRef(null);
const mapObj = useMap();
useEffect(() => {
if(!vectorLayerRef.current) return;
if(data.geoJSONs.length <= 0) return;
let bounds = vectorLayerRef.current.getBounds().pad(0.1);
let maxLength = Math.max(bounds.getNorth() - bounds.getSouth(),
bounds.getEast() - bounds.getWest());
let minZoom = 0;
if(data.selectedSRID !== 4326) {
if (maxLength >= 180) {
// calculate the min zoom level to enable the map to fit the whole geometry.
minZoom = Math.floor(Math.log2(360 / maxLength)) - 2;
}
}
mapObj.setMinZoom(minZoom);
if (maxLength > 0) {
mapObj.fitBounds(bounds);
} else {
mapObj.setView(bounds.getCenter(), mapObj.getZoom());
}
});
return (
<GeoJSON
ref={vectorLayerRef}
pointToLayer={(_feature, latlng)=>{
return Leaflet.circleMarker(latlng, {
radius: 4,
weight: 3,
});
}}
style={{weight: 2}}
onEachFeature={(feature, layer)=>{
if(_.isFunction(data.getPopupContent)) {
const popupContentNode = (
<Theme>
<PopupTable data={data.getPopupContent(layer.feature.geometry)}/>
</Theme>
);
const popupContentHtml = ReactDOMServer.renderToString(popupContentNode);
layer.bindPopup(popupContentHtml, {
closeButton: false,
minWidth: 260,
maxWidth: 300,
maxHeight: 300,
});
}
}}
data={data.geoJSONs}
/>
);
}
GeoJsonLayer.propTypes = {
data: PropTypes.shape({
geoJSONs: PropTypes.array,
selectedSRID: PropTypes.number,
getPopupContent: PropTypes.func,
infoList: PropTypes.array,
}),
};
function TheMap({data}) {
const mapObj = useMap();
const infoControl = useRef(null);
useEffect(()=>{
infoControl.current = Leaflet.control({position: 'topright'});
infoControl.current.onAdd = function () {
let ele = Leaflet.DomUtil.create('div', 'geometry-viewer-info-control');
ele.innerHTML = data.infoList.join('<br />');
return ele;
};
if(data.infoList.length > 0) {
infoControl.current.addTo(mapObj);
}
return ()=>{infoControl.current && infoControl.current.remove();};
}, [data]);
return (
<>
{data.selectedSRID === 4326 &&
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="Empty">
<TileLayer
url=""
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer checked name="Street">
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
maxZoom={19}
attribution='&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Topography">
<TileLayer
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
maxZoom={17}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://viewfinderpanoramas.org" target="_blank">SRTM</a>,'
+ ' &copy; <a href="https://opentopomap.org" target="_blank">OpenTopoMap</a>'
}
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Gray Style">
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png"
maxZoom={19}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>'
}
subdomains='abcd'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Light Color">
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}{r}.pn"
maxZoom={19}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>'
}
subdomains='abcd'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Dark Matter">
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png"
maxZoom={19}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>'
}
subdomains='abcd'
/>
</LayersControl.BaseLayer>
</LayersControl>}
<GeoJsonLayer key={data.geoJSONs.length} data={data}/>
</>
);
}
TheMap.propTypes = {
data: PropTypes.shape({
geoJSONs: PropTypes.array,
selectedSRID: PropTypes.number,
getPopupContent: PropTypes.func,
infoList: PropTypes.array,
}),
};
export function GeometryViewer({rows, columns, column}) {
const classes = useStyles();
const data = parseData(rows, columns, column);
const crs = data.selectedSRID === 4326 ? CRS.EPSG3857 : CRS.Simple;
return (
<MapContainer
crs={crs}
zoom={2} center={[20, 100]}
preferCanvas={true}
scrollWheelZoom={false}
className={classes.mapContainer}
>
<TheMap data={data} />
</MapContainer>
);
}
GeometryViewer.propTypes = {
rows: PropTypes.array,
columns: PropTypes.array,
column: PropTypes.object,
};

View File

@@ -0,0 +1,607 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useState} from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box } from '@material-ui/core';
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import FolderRoundedIcon from '@material-ui/icons/FolderRounded';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import SaveRoundedIcon from '@material-ui/icons/SaveRounded';
import StopRoundedIcon from '@material-ui/icons/StopRounded';
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
import { FilterIcon, CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import EditRoundedIcon from '@material-ui/icons/EditRounded';
import AssessmentRoundedIcon from '@material-ui/icons/AssessmentRounded';
import ExplicitRoundedIcon from '@material-ui/icons/ExplicitRounded';
import FormatListNumberedRoundedIcon from '@material-ui/icons/FormatListNumberedRounded';
import HelpIcon from '@material-ui/icons/HelpRounded';
import {QUERY_TOOL_EVENTS, CONNECTION_STATUS} from '../QueryToolConstants';
import { QueryToolConnectionContext, QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import { PgMenu, PgMenuDivider, PgMenuItem } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import { useKeyboardShortcuts } from '../../../../../../static/js/custom_hooks';
import {shortcut_key} from 'sources/keyboard_shortcuts';
import url_for from 'sources/url_for';
import _ from 'lodash';
import { InputSelectNonSearch } from '../../../../../../static/js/components/FormComponents';
import PropTypes from 'prop-types';
import CustomPropTypes from '../../../../../../static/js/custom_prop_types';
import ConfirmTransactionContent from '../dialogs/ConfirmTransactionContent';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.bottom,
},
}));
const FIXED_PREF = {
find: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 70,
'char': 'F',
},
},
replace: {
'control': true,
ctrl_is_meta: true,
'shift': isMac() ? false : true,
'alt': isMac() ? true : false,
'key': {
'key_code': 70,
'char': 'F',
},
},
jump: {
'control': false,
'shift': false,
'alt': true,
'key': {
'key_code': 71,
'char': 'G',
},
},
indent: {
'control': false,
'shift': false,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
unindent: {
'control': false,
'shift': true,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
comment: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 191,
'char': '/',
},
},
uncomment: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 190,
'char': '.',
},
},
format_sql: {
'control': true,
'shift': true,
'alt': false,
'key': {
'key_code': 75,
'char': 'k',
},
},
};
function autoCommitRollback(type, api, transId, value) {
let url = url_for(`sqleditor.${type}`, {
'trans_id': transId,
});
return api.post(url, JSON.stringify(value));
}
export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
const queryToolConnCtx = useContext(QueryToolConnectionContext);
const [highlightFilter, setHighlightFilter] = useState(false);
const [limit, setLimit] = useState('-1');
const [buttonsDisabled, setButtonsDisabled] = useState({
'save': true,
'cancel': true,
'save-data': true,
'delete-rows': true,
'commit': true,
'rollback': true,
'filter': true,
'limit': false,
});
const [menuOpenId, setMenuOpenId] = React.useState(null);
const [checkedMenuItems, setCheckedMenuItems] = React.useState({});
/* Menu button refs */
const saveAsMenuRef = React.useRef(null);
const editMenuRef = React.useRef(null);
const autoCommitMenuRef = React.useRef(null);
const explainMenuRef = React.useRef(null);
const macrosMenuRef = React.useRef(null);
const filterMenuRef = React.useRef(null);
const queryToolPref = queryToolCtx.preferences.sqleditor;
const setDisableButton = useCallback((name, disable=true)=>{
setButtonsDisabled((prev)=>({...prev, [name]: disable}));
}, []);
const executeQuery = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
}, []);
const cancelQuery = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_STOP_EXECUTION);
}, []);
const explain = useCallback((analyze=false)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, {
format: 'json',
analyze: analyze,
verbose: Boolean(checkedMenuItems['explain_verbose']),
costs: Boolean(checkedMenuItems['explain_costs']),
buffers: analyze ? Boolean(checkedMenuItems['explain_buffers']) : false,
timing: analyze ? Boolean(checkedMenuItems['explain_timing']) : false,
summary: Boolean(checkedMenuItems['explain_summary']),
settings: Boolean(checkedMenuItems['explain_settings']),
});
}, [checkedMenuItems]);
const explainAnalyse = useCallback(()=>{
explain(true);
}, [explain]);
const openMenu = useCallback((e)=>{
setMenuOpenId(e.currentTarget.name);
}, []);
const handleMenuClose = useCallback(()=>{
setMenuOpenId(null);
}, []);
const checkMenuClick = useCallback((e)=>{
setCheckedMenuItems((prev)=>{
let newVal = !prev[e.value];
if(e.value === 'auto_commit' || e.value === 'auto_rollback') {
autoCommitRollback(e.value, queryToolCtx.api, queryToolCtx.params.trans_id, newVal)
.catch ((error)=>{
newVal = prev[e.value];
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
checkTransaction: true,
});
});
}
return {
...prev,
[e.value]: newVal,
};
});
}, []);
const openFile = useCallback(()=>{
confirmDiscard(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_LOAD_FILE);
});
}, []);
const saveFile = useCallback((saveAs=false)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, saveAs);
}, []);
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, ()=>{
setDisableButton('execute', true);
setDisableButton('cancel', false);
setDisableButton('explain', true);
setDisableButton('explain_analyse', true);
setDisableButton('limit', true);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, ()=>{
setDisableButton('execute', false);
setDisableButton('cancel', true);
setDisableButton('explain', false);
setDisableButton('explain_analyse', false);
setDisableButton('limit', false);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS, ()=>{
setDisableButton('execute', true);
setDisableButton('explain', true);
setDisableButton('explain_analyse', true);
setDisableButton('limit', true);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS_END, ()=>{
setDisableButton('execute', false);
setDisableButton('explain', false);
setDisableButton('explain_analyse', false);
setDisableButton('limit', false);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.QUERY_CHANGED, (isDirty)=>{
setDisableButton('save', !isDirty);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (isDirty)=>{
setDisableButton('save-data', !isDirty);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CHANGED, (rows)=>{
setDisableButton('delete-rows', !rows);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SET_FILTER_INFO, (canFilter, filterApplied)=>{
setDisableButton('filter', !canFilter);
setHighlightFilter(filterApplied);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SET_LIMIT_VALUE, (l)=>{
setLimit(l);
});
}, []);
useEffect(()=>{
setDisableButton('execute', queryToolConnCtx.obtainingConn);
setDisableButton('explain', queryToolConnCtx.obtainingConn);
setDisableButton('explain_analyse', queryToolConnCtx.obtainingConn);
}, [queryToolConnCtx.obtainingConn]);
const isInTxn = ()=>(queryToolConnCtx.connectionStatus == CONNECTION_STATUS.TRANSACTION_STATUS_INTRANS
|| queryToolConnCtx.connectionStatus == CONNECTION_STATUS.TRANSACTION_STATUS_INERROR);
const onExecutionDone = ()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, (success)=>{
if(success) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL);
}
}, true);
};
const warnTxnClose = ()=>{
if(!isInTxn() || !queryToolCtx.preferences?.sqleditor.prompt_commit_transaction) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL);
return;
}
queryToolCtx.modal.showModal(gettext('Commit transaction?'), (closeModal)=>(
<ConfirmTransactionContent
closeModal={closeModal}
text={gettext('The current transaction is not commited to the database. '
+'Do you want to commit or rollback the transaction?')}
onRollback={()=>{
onExecutionDone();
onRollbackClick();
}}
onCommit={()=>{
onExecutionDone();
onCommitClick();
}}
/>
));
};
useEffect(()=>{
if(isInTxn()) {
setDisableButton('commit', false);
setDisableButton('rollback', false);
} else {
setDisableButton('commit', true);
setDisableButton('rollback', true);
}
eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE, warnTxnClose);
return ()=>{
eventBus.deregisterListener(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE, warnTxnClose);
};
}, [queryToolConnCtx.connectionStatus]);
const onCommitClick=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, true);
};
const onRollbackClick=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, true);
};
const executeMacro = (m)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, m.sql, null, true);
};
const onLimitChange=(e)=>{
setLimit(e.target.value);
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SET_LIMIT,e.target.value);
};
const formatSQL=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL);
};
const clearQuery=()=>{
confirmDiscard(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, '');
});
};
const onHelpClick=()=>{
let url = url_for('help.static', {'filename': 'query_tool.html'});
window.open(url, 'pgadmin_help');
};
const confirmDiscard=(callback)=>{
queryToolCtx.modal.confirm(
gettext('Unsaved changes'),
gettext('Are you sure you wish to discard the current changes?'),
function() {
callback();
},
function() {
return true;
}
);
};
useEffect(()=>{
if(queryToolPref) {
/* Get the prefs first time */
if(_.isUndefined(checkedMenuItems.auto_commit)) {
setCheckedMenuItems({
auto_commit: queryToolPref.auto_commit,
auto_rollback: queryToolPref.auto_rollback,
explain_verbose: queryToolPref.explain_verbose,
explain_costs: queryToolPref.explain_costs,
explain_buffers: queryToolPref.explain_buffers,
explain_timing: queryToolPref.explain_timing,
explain_summary: queryToolPref.explain_summary,
explain_settings: queryToolPref.explain_settings,
});
}
}
}, [queryToolPref]);
/* Button shortcuts */
useKeyboardShortcuts([
{
shortcut: queryToolPref.execute_query,
options: {
callback: ()=>{executeQuery();}
}
},
{
shortcut: queryToolPref.explain_query,
options: {
callback: (e)=>{e.preventDefault();explain();}
}
},
{
shortcut: queryToolPref.explain_analyze_query,
options: {
callback: ()=>{explainAnalyse();}
}
},
{
shortcut: queryToolPref.commit_transaction,
options: {
callback: ()=>{onCommitClick();}
}
},
{
shortcut: queryToolPref.rollback_transaction,
options: {
callback: ()=>{onRollbackClick();}
}
},
{
shortcut: FIXED_PREF.format_sql,
options: {
callback: ()=>{formatSQL();}
}
},
{
shortcut: queryToolPref.clear_query,
options: {
callback: ()=>{clearQuery();}
}
},
], containerRef);
/* Macro shortcuts */
useKeyboardShortcuts(
queryToolCtx.params?.macros?.map((m)=>{
return {
shortcut: {
...m,
'key': {
'key_code': m.key_code,
'char': m.key,
},
},
options: {
callback: ()=>{executeMacro(m);}
}
};
}) || [],
containerRef
);
return (
<>
<Box className={classes.root}>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Open File')} icon={<FolderRoundedIcon />} disabled={!queryToolCtx.params.is_query_tool}
accesskey={shortcut_key(queryToolPref.btn_open_file)} onClick={openFile} />
<PgIconButton title={gettext('Save File')} icon={<SaveRoundedIcon />}
accesskey={shortcut_key(queryToolPref.btn_save_file)} disabled={buttonsDisabled['save'] || !queryToolCtx.params.is_query_tool}
onClick={()=>{saveFile(false);}} />
<PgIconButton title={gettext('File')} icon={<KeyboardArrowDownIcon />} splitButton disabled={!queryToolCtx.params.is_query_tool}
name="menu-saveas" ref={saveAsMenuRef} onClick={openMenu}
/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Edit')} icon={
<><EditRoundedIcon /><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></>}
disabled={!queryToolCtx.params.is_query_tool}
name="menu-edit" ref={editMenuRef} onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small" color={highlightFilter ? 'primary' : 'default'}>
<PgIconButton title={gettext('Sort/Filter')} icon={<FilterIcon />}
onClick={onFilterClick} disabled={buttonsDisabled['filter']} accesskey={shortcut_key(queryToolPref.btn_filter_dialog)}/>
<PgIconButton title={gettext('Filter options')} icon={<KeyboardArrowDownIcon />} splitButton
disabled={buttonsDisabled['filter']} name="menu-filter" ref={filterMenuRef} accesskey={shortcut_key(queryToolPref.btn_filter_options)}
onClick={openMenu} />
</PgButtonGroup>
<InputSelectNonSearch options={[
{label: gettext('No limit'), value: '-1'},
{label: gettext('1000 rows'), value: '1000'},
{label: gettext('500 rows'), value: '500'},
{label: gettext('100 rows'), value: '100'},
]} value={limit} onChange={onLimitChange} disabled={buttonsDisabled['limit'] || queryToolCtx.params.is_query_tool} />
<PgButtonGroup size="small">
<PgIconButton title={gettext('Cancel query')} icon={<StopRoundedIcon style={{height: 'unset'}} />}
onClick={cancelQuery} disabled={buttonsDisabled['cancel']} accesskey={shortcut_key(queryToolPref.btn_cancel_query)} />
<PgIconButton title={gettext('Execute/Refresh')} icon={<PlayArrowRoundedIcon style={{height: 'unset'}} />}
onClick={executeQuery} disabled={buttonsDisabled['execute']} shortcut={queryToolPref.execute_query}/>
<PgIconButton title={gettext('Execute options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-autocommit" ref={autoCommitMenuRef} accesskey={shortcut_key(queryToolPref.btn_delete_row)}
onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Explain')} icon={<ExplicitRoundedIcon />}
onClick={()=>{explain();}} disabled={buttonsDisabled['explain'] || !queryToolCtx.params.is_query_tool} shortcut={queryToolPref.explain_query}/>
<PgIconButton title={gettext('Explain Analyze')} icon={<AssessmentRoundedIcon />}
onClick={()=>{explainAnalyse();}} disabled={buttonsDisabled['explain_analyse'] || !queryToolCtx.params.is_query_tool} shortcut={queryToolPref.explain_analyze_query}/>
<PgIconButton title={gettext('Explain Settings')} icon={<KeyboardArrowDownIcon />} splitButton
disabled={!queryToolCtx.params.is_query_tool}
name="menu-explain" ref={explainMenuRef} onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Commit')} icon={<CommitIcon />}
onClick={onCommitClick} disabled={buttonsDisabled['commit']} shortcut={queryToolPref.commit_transaction}/>
<PgIconButton title={gettext('Rollback')} icon={<RollbackIcon />}
onClick={onRollbackClick} disabled={buttonsDisabled['rollback']} shortcut={queryToolPref.rollback_transaction}/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Macros')} icon={
<><FormatListNumberedRoundedIcon /><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></>}
disabled={!queryToolCtx.params.is_query_tool} name="menu-macros" ref={macrosMenuRef} onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Help')} icon={<HelpIcon />} onClick={onHelpClick} />
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={saveAsMenuRef}
open={menuOpenId=='menu-saveas'}
onClose={handleMenuClose}
>
<PgMenuItem onClick={()=>{saveFile(true);}}>{gettext('Save as')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={editMenuRef}
open={menuOpenId=='menu-edit'}
onClose={handleMenuClose}
>
<PgMenuItem shortcut={FIXED_PREF.find}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.replace}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.jump}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'jumpToLine');}}>{gettext('Jump')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={FIXED_PREF.indent}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentMore');}}>{gettext('Indent')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.unindent}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentLess');}}>{gettext('Unindent')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.comment}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'toggleComment');}}>{gettext('Toggle comment')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.clear_query}
onClick={clearQuery}>{gettext('Clear query')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={FIXED_PREF.format_sql}onClick={formatSQL}>{gettext('Format SQL')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={filterMenuRef}
open={menuOpenId=='menu-filter'}
onClose={handleMenuClose}
>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, true);}}>{gettext('Filter by Selection')}</PgMenuItem>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, false);}}>{gettext('Exclude by Selection')}</PgMenuItem>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_REMOVE_FILTER);}}>{gettext('Remove Sort/Filter')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={autoCommitMenuRef}
open={menuOpenId=='menu-autocommit'}
onClose={handleMenuClose}
>
<PgMenuItem hasCheck value="auto_commit" checked={checkedMenuItems['auto_commit']}
onClick={checkMenuClick}>{gettext('Auto commit?')}</PgMenuItem>
<PgMenuItem hasCheck value="auto_rollback" checked={checkedMenuItems['auto_rollback']}
onClick={checkMenuClick}>{gettext('Auto rollback on error?')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={explainMenuRef}
open={menuOpenId=='menu-explain'}
onClose={handleMenuClose}
>
<PgMenuItem hasCheck value="explain_verbose" checked={checkedMenuItems['explain_verbose']}
onClick={checkMenuClick}>{gettext('Verbose')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_costs" checked={checkedMenuItems['explain_costs']}
onClick={checkMenuClick}>{gettext('Costs')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_buffers" checked={checkedMenuItems['explain_buffers']}
onClick={checkMenuClick}>{gettext('Buffers')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_timing" checked={checkedMenuItems['explain_timing']}
onClick={checkMenuClick}>{gettext('Timing')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_summary" checked={checkedMenuItems['explain_summary']}
onClick={checkMenuClick}>{gettext('Summary')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_settings" checked={checkedMenuItems['explain_settings']}
onClick={checkMenuClick}>{gettext('Settings')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={macrosMenuRef}
open={menuOpenId=='menu-macros'}
onClose={handleMenuClose}
>
<PgMenuItem onClick={onManageMacros}>{gettext('Manage macros')}</PgMenuItem>
<PgMenuDivider />
{queryToolCtx.params?.macros?.map((m, i)=>{
return (
<PgMenuItem shortcut={{
...m,
'key': {
'key_code': m.key_code,
'char': m.key,
},
}} onClick={()=>executeMacro(m)} key={i}>
{m.name}
</PgMenuItem>
);
})}
</PgMenu>
</>
);
}
MainToolBar.propTypes = {
containerRef: CustomPropTypes.ref,
onFilterClick: PropTypes.func,
onManageMacros: PropTypes.func,
};

View File

@@ -0,0 +1,46 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { QueryToolEventsContext } from '../QueryToolComponent';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
const useStyles = makeStyles((theme)=>({
root: {
whiteSpace: 'pre-wrap',
fontFamily: '"Source Code Pro", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
padding: '5px 10px',
overflow: 'auto',
height: '100%',
fontSize: '12px',
userSelect: 'text',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
...theme.mixins.fontSourceCode,
}
}));
export function Messages() {
const classes = useStyles();
const [messageText, setMessageText] = React.useState('');
const eventBus = React.useContext(QueryToolEventsContext);
React.useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.SET_MESSAGE, (text, append=false)=>{
setMessageText((prev)=>{
if(append) {
return prev+text;
}
return text;
});
});
}, []);
return (
<div className={classes.root} tabIndex="0">{messageText}</div>
);
}

View File

@@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { commonTableStyles } from '../../../../../../static/js/Theme';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import gettext from 'sources/gettext';
import _ from 'lodash';
import clsx from 'clsx';
import { QueryToolEventsContext } from '../QueryToolComponent';
export function Notifications() {
const [notices, setNotices] = React.useState([]);
const tableClasses = commonTableStyles();
const eventBus = React.useContext(QueryToolEventsContext);
React.useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.PUSH_NOTICE, (notice)=>{
if(_.isArray(notice)) {
setNotices((prev)=>[
...prev,
...notice,
]);
} else {
setNotices((prev)=>[
...prev,
notice,
]);
}
});
}, []);
return <table className={clsx(tableClasses.table, tableClasses.borderBottom)}>
<thead>
<tr>
<th>{gettext('Recorded time')}</th>
<th>{gettext('Event')}</th>
<th>{gettext('Process ID')}</th>
<th>{gettext('Payload')}</th>
</tr>
</thead>
<tbody>
{notices.map((notice, i)=>{
return <tr key={i}>
<td>{notice.recorded_time}</td>
<td>{notice.channel}</td>
<td>{notice.pid}</td>
<td>{notice.payload}</td>
</tr>;
})}
</tbody>
</table>;
}

View File

@@ -0,0 +1,401 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/styles';
import React, {useContext, useCallback, useEffect } from 'react';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import {PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import url_for from 'sources/url_for';
import { LayoutEventsContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import ConfirmSaveContent from '../dialogs/ConfirmSaveContent';
import gettext from 'sources/gettext';
import OrigCodeMirror from 'bundled_codemirror';
import Notifier from '../../../../../../static/js/helpers/Notifier';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
const useStyles = makeStyles(()=>({
sql: {
height: '100%',
}
}));
function registerAutocomplete(api, transId, onFailure) {
OrigCodeMirror.registerHelper('hint', 'sql', function (editor) {
var data = [],
doc = editor.getDoc(),
cur = doc.getCursor(),
// Get the current cursor position
current_cur = cur.ch,
// function context
ctx = {
editor: editor,
// URL for auto-complete
url: url_for('sqleditor.autocomplete', {
'trans_id': transId,
}),
data: data,
// Get the line number in the cursor position
current_line: cur.line,
/*
* Render function for hint to add our own class
* and icon as per the object type.
*/
hint_render: function (elt, data_arg, cur_arg) {
var el = document.createElement('span');
switch (cur_arg.type) {
case 'database':
el.className = 'sqleditor-hint pg-icon-' + cur_arg.type;
break;
case 'datatype':
el.className = 'sqleditor-hint icon-type';
break;
case 'keyword':
el.className = 'fa fa-key';
break;
case 'table alias':
el.className = 'fa fa-at';
break;
default:
el.className = 'sqleditor-hint icon-' + cur_arg.type;
}
el.appendChild(document.createTextNode(cur_arg.text));
elt.appendChild(el);
},
};
data.push(doc.getValue());
// Get the text from start to the current cursor position.
data.push(
doc.getRange({
line: 0,
ch: 0,
}, {
line: ctx.current_line,
ch: current_cur,
})
);
return {
then: function (cb) {
var self_local = this;
// Make ajax call to find the autocomplete data
api.post(self_local.url, JSON.stringify(self_local.data))
.then((res) => {
var result = [];
_.each(res.data.data.result, function (obj, key) {
result.push({
text: key,
type: obj.object_type,
render: self_local.hint_render,
});
});
// Sort function to sort the suggestion's alphabetically.
result.sort(function (a, b) {
var textA = a.text.toLowerCase(),
textB = b.text.toLowerCase();
if (textA < textB) //sort string ascending
return -1;
if (textA > textB)
return 1;
return 0; //default return value (no sorting)
});
/*
* Below logic find the start and end point
* to replace the selected auto complete suggestion.
*/
var token = self_local.editor.getTokenAt(cur),
start, end, search;
if (token.end > cur.ch) {
token.end = cur.ch;
token.string = token.string.slice(0, cur.ch - token.start);
}
if (token.string.match(/^[.`\w@]\w*$/)) {
search = token.string;
start = token.start;
end = token.end;
} else {
start = end = cur.ch;
search = '';
}
/*
* Added 1 in the start position if search string
* started with "." or "`" else auto complete of code mirror
* will remove the "." when user select any suggestion.
*/
if (search.charAt(0) == '.' || search.charAt(0) == '``')
start += 1;
cb({
list: result,
from: {
line: self_local.current_line,
ch: start,
},
to: {
line: self_local.current_line,
ch: end,
},
});
})
.catch((err) => {
onFailure?.(err);
});
}.bind(ctx),
};
});
}
export default function Query() {
const classes = useStyles();
const editor = React.useRef();
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
const layoutEvenBus = useContext(LayoutEventsContext);
const lastSavedText = React.useRef('');
const markedLine = React.useRef(0);
const removeHighlightError = (cmObj)=>{
// Remove already existing marker
cmObj.removeLineClass(markedLine.current, 'wrap', 'CodeMirror-activeline-background');
markedLine.current = 0;
};
const highlightError = (cmObj, result)=>{
let errorLineNo = 0,
startMarker = 0,
endMarker = 0,
selectedLineNo = 0;
removeHighlightError(cmObj);
// In case of selection we need to find the actual line no
if (cmObj.getSelection().length > 0)
selectedLineNo = cmObj.getCursor(true).line;
// Fetch the LINE string using regex from the result
var line = /LINE (\d+)/.exec(result),
// Fetch the Character string using regex from the result
char = /Character: (\d+)/.exec(result);
// If line and character is null then no need to mark
if (line != null && char != null) {
errorLineNo = (parseInt(line[1]) - 1) + selectedLineNo;
var errorCharNo = (parseInt(char[1]) - 1);
/* We need to loop through each line till the error line and
* count the total no of character to figure out the actual
* starting/ending marker point for the individual line. We
* have also added 1 per line for the "\n" character.
*/
var prevLineChars = 0;
for (let i = selectedLineNo > 0 ? selectedLineNo : 0; i < errorLineNo; i++)
prevLineChars += cmObj.getLine(i).length + 1;
/* Marker starting point for the individual line is
* equal to error character index minus total no of
* character till the error line starts.
*/
startMarker = errorCharNo - prevLineChars;
// Find the next space from the character or end of line
var errorLine = cmObj.getLine(errorLineNo);
if (_.isUndefined(errorLine)) return;
endMarker = errorLine.indexOf(' ', startMarker);
if (endMarker < 0)
endMarker = errorLine.length;
// Mark the error text
cmObj.markText({
line: errorLineNo,
ch: startMarker,
}, {
line: errorLineNo,
ch: endMarker,
}, {
className: 'sql-editor-mark',
});
markedLine.current = errorLineNo;
cmObj.addLineClass(errorLineNo, 'wrap', 'CodeMirror-activeline-background');
cmObj.focus();
cmObj.setCursor(errorLineNo, 0);
}
};
const triggerExecution = (explainObject)=>{
if(queryToolCtx.params.is_query_tool) {
let query = editor.current?.getSelection() || editor.current?.getValue() || '';
if(query) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject);
}
} else {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null);
}
};
useEffect(()=>{
layoutEvenBus.registerListener(LAYOUT_EVENTS.ACTIVE, (currentTabId)=>{
currentTabId == PANELS.QUERY && editor.current.focus();
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, triggerExecution);
eventBus.registerListener(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, (result)=>{
if(result) {
highlightError(editor.current, result);
} else {
removeHighlightError(editor.current);
}
});
eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName)=>{
queryToolCtx.api.post(url_for('sqleditor.load_file'), {
'file_name': decodeURI(fileName),
}).then((res)=>{
editor.current.setValue(res.data);
lastSavedText.current = res.data;
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, isDirty());
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false);
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_FILE, (fileName)=>{
let editorValue = editor.current.getValue();
queryToolCtx.api.post(url_for('sqleditor.save_file'), {
'file_name': decodeURI(fileName),
'file_content': editor.current.getValue(),
}).then(()=>{
lastSavedText.current = editorValue;
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileName, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, isDirty());
Notifier.success(gettext('File saved successfully.'));
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, null, false);
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, (cmd='')=>{
editor.current?.execCommand(cmd);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, (text)=>{
editor.current?.setValue(text);
eventBus.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.QUERY);
setTimeout(()=>{
editor.current?.focus();
editor.current?.setCursor(editor.current.lineCount(), 0);
}, 250);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, (replace=false)=>{
editor.current?.focus();
let key = {
keyCode: 70, metaKey: false, ctrlKey: true, shiftKey: replace, altKey: false,
};
if(isMac()) {
key.metaKey = true;
key.ctrlKey = false;
key.shiftKey = false;
key.altKey = true;
}
editor.current?.triggerOnKeyDown(
new KeyboardEvent('keydown', key)
);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, (value, focus=true)=>{
focus && editor.current?.focus();
editor.current?.setValue(value);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE, ()=>{
change();
});
eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE, ()=>{
if(!isDirty() || !queryToolCtx.preferences?.sqleditor.prompt_save_query_changes) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
return;
}
queryToolCtx.modal.showModal(gettext('Save query changes?'), (closeModal)=>(
<ConfirmSaveContent
closeModal={closeModal}
text={gettext('The query text has changed. Do you want to save changes?')}
onDontSave={()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
}}
onSave={()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, (_f, success)=>{
if(success) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
}
}, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE);
}}
/>
));
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, ()=>{
let selection = true, sql = editor.current?.getSelection();
if(sql == '') {
sql = editor.current.getValue();
selection = false;
}
queryToolCtx.api.post(url_for('sql.format'), {
'sql': sql,
}).then((res)=>{
if(selection) {
editor.current.replaceSelection(res.data.data.sql, 'around');
} else {
editor.current.setValue(res.data.data.sql);
}
}).catch(()=>{/* failure should be ignored */});
});
editor.current.focus();
}, []);
useEffect(()=>{
registerAutocomplete(queryToolCtx.api, queryToolCtx.params.trans_id, (err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
}, [queryToolCtx.params.trans_id]);
const isDirty = ()=>lastSavedText.current !== editor.current.getValue();
const cursorActivity = useCallback((cmObj)=>{
const c = cmObj.getCursor();
eventBus.fireEvent(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, [c.line+1, c.ch+1]);
}, []);
const change = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, isDirty());
}, []);
return <CodeMirror
currEditor={(obj)=>{
editor.current=obj;
}}
value={''}
className={classes.sql}
events={{
'focus': cursorActivity,
'cursorActivity': cursorActivity,
'change': change,
}}
disabled={!queryToolCtx.params.is_query_tool}
autocomplete={true}
/>;
}

View File

@@ -0,0 +1,500 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { PANELS, QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import gettext from 'sources/gettext';
import _ from 'lodash';
import clsx from 'clsx';
import { Box, Grid, List, ListItem, ListSubheader } from '@material-ui/core';
import url_for from 'sources/url_for';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import moment from 'moment';
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
import AssessmentRoundedIcon from '@material-ui/icons/AssessmentRounded';
import ExplicitRoundedIcon from '@material-ui/icons/ExplicitRounded';
import { SaveDataIcon, CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import { InputSwitch } from '../../../../../../static/js/components/FormComponents';
import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import { DefaultButton } from '../../../../../../static/js/components/Buttons';
import { useDelayedCaller } from '../../../../../../static/js/custom_hooks';
import Notifier from '../../../../../../static/js/helpers/Notifier';
import Loader from 'sources/components/Loader';
import { LayoutEventsContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import PropTypes from 'prop-types';
import { parseApiError } from '../../../../../../static/js/api_instance';
import * as clipboard from '../../../../../../static/js/clipboard';
const useStyles = makeStyles((theme)=>({
leftRoot: {
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.right,
},
listRoot: {
...theme.mixins.panelBorder.top,
},
listSubheader: {
padding: '0.25rem',
lineHeight: 'unset',
color: theme.palette.text.muted,
backgroundColor: theme.palette.background.default,
...theme.mixins.panelBorder.bottom,
...theme.mixins.fontSourceCode,
},
removePadding: {
padding: 0,
},
fontSourceCode:{
...theme.mixins.fontSourceCode,
userSelect: 'text',
},
itemError: {
backgroundColor: theme.palette.error.light,
'&.Mui-selected': {
backgroundColor: theme.palette.error.light,
'&:hover': {
backgroundColor: theme.palette.error.light,
}
}
},
detailsQuery: {
marginTop: '0.5rem',
...theme.mixins.panelBorder.all,
},
copyBtn: {
borderRadius: 0,
paddingLeft: '8px',
paddingRight: '8px',
borderTop: 'none',
borderLeft: 'none',
borderColor: theme.otherVars.borderColor,
fontSize: '13px',
},
infoHeader: {
fontSize: '13px',
padding: '0.5rem',
backgroundColor: theme.otherVars.editorToolbarBg,
},
removeBtnMargin: {
marginLeft: '0.25rem',
}
}));
export const QuerySources = {
EXECUTE: {
ICON_CSS_CLASS: 'fa fa-play',
},
EXPLAIN: {
ICON_CSS_CLASS: 'fa fa-hand-pointer',
},
EXPLAIN_ANALYZE: {
ICON_CSS_CLASS: 'fa fa-list-alt',
},
COMMIT: {
ICON_CSS_CLASS: 'pg-font-icon icon-commit',
},
ROLLBACK: {
ICON_CSS_CLASS: 'pg-font-icon icon-rollback',
},
SAVE_DATA: {
ICON_CSS_CLASS: 'pg-font-icon icon-save_data_changes',
},
VIEW_DATA: {
ICON_CSS_CLASS: 'pg-font-icon icon-view_data',
},
};
class QueryHistoryUtils {
constructor() {
this._entries = [];
this.showInternal = true;
}
dateAsGroupKey(date) {
return moment(date).format('YYYY MM DD');
}
getItemKey(entry) {
return this.dateAsGroupKey(entry.start_time) + this.formatEntryDate(entry.start_time) + (entry.subKey ?? '');
}
getDateFormatted(date) {
return date.toLocaleDateString();
}
formatEntryDate(date) {
return moment(date).format('HH:mm:ss');
}
isDaysBefore(date, before) {
return (
this.getDateFormatted(date) ===
this.getDateFormatted(moment().subtract(before, 'days').toDate())
);
}
getDatePrefix(date) {
let prefix = '';
if (this.isDaysBefore(date, 0)) {
prefix = 'Today - ';
} else if (this.isDaysBefore(date, 1)) {
prefix = 'Yesterday - ';
}
return prefix;
}
addEntry(entry) {
entry.groupKey = this.dateAsGroupKey(entry.start_time);
entry.itemKey = this.getItemKey(entry);
let existEntry = _.find(this._entries, (e)=>e.itemKey==entry.itemKey);
if(existEntry) {
entry.itemKey = this.getItemKey(entry) + _.uniqueId();
}
let insertIndex = _.sortedIndexBy(this._entries, entry, (e)=>e.itemKey);
this._entries = [
...this._entries.slice(0, insertIndex),
entry,
...this._entries.slice(insertIndex),
];
}
getEntries() {
if(!this.showInternal) {
return this._entries.filter((e)=>!e.is_pgadmin_query);
}
return this._entries;
}
getEntry(itemKey) {
return _.find(this.getEntries(), (e)=>e.itemKey==itemKey);
}
getGroupHeader(entry) {
return this.getDatePrefix(entry.start_time)+this.getDateFormatted(entry.start_time);
}
getGroups() {
return _.sortedUniqBy(this.getEntries().map((e)=>[e.groupKey, this.getGroupHeader(e)]), (g)=>g[0]).reverse();
}
getGroupEntries(groupKey) {
return this.getEntries().filter((e)=>e.groupKey==groupKey).reverse();
}
getNextItemKey(currKey) {
let nextIndex = this.getEntries().length-1;
if(currKey) {
let currIndex = _.findIndex(this.getEntries(), (e)=>e.itemKey==currKey);
if(currIndex == 0) {
nextIndex = currIndex;
} else {
nextIndex = currIndex - 1;
}
}
return this.getEntries()[nextIndex]?.itemKey;
}
getPrevItemKey(currKey) {
let nextIndex = this.getEntries().length-1;
if(currKey) {
let currIndex = _.findIndex(this.getEntries(), (e)=>e.itemKey==currKey);
if(currIndex == this.getEntries().length-1) {
nextIndex = currIndex;
} else {
nextIndex = currIndex + 1;
}
}
return this.getEntries()[nextIndex]?.itemKey;
}
clear(itemKey) {
if(itemKey) {
let nextKey = this.getNextItemKey(itemKey);
let removeIdx = _.findIndex(this._entries, (e)=>e.itemKey==itemKey);
this._entries.splice(removeIdx, 1);
return nextKey;
} else {
this._entries = [];
}
}
size() {
return this._entries.length;
}
}
function QuerySourceIcon({source}) {
switch(JSON.stringify(source)) {
case JSON.stringify(QuerySources.EXECUTE):
return <PlayArrowRoundedIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.EXPLAIN):
return <ExplicitRoundedIcon/>;
case JSON.stringify(QuerySources.EXPLAIN_ANALYZE):
return <AssessmentRoundedIcon/>;
case JSON.stringify(QuerySources.COMMIT):
return <CommitIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.ROLLBACK):
return <RollbackIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.SAVE_DATA):
return <SaveDataIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.VIEW_DATA):
return <SaveDataIcon style={{marginLeft: '-4px'}}/>;
default:
return <></>;
}
}
QuerySourceIcon.propTypes = {
source: PropTypes.object,
};
function HistoryEntry({entry, formatEntryDate, itemKey, selectedItemKey, onClick}) {
const classes = useStyles();
return <ListItem tabIndex="0" ref={(ele)=>{
selectedItemKey==itemKey && ele && ele.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
}} className={clsx(classes.fontSourceCode, entry.status ? '' : classes.itemError)} selected={selectedItemKey==itemKey} onClick={onClick}>
<Box whiteSpace="nowrap" textOverflow="ellipsis" overflow="hidden" >
<QuerySourceIcon source={entry.query_source}/>
{entry.query}
</Box>
<Box fontSize="12px">
{formatEntryDate(entry.start_time)}
</Box>
</ListItem>;
}
const EntryPropType = PropTypes.shape({
info: PropTypes.string,
status: PropTypes.bool,
start_time: PropTypes.objectOf(Date),
query: PropTypes.string,
row_affected: PropTypes.number,
total_time: PropTypes.string,
message: PropTypes.string,
query_source: PropTypes.object,
is_pgadmin_query: PropTypes.bool,
});
HistoryEntry.propTypes = {
entry: EntryPropType,
formatEntryDate: PropTypes.func,
itemKey: PropTypes.string,
selectedItemKey: PropTypes.string,
onClick: PropTypes.func,
};
function QueryHistoryDetails({entry}) {
const classes = useStyles();
const [copyText, setCopyText] = React.useState(gettext('Copy'));
const eventBus = React.useContext(QueryToolEventsContext);
const revertCopiedText = useDelayedCaller(()=>{
setCopyText(gettext('Copy'));
});
const onCopyClick = React.useCallback(()=>{
clipboard.copyToClipboard(entry.query);
setCopyText(gettext('Copied!'));
revertCopiedText(1500);
}, [entry]);
const onCopyToEditor = React.useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, entry.query);
}, [entry]);
if(!entry) {
return <></>;
}
return (
<>
{entry.info && <Box className={classes.infoHeader}>{entry.info}</Box>}
<Box padding="0.5rem">
<Grid container>
<Grid item sm={4}>{entry.start_time.toLocaleDateString() + ' ' + entry.start_time.toLocaleTimeString()}</Grid>
<Grid item sm={4}>{entry?.row_affected > 0 && entry.row_affected}</Grid>
<Grid item sm={4}>{entry.total_time}</Grid>
</Grid>
<Grid container>
<Grid item sm={4}>{gettext('Date')}</Grid>
<Grid item sm={4}>{gettext('Rows affected')}</Grid>
<Grid item sm={4}>{gettext('Duration')}</Grid>
</Grid>
<Box className={classes.detailsQuery}>
<DefaultButton size="xs" className={classes.copyBtn} onClick={onCopyClick}>{copyText}</DefaultButton>
<DefaultButton size="xs" className={classes.copyBtn} onClick={onCopyToEditor}>{gettext('Copy to Query Editor')}</DefaultButton>
<CodeMirror
value={entry.query}
options={{
foldGutter: false,
lineNumbers: false,
gutters: [],
readOnly: true,
}}
/>
</Box>
<Box marginTop="0.5rem">
<Box>{gettext('Messages')}</Box>
<Box className={classes.fontSourceCode} fontSize="13px" whiteSpace="pre-wrap">{entry.message}</Box>
</Box>
</Box>
</>
);
}
QueryHistoryDetails.propTypes = {
entry: EntryPropType,
};
export function QueryHistory() {
const qhu = React.useRef(new QueryHistoryUtils());
const queryToolCtx = React.useContext(QueryToolContext);
const classes = useStyles();
const eventBus = React.useContext(QueryToolEventsContext);
const [selectedItemKey, setSelectedItemKey] = React.useState(1);
const [showInternal, setShowInternal] = React.useState(true);
const [loaderText, setLoaderText] = React.useState('');
const [,refresh] = React.useState({});
const selectedEntry = qhu.current.getEntry(selectedItemKey);
const layoutEvenBus = React.useContext(LayoutEventsContext);
const listRef = React.useRef();
React.useEffect(async ()=>{
layoutEvenBus.registerListener(LAYOUT_EVENTS.ACTIVE, (currentTabId)=>{
currentTabId == PANELS.HISTORY && listRef.current.focus();
});
setLoaderText(gettext('Fetching history...'));
try {
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_query_history', {
'trans_id': queryToolCtx.params.trans_id,
}));
respData.data.result.forEach((h)=>{
h = JSON.parse(h);
h.start_time_orig = h.start_time;
h.start_time = new Date(h.start_time);
qhu.current.addEntry(h);
});
setSelectedItemKey(qhu.current.getNextItemKey());
} catch (error) {
console.error(error);
Notifier.error(gettext('Failed to fetch query history.') + parseApiError(error));
}
setLoaderText('');
const pushHistory = (h)=>{
qhu.current.addEntry(h);
refresh({});
};
listRef.current.focus();
eventBus.registerListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
}, []);
const onRemove = async ()=>{
setLoaderText(gettext('Removing history entry...'));
try {
await queryToolCtx.api.delete(url_for('sqleditor.clear_query_history', {
'trans_id': queryToolCtx.params.trans_id,
}), {
data: {
query: selectedEntry.query,
start_time: selectedEntry.start_time,
}
});
setSelectedItemKey(qhu.current.clear(selectedItemKey));
} catch (error) {
console.error(error);
Notifier.error(gettext('Failed to remove query history.') + parseApiError(error));
}
setLoaderText('');
};
const onRemoveAll = React.useCallback(()=>{
queryToolCtx.modal.confirm(gettext('Clear history'),
gettext('Are you sure you wish to clear the history?') + '</br>' +
gettext('This will remove all of your query history from this and other sessions for this database.'),
async function() {
setLoaderText(gettext('Removing history...'));
try {
await queryToolCtx.api.delete(url_for('sqleditor.clear_query_history', {
'trans_id': queryToolCtx.params.trans_id,
}));
qhu.current.clear();
setSelectedItemKey(null);
} catch (error) {
console.error(error);
Notifier.error(gettext('Failed to remove query history.') + parseApiError(error));
}
setLoaderText('');
},
function() {
return true;
}
);
}, []);
const onKeyPressed = (e) => {
if (e.keyCode == '38') {
e.preventDefault();
setSelectedItemKey(qhu.current.getPrevItemKey(selectedItemKey));
} else if (e.keyCode == '40') {
e.preventDefault();
setSelectedItemKey(qhu.current.getNextItemKey(selectedItemKey));
}
};
return (
<>
<Loader message={loaderText} />
{React.useMemo(()=>(
<Box display="flex" height="100%">
<Box flexBasis="50%" maxWidth="50%" className={classes.leftRoot}>
<Box padding="0.25rem" display="flex">
{gettext('Show queries generated internally by pgAdmin?')}
<InputSwitch value={showInternal} onChange={(e)=>{
setShowInternal(e.target.checked);
qhu.current.showInternal = e.target.checked;
setSelectedItemKey(qhu.current.getNextItemKey());
}} />
<Box marginLeft="auto">
<DefaultButton size="small" disabled={!selectedItemKey} onClick={onRemove}>Remove</DefaultButton>
<DefaultButton size="small" disabled={!qhu.current?.getGroups()?.length}
className={classes.removeBtnMargin} onClick={onRemoveAll}>Remove All</DefaultButton>
</Box>
</Box>
<Box flexGrow="1" overflow="auto" className={classes.listRoot}>
<List innerRef={listRef} className={classes.root} subheader={<li />} tabIndex="0" onKeyDown={onKeyPressed}>
{qhu.current.getGroups().map(([groupKey, groupHeader]) => (
<ListItem key={`section-${groupKey}`} className={classes.removePadding}>
<List className={classes.removePadding}>
<ListSubheader className={classes.listSubheader}>{groupHeader}</ListSubheader>
{qhu.current.getGroupEntries(groupKey).map((entry) => (
<HistoryEntry key={entry.itemKey} entry={entry} formatEntryDate={qhu.current.formatEntryDate}
itemKey={entry.itemKey} selectedItemKey={selectedItemKey} onClick={()=>{setSelectedItemKey(entry.itemKey);}}/>
))}
</List>
</ListItem>
))}
</List>
</Box>
</Box>
<Box flexBasis="50%" maxWidth="50%" overflow="auto">
<QueryHistoryDetails entry={selectedEntry}/>
</Box>
</Box>
), [selectedItemKey, showInternal, qhu.current.size()])}
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useState} from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box } from '@material-ui/core';
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import PlaylistAddRoundedIcon from '@material-ui/icons/PlaylistAddRounded';
import FileCopyRoundedIcon from '@material-ui/icons/FileCopyRounded';
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
import { PasteIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import GetAppRoundedIcon from '@material-ui/icons/GetAppRounded';
import {QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import { useKeyboardShortcuts } from '../../../../../../static/js/custom_hooks';
import {shortcut_key} from 'sources/keyboard_shortcuts';
import CopyData from '../QueryToolDataGrid/CopyData';
import PropTypes from 'prop-types';
import CustomPropTypes from '../../../../../../static/js/custom_prop_types';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.bottom,
},
}));
export function ResultSetToolbar({containerRef, canEdit}) {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
const [buttonsDisabled, setButtonsDisabled] = useState({
'save-data': true,
'delete-rows': true,
'copy-rows': true,
});
const [menuOpenId, setMenuOpenId] = React.useState(null);
const [checkedMenuItems, setCheckedMenuItems] = React.useState({});
/* Menu button refs */
const copyMenuRef = React.useRef(null);
const queryToolPref = queryToolCtx.preferences.sqleditor;
const setDisableButton = useCallback((name, disable=true)=>{
setButtonsDisabled((prev)=>({...prev, [name]: disable}));
}, []);
const saveData = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_DATA);
}, []);
const deleteRows = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS);
}, []);
const pasteRows = useCallback(async ()=>{
let copyUtils = new CopyData({
quoting: queryToolPref.results_grid_quoting,
quote_char: queryToolPref.results_grid_quote_char,
field_separator: queryToolPref.results_grid_field_separator,
});
let copiedRows = copyUtils.getCopiedRows();
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, copiedRows);
}, [queryToolPref]);
const copyData = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.COPY_DATA, checkedMenuItems['copy_with_headers']);
};
const addRow = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, [[]]);
}, []);
const downloadResult = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS);
}, []);
const openMenu = useCallback((e)=>{
setMenuOpenId(e.currentTarget.name);
}, []);
const handleMenuClose = useCallback(()=>{
setMenuOpenId(null);
}, []);
const checkMenuClick = useCallback((e)=>{
setCheckedMenuItems((prev)=>{
let newVal = !prev[e.value];
return {
...prev,
[e.value]: newVal,
};
});
}, []);
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (isDirty)=>{
setDisableButton('save-data', !isDirty);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CHANGED, (rows, cols)=>{
setDisableButton('delete-rows', !rows);
setDisableButton('copy-rows', (!rows && !cols));
});
}, []);
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA, copyData);
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA, copyData);
}, [checkedMenuItems['copy_with_headers']]);
useKeyboardShortcuts([
{
shortcut: queryToolPref.save_data,
options: {
callback: ()=>{saveData();}
}
},
{
shortcut: queryToolPref.download_results,
options: {
callback: (e)=>{e.preventDefault(); downloadResult();}
}
},
], containerRef);
return (
<>
<Box className={classes.root}>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
accesskey={shortcut_key(queryToolPref.btn_add_row)} disabled={!canEdit} onClick={addRow} />
<PgIconButton title={gettext('Copy')} icon={<FileCopyRoundedIcon />}
accesskey={shortcut_key(queryToolPref.btn_copy_row)} disabled={buttonsDisabled['copy-rows']} onClick={copyData} />
<PgIconButton title={gettext('Copy options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-copyheader" ref={copyMenuRef} onClick={openMenu} />
<PgIconButton title={gettext('Paste')} icon={<PasteIcon />}
accesskey={shortcut_key(queryToolPref.btn_paste_row)} disabled={!canEdit} onClick={pasteRows} />
<PgIconButton title={gettext('Delete')} icon={<DeleteRoundedIcon />}
accesskey={shortcut_key(queryToolPref.btn_delete_row)} disabled={buttonsDisabled['delete-rows'] || !canEdit} onClick={deleteRows} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Save Data Changes')} icon={<SaveDataIcon />}
shortcut={queryToolPref.save_data} disabled={buttonsDisabled['save-data'] || !canEdit} onClick={saveData}/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Save results to file')} icon={<GetAppRoundedIcon />}
onClick={downloadResult} shortcut={queryToolPref.download_results}/>
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={copyMenuRef}
open={menuOpenId=='menu-copyheader'}
onClose={handleMenuClose}
>
<PgMenuItem hasCheck value="copy_with_headers" checked={checkedMenuItems['copy_with_headers']} onClick={checkMenuClick}>Copy with headers</PgMenuItem>
</PgMenu>
</>
);
}
ResultSetToolbar.propTypes = {
containerRef: CustomPropTypes.ref,
canEdit: PropTypes.bool,
};

View File

@@ -0,0 +1,117 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useState, useContext } from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box } from '@material-ui/core';
import clsx from 'clsx';
import _ from 'lodash';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import { useStopwatch } from '../../../../../../static/js/custom_hooks';
import { QueryToolEventsContext } from '../QueryToolComponent';
const useStyles = makeStyles((theme)=>({
root: {
display: 'flex',
alignItems: 'center',
...theme.mixins.panelBorder.top,
flexWrap: 'wrap',
backgroundColor: theme.otherVars.editorToolbarBg,
userSelect: 'text',
},
padding: {
padding: '2px 12px',
},
divider: {
...theme.mixins.panelBorder.right,
},
mlAuto: {
marginLeft: 'auto',
}
}));
export function StatusBar() {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
const [position, setPosition] = useState([1, 1]);
const [lastTaskText, setLastTaskText] = useState(null);
const [rowsCount, setRowsCount] = useState([0, 0]);
const [selectedRowsCount, setSelectedRowsCount] = useState(0);
const [dataRowChangeCounts, setDataRowChangeCounts] = useState({
isDirty: false,
added: 0,
updated: 0,
deleted: 0,
});
const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({});
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{
setPosition(newPos||[1, 1]);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, ()=>{
pauseTimer();
setLastTaskText('Query complete');
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TASK_START, (taskText, startTime)=>{
resetTimer();
startTimer(startTime);
setLastTaskText(taskText);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TASK_END, (taskText, endTime)=>{
pauseTimer(endTime);
setLastTaskText(taskText);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.ROWS_FETCHED, (fetched, total)=>{
setRowsCount([fetched||0, total||0]);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CHANGED, (rows)=>{
setSelectedRowsCount(rows);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (_isDirty, dataChangeStore)=>{
setDataRowChangeCounts({
added: Object.keys(dataChangeStore.added||{}).length,
updated: Object.keys(dataChangeStore.updated||{}).length,
deleted: Object.keys(dataChangeStore.deleted||{}).length,
});
});
}, []);
let stagedText = '';
if(dataRowChangeCounts.added > 0) {
stagedText += ` Added: ${dataRowChangeCounts.added};`;
}
if(dataRowChangeCounts.updated > 0) {
stagedText += ` Updated: ${dataRowChangeCounts.updated};`;
}
if(dataRowChangeCounts.deleted > 0) {
stagedText += ` Deleted: ${dataRowChangeCounts.deleted};`;
}
return (
<Box className={classes.root}>
<Box className={clsx(classes.padding, classes.divider)}>Total rows: {rowsCount[0]} of {rowsCount[1]}</Box>
{lastTaskText &&
<Box className={clsx(classes.padding, classes.divider)}>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
}
{!lastTaskText && !_.isNull(lastTaskText) &&
<Box className={clsx(classes.padding, classes.divider)}>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
}
{Boolean(selectedRowsCount) &&
<Box className={clsx(classes.padding, classes.divider)}>Rows selected: {selectedRowsCount}</Box>}
{stagedText &&
<Box className={clsx(classes.padding, classes.divider)}>
<span>Changes staged: {stagedText}</span>
</Box>
}
<Box className={clsx(classes.padding, classes.mlAuto)}>Ln {position[0]}, Col {position[1]}</Box>
</Box>
);
}

View File

@@ -0,0 +1,26 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import SQLEditor from './SQLEditorModule';
/* eslint-disable */
/* This is used to change publicPath of webpack at runtime for loading chunks */
/* Do not add let, var, const to this variable */
__webpack_public_path__ = window.resourceBasePath;
/* eslint-enable */
if(!pgAdmin.Tools) {
pgAdmin.Tools = {};
}
pgAdmin.Tools.SQLEditor = SQLEditor.getInstance(pgAdmin, pgBrowser);
module.exports = {
SQLEditor: SQLEditor,
};

View File

@@ -9,7 +9,7 @@
import gettext from '../../../../static/js/gettext';
import url_for from '../../../../static/js/url_for';
import {getPanelTitle} from './datagrid_panel_title';
import {getPanelTitle} from './sqleditor_title';
import {getRandomInt} from 'sources/utils';
import $ from 'jquery';
import Notify from '../../../../static/js/helpers/Notifier';
@@ -18,18 +18,20 @@ function hasDatabaseInformation(parentData) {
return parentData.database;
}
function generateUrl(trans_id, title, parentData, sqlId) {
let url_endpoint = url_for('datagrid.panel', {
export function generateUrl(trans_id, parentData, sqlId) {
let url_endpoint = url_for('sqleditor.panel', {
'trans_id': trans_id,
});
url_endpoint += `?is_query_tool=${true}`
+`&sgid=${parentData.server_group._id}`
+`&sid=${parentData.server._id}`
+`&server_type=${parentData.server.server_type}`;
+`&sid=${parentData.server._id}`;
if (hasDatabaseInformation(parentData)) {
url_endpoint += `&did=${parentData.database._id}`;
if(parentData.database.label) {
url_endpoint += `&database_name=${parentData.database.label}`;
}
}
if(sqlId) {
@@ -47,7 +49,7 @@ function generateTitle(pgBrowser, aciTreeIdentifier) {
return getPanelTitle(pgBrowser, aciTreeIdentifier);
}
export function showQueryTool(datagrid, pgBrowser, url, aciTreeIdentifier, transId) {
export function showQueryTool(queryToolMod, pgBrowser, url, aciTreeIdentifier, transId) {
const sURL = url || '';
const queryToolTitle = generateTitle(pgBrowser, aciTreeIdentifier);
@@ -66,15 +68,15 @@ export function showQueryTool(datagrid, pgBrowser, url, aciTreeIdentifier, trans
return;
}
const gridUrl = generateUrl(transId, queryToolTitle, parentData);
launchDataGrid(datagrid, transId, gridUrl, queryToolTitle, sURL);
const gridUrl = generateUrl(transId, parentData);
launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, sURL);
}
export function generateScript(parentData, datagrid) {
export function generateScript(parentData, queryToolMod) {
const queryToolTitle = `${parentData.database}/${parentData.user}@${parentData.server}`;
const transId = getRandomInt(1, 9999999);
let url_endpoint = url_for('datagrid.panel', {
let url_endpoint = url_for('sqleditor.panel', {
'trans_id': transId,
});
@@ -82,12 +84,13 @@ export function generateScript(parentData, datagrid) {
+`&sgid=${parentData.sgid}`
+`&sid=${parentData.sid}`
+`&server_type=${parentData.stype}`
+`&did=${parentData.did}`;
+`&did=${parentData.did}`
+`&sql_id=${parentData.sql_id}`;
launchDataGrid(datagrid, transId, url_endpoint, queryToolTitle, '');
launchQueryTool(queryToolMod, transId, url_endpoint, queryToolTitle, '');
}
export function showERDSqlTool(parentData, erdSqlId, queryToolTitle, datagrid) {
export function showERDSqlTool(parentData, erdSqlId, queryToolTitle, queryToolMod) {
const transId = getRandomInt(1, 9999999);
parentData = {
server_group: {
@@ -102,12 +105,12 @@ export function showERDSqlTool(parentData, erdSqlId, queryToolTitle, datagrid) {
},
};
const gridUrl = generateUrl(transId, queryToolTitle, parentData, erdSqlId);
launchDataGrid(datagrid, transId, gridUrl, queryToolTitle, '');
const gridUrl = generateUrl(transId, parentData, erdSqlId);
launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, '');
}
export function launchDataGrid(datagrid, transId, gridUrl, queryToolTitle, sURL) {
let retVal = datagrid.launch_grid(transId, gridUrl, true, queryToolTitle, sURL);
export function launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, sURL) {
let retVal = queryToolMod.launch(transId, gridUrl, true, queryToolTitle, sURL);
if(!retVal) {
Notify.alert(
@@ -119,23 +122,17 @@ export function launchDataGrid(datagrid, transId, gridUrl, queryToolTitle, sURL)
}
}
function setPanelTitle(panel, value) {
if(value) {
$('#' + panel.$title.index() + ' div:first').addClass('wcPanelTab-dynamic');
} else {
$('#' + panel.$title.index() + ' div:first').removeClass('wcPanelTab-dynamic');
}
}
export function _set_dynamic_tab(pgBrowser, value){
var datagrid_panels = pgBrowser.docker.findPanels('frm_datagrid');
datagrid_panels.forEach(panel => {
setPanelTitle(panel, value);
});
var sqleditor_panels = pgBrowser.docker.findPanels('frm_sqleditor');
const process = panel => {
if(value) {
$('#' + panel.$title.index() + ' div:first').addClass('wcPanelTab-dynamic');
} else {
$('#' + panel.$title.index() + ' div:first').removeClass('wcPanelTab-dynamic');
}
};
sqleditor_panels.forEach(process);
var debugger_panels = pgBrowser.docker.findPanels('frm_debugger');
debugger_panels.forEach(panel => {
setPanelTitle(panel, value);
});
debugger_panels.forEach(process);
}

View File

@@ -8,24 +8,24 @@
//////////////////////////////////////////////////////////////
import gettext from '../../../../static/js/gettext';
import url_for from '../../../../static/js/url_for';
import {getDatabaseLabel, generateTitle} from './datagrid_panel_title';
import {getDatabaseLabel, generateTitle} from './sqleditor_title';
import CodeMirror from 'bundled_codemirror';
import * as SqlEditorUtils from 'sources/sqleditor_utils';
import $ from 'jquery';
import _ from 'underscore';
import Notify from '../../../../static/js/helpers/Notifier';
export function showDataGrid(
datagrid,
export function showViewData(
queryToolMod,
pgBrowser,
alertify,
connectionData,
aciTreeIdentifier,
treeIdentifier,
transId,
filter=false,
preferences=null
) {
const node = pgBrowser.tree.findNodeByDomElement(aciTreeIdentifier);
const node = pgBrowser.tree.findNodeByDomElement(treeIdentifier);
if (node === undefined || !node.getData()) {
Notify.alert(
gettext('Data Grid Error'),
@@ -34,7 +34,7 @@ export function showDataGrid(
return;
}
const parentData = pgBrowser.tree.getTreeNodeHierarchy( aciTreeIdentifier
const parentData = pgBrowser.tree.getTreeNodeHierarchy( treeIdentifier
);
if (hasServerOrDatabaseConfiguration(parentData)
@@ -48,30 +48,33 @@ export function showDataGrid(
}
const gridUrl = generateUrl(transId, connectionData, node.getData(), parentData);
const queryToolTitle = generateDatagridTitle(pgBrowser, aciTreeIdentifier);
const queryToolTitle = generateViewDataTitle(pgBrowser, treeIdentifier);
if(filter) {
initFilterDialog(alertify, pgBrowser);
const validateUrl = generateFilterValidateUrl(node.getData(), parentData);
let okCallback = function(sql) {
datagrid.launch_grid(transId, gridUrl, false, queryToolTitle, null, sql);
queryToolMod.launch(transId, gridUrl, false, queryToolTitle, null, sql);
};
$.get(url_for('datagrid.filter'),
$.get(url_for('sqleditor.filter'),
function(data) {
alertify.filterDialog(gettext('Data Filter - %s', queryToolTitle), data, validateUrl, preferences, okCallback)
.resizeTo(pgBrowser.stdW.sm,pgBrowser.stdH.sm);
}
);
} else {
datagrid.launch_grid(transId, gridUrl, false, queryToolTitle);
queryToolMod.launch(transId, gridUrl, false, queryToolTitle);
}
}
export function retrieveNameSpaceName(parentData) {
if (parentData.schema !== undefined) {
if(!parentData) {
return null;
}
else if (parentData.schema !== undefined) {
return parentData.schema.label;
}
else if (parentData.view !== undefined) {
@@ -83,8 +86,24 @@ export function retrieveNameSpaceName(parentData) {
return '';
}
export function retrieveNodeName(parentData) {
if(!parentData) {
return null;
}
else if (parentData.table !== undefined) {
return parentData.table.label;
}
else if (parentData.view !== undefined) {
return parentData.view.label;
}
else if (parentData.catalog !== undefined) {
return parentData.catalog.label;
}
return '';
}
function generateUrl(trans_id, connectionData, nodeData, parentData) {
let url_endpoint = url_for('datagrid.panel', {
let url_endpoint = url_for('sqleditor.panel', {
'trans_id': trans_id,
});
@@ -108,7 +127,7 @@ function generateFilterValidateUrl(nodeData, parentData) {
'obj_id': nodeData._id,
};
return url_for('datagrid.filter_validate', url_params);
return url_for('sqleditor.filter_validate', url_params);
}
function initFilterDialog(alertify, pgBrowser) {
@@ -286,15 +305,15 @@ function hasSchemaOrCatalogOrViewInformation(parentData) {
parentData.catalog !== undefined;
}
export function generateDatagridTitle(pgBrowser, aciTreeIdentifier, custom_title=null, backend_entity=null) {
export function generateViewDataTitle(pgBrowser, treeIdentifier, custom_title=null, backend_entity=null) {
var preferences = pgBrowser.get_preferences_for_module('browser');
const parentData = pgBrowser.tree.getTreeNodeHierarchy(
aciTreeIdentifier
treeIdentifier
);
const namespaceName = retrieveNameSpaceName(parentData);
const db_label = !_.isUndefined(backend_entity) && backend_entity != null && backend_entity.hasOwnProperty('db_name') ? backend_entity['db_name'] : getDatabaseLabel(parentData);
const node = pgBrowser.tree.findNodeByDomElement(aciTreeIdentifier);
const node = pgBrowser.tree.findNodeByDomElement(treeIdentifier);
var dtg_title_placeholder = '';
if(custom_title) {
@@ -310,7 +329,7 @@ export function generateDatagridTitle(pgBrowser, aciTreeIdentifier, custom_title
'server': parentData.server.label,
'schema': namespaceName,
'table': node.getData().label,
'type': 'datagrid',
'type': 'view_data',
};
return generateTitle(dtg_title_placeholder, title_data);
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,9 @@
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
import pgWindow from 'sources/window';
import Notify from '../../../../static/js/helpers/Notifier';
import { retrieveNameSpaceName, retrieveNodeName } from './show_view_data';
const pgAdmin = pgWindow.pgAdmin;
@@ -22,6 +23,25 @@ function isServerInformationAvailable(parentData) {
return parentData.server === undefined;
}
export function getTitle(pgAdmin, browserPref, parentData=null, isConnTitle=false, server=null, database=null, username=null, isQueryTool=true) {
let titleTemplate = isQueryTool ? pgAdmin['qt_default_placeholder'] : pgAdmin['vw_edt_default_placeholder'];
if (!isConnTitle) {
if(!isQueryTool) {
titleTemplate = browserPref['vw_edt_tab_title_placeholder'] ?? pgAdmin['qt_default_placeholder'];
} else {
titleTemplate = browserPref['qt_tab_title_placeholder'] ?? pgAdmin['vw_edt_default_placeholder'];
}
}
return generateTitle(titleTemplate, {
'database': database,
'username': username,
'server': server,
'schema': retrieveNameSpaceName(parentData),
'table': retrieveNodeName(parentData),
'type': isQueryTool ? 'query_tool' : 'view_data',
});
}
export function getPanelTitle(pgBrowser, selected_item=null, custom_title=null, parentData=null, conn_title=false, db_label=null) {
var preferences = pgBrowser.get_preferences_for_module('browser');
if(selected_item == null && parentData == null) {
@@ -57,6 +77,7 @@ export function getPanelTitle(pgBrowser, selected_item=null, custom_title=null,
'server': parentData.server.label,
'type': 'query_tool',
};
return generateTitle(qt_title_placeholder, title_data);
}
@@ -69,7 +90,7 @@ export function setQueryToolDockerTitle(panel, is_query_tool, panel_title, is_fi
panel_tooltip = gettext('File - ') + panel_title;
panel_icon = 'fa fa-file-alt';
}
else if (is_query_tool == 'false' || is_query_tool == false) {
else if (is_query_tool == 'false' || !is_query_tool) {
// Edit grid titles
panel_tooltip = gettext('View/Edit Data - ') + panel_title;
panel_icon = 'pg-font-icon icon-view_data';
@@ -86,33 +107,29 @@ export function setQueryToolDockerTitle(panel, is_query_tool, panel_title, is_fi
export function set_renamable_option(panel, is_file) {
if(is_file || is_file == 'true') {
panel.renamable(false);
panel?.renamable(false);
} else {
panel.renamable(true);
panel?.renamable(true);
}
}
export function generateTitle(title_placeholder, title_data) {
if(title_data.type == 'query_tool') {
title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
title_placeholder = title_placeholder.replace(new RegExp('%USERNAME%'), _.unescape(title_data.username));
title_placeholder = title_placeholder.replace(new RegExp('%SERVER%'), _.unescape(title_data.server));
} else if(title_data.type == 'datagrid') {
title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
title_placeholder = title_placeholder.replace(new RegExp('%USERNAME%'), _.unescape(title_data.username));
title_placeholder = title_placeholder.replace(new RegExp('%SERVER%'), _.unescape(title_data.server));
title_placeholder = title_placeholder.replace(new RegExp('%SCHEMA%'), _.unescape(title_data.schema));
title_placeholder = title_placeholder.replace(new RegExp('%TABLE%'), _.unescape(title_data.table));
if(title_data.type == 'query_tool' || title_data.type == 'psql_tool') {
title_placeholder = title_placeholder.replace('%DATABASE%', _.unescape(title_data.database));
title_placeholder = title_placeholder.replace('%USERNAME%', _.unescape(title_data.username));
title_placeholder = title_placeholder.replace('%SERVER%', _.unescape(title_data.server));
} else if(title_data.type == 'view_data') {
title_placeholder = title_placeholder.replace('%DATABASE%', _.unescape(title_data.database));
title_placeholder = title_placeholder.replace('%USERNAME%', _.unescape(title_data.username));
title_placeholder = title_placeholder.replace('%SERVER%', _.unescape(title_data.server));
title_placeholder = title_placeholder.replace('%SCHEMA%', _.unescape(title_data.schema));
title_placeholder = title_placeholder.replace('%TABLE%', _.unescape(title_data.table));
} else if(title_data.type == 'debugger') {
title_placeholder = title_placeholder.replace(new RegExp('%FUNCTION%'), _.unescape(title_data.function_name));
title_placeholder = title_placeholder.replace(new RegExp('%ARGS%'), _.unescape(title_data.args));
title_placeholder = title_placeholder.replace(new RegExp('%SCHEMA%'), _.unescape(title_data.schema));
title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
} else if(title_data.type == 'psql_tool') {
title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
title_placeholder = title_placeholder.replace(new RegExp('%USERNAME%'), _.unescape(title_data.username));
title_placeholder = title_placeholder.replace(new RegExp('%SERVER%'), _.unescape(title_data.server));
title_placeholder = title_placeholder.replace('%FUNCTION%', _.unescape(title_data.function_name));
title_placeholder = title_placeholder.replace('%ARGS%', _.unescape(title_data.args));
title_placeholder = title_placeholder.replace('%SCHEMA%', _.unescape(title_data.schema));
title_placeholder = title_placeholder.replace('%DATABASE%', _.unescape(title_data.database));
}
return _.escape(title_placeholder);
@@ -122,7 +139,7 @@ export function generateTitle(title_placeholder, title_data) {
* This function is used refresh the db node after showing alert to the user
*/
export function refresh_db_node(message, dbNode) {
Notify.alert()
Alertify.alert()
.setting({
'title': gettext('Database moved/renamed'),
'label':gettext('OK'),

View File

@@ -1,254 +0,0 @@
.query-history {
overflow: auto;
.list-item {
border-bottom: $panel-border;
background-color: $color-bg;
}
.entry {
font-family: $font-family-editor;
border: $panel-border-width solid transparent;
margin-left: 1px;
padding: 0 5px;
.other-info {
@extend .text-12;
color: $text-muted;
font-family: $font-family-editor;
display: flex;
flex-direction: row;
justify-content: space-between;
.timestamp {
align-self: flex-start;
}
}
.query {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
user-select: initial;
.query-history-icon {
width: 18px;
text-align: center;
}
}
}
.date-label {
font-family: $font-family-editor;
background: $color-gray-lighter;
padding: 2px 9px;
font-size: 11px;
font-weight: bold;
color: $color-gray;
border-bottom: $panel-border;
}
.entry.error {
background: $sql-history-error-bg;
}
.selected {
& .entry.error {
background-color: $sql-history-error-bg;
}
& .entry {
color: $sql-history-success-fg;
border: $table-hover-border;
background: $sql-history-success-bg;
font-weight: bold;
.other-info {
color: $sql-history-success-fg;
font-weight: bold;
}
}
}
}
#history-detail-query .CodeMirror {
border: $panel-border;
background-color: $sql-history-detail-bg;
width: 100%;
padding-left: 5px;
position: absolute;
}
.header-label {
@extend .text-12;
display: block;
color: $color-fg;
}
.sql-editor-history-container {
height: 100%;
overflow: hidden;
background-color: $negative-bg;
}
.query-detail {
height: 100%;
width: 100%;
min-height: 19em;
overflow: auto;
display: flex;
flex-direction: column;
background-color: $color-bg;
.error-message-block {
background: $sql-history-error-bg;
flex: 0.3;
padding-left: 20px;
.history-error-text {
@extend .text-12;
padding: 7px 0;
span {
color:$sql-history-error-fg;
font-weight: 500;
margin-right: 8px;
}
}
}
.info-message-block {
background: $sql-history-detail-bg;
flex: 0.3;
padding-left: 20px;
.history-info-text {
@extend .text-12;
padding: 7px 0;
}
}
.metadata-block {
flex: 0.4;
padding: 10px 20px;
.metadata {
display: flex;
flex-wrap: wrap;
.item {
flex: 1;
min-width: 130px;
.value {
@extend .text-14;
display: block;
}
.description {
@extend .header-label;
}
}
}
}
.query-statement-block {
flex: 5;
margin-left: 10px;
margin-right: 10px;
min-height: 4em;
position: relative;
.copy-all, .was-copied, .copy-to-editor {
float: left;
position: relative;
z-index: 10;
border: 1px solid $border-color;
color: $color-fg;
font-size: 12px;
box-shadow: 1px 2px 4px 0px $color-gray-light;
padding: 3px 12px 3px 10px;
font-weight: 500;
min-width: 75px;
}
.copy-all, .copy-to-editor {
background-color: $color-bg;
}
.was-copied {
background-color: $color-primary-light;
border-color: $color-primary-light;
color: $btn-copied-color-fg;
}
.CodeMirror-scroll {
padding-top: 25px;
}
}
.block-divider {
margin-top: 11px;
margin-bottom: 8px;
}
.message-block {
flex: 2;
display: flex;
padding: 0 20px;
min-height: 6em;
.message {
flex: 2 2 0%;
flex-direction: column;
display: flex;
.message-header {
@extend .header-label;
@extend .not-selectable;
flex: 0 0 auto;
}
.content {
flex: 0 1 auto;
overflow: auto;
position: relative;
height: 100%;
.content-value {
@extend .bg-white;
@extend .text-13;
font-family: $font-family-editor;
color: $color-fg;
border: 0;
padding-left: 0;
position: absolute;
}
}
}
}
}
#history_grid {
.gutter.gutter-horizontal {
width: $panel-border-width;
background: $panel-border-color;
&:hover {
cursor: ew-resize;
}
}
.toggle-and-history-container {
display: flex;
flex-direction: column;
height: 100%;
.query-history-toggle {
padding-top: 4px;
padding-bottom: 4px;
}
}
}

View File

@@ -1,431 +0,0 @@
.filter-title {
background-color: $color-primary;
padding: 2px;
color: $color-primary-fg;
font-size: 13px;
}
.sql-icon-lg {
font-size: 0.875rem;
line-height: 1.3;
}
.sql_textarea {
height: 100%;
}
.sql_textarea .CodeMirror-scroll {
z-index: 0;
}
.data-output-container {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
overflow: hidden;
}
.sql-editor-busy-text-status {
position: absolute;
padding: 1rem 1.5rem;
bottom: 0;
right: 0;
opacity: 0.8;
background-color: $color-warning-light;
color: $color-warning-fg;
}
.connection_status {
background-color: $sql-title-bg;
color: $sql-title-fg;
border-right: $border-width solid $border-color;
}
.editor-title {
padding: $sql-title-padding;
background: $sql-title-bg;
color: $sql-title-fg;
}
.connection-info {
background: $sql-title-bg;
color: $sql-title-fg;
width:100%;
display: inherit;
}
.conn-info-dd {
padding-top: 0.3em;
padding-left: 0.2em;
cursor: pointer;
}
.connection-data {
display: inherit;
cursor: pointer;
width: auto;
}
#editor-panel {
z-index: 0;
position: absolute;
top: $sql-editor-panel-top;
bottom: 0;
left: 0;
right: 0;
}
.ajs-body .warn-icon {
color: $color-warning;
font-size: 2em;
margin-right: 20px;
padding-top: 10px;
}
.connection_status_wrapper {
width: 100%;
border-top: $panel-border;
border-bottom: $panel-border;
}
li.CodeMirror-hint-active {
background: $color-primary-light;
color: $color-primary-fg;
}
.sql-editor .CodeMirror-activeline-background {
background: $color-editor-activeline-light !important;
border: 1px solid $color-editor-activeline-border-color;
}
.filter-container {
position: relative;
background-color: $color-bg;
border: 1px solid $border-color;
padding-bottom: 30px;
top: 10px;
z-index: 1;
margin: auto;
width: 60%;
}
.limit-enabled {
background-color: $color-bg;
}
.CodeMirror-hint {
margin: 0;
padding: 0 4px;
border-radius: 2px;
overflow: hidden;
white-space: pre;
color: $color-fg;
cursor: pointer;
}
.grid-header .ui-icon.ui-state-hover {
background-color: $color-bg;
}
.slick-cell.selected span[data-cell-type="row-header-selector"] {
color: $color-primary-fg;
}
.slick-cell.cell-move-handle {
font-weight: bold;
text-align: right;
border-right: solid $border-color;
background: $color-gray-lighter;
cursor: move;
}
.cell-move-handle:hover {
background: $color-gray-light;
}
.slick-row.selected .cell-move-handle {
background: $color-warning-light;
}
.slick-row.complete {
background-color: $color-success-light;
color: $color-gray-dark;
}
.cell-selection {
border-right-color: $border-color;
border-right-style: solid;
background: $color-gray-lighter;
color: $color-gray;
text-align: right;
font-size: 10px;
}
#datagrid .slick-header .slick-header-columns {
background: $sql-grid-title-cell-bg;
color: $sql-grid-title-cell-fg;
height: 40px;
border-bottom: $panel-border;
}
#datagrid .slick-header .slick-header-column.ui-state-default {
padding: 4px 0 3px 6px;
border-bottom: $panel-border;
border-right: $panel-border;
}
.slick-row:hover .slick-cell{
border-top: $table-hover-border;
border-bottom: $table-hover-border;
background-color: $table-hover-bg-color;
}
#datagrid .slick-header .slick-header-column.selected {
background-color: $color-primary;
}
.slick-row .slick-cell {
border-bottom: $panel-border;
border-right: $panel-border;
z-index: 0;
}
#datagrid {
background: none;
background-color: $datagrid-bg;
}
.ui-widget-content.slick-row {
&.even, &.odd {
background: none;
background-color: $table-bg;
}
}
/* Remove active cell border */
.slick-cell.active {
border: 1px solid transparent;
border-right: 1px solid $color-gray-light;
border-bottom-color: $color-gray-light;
}
/* To highlight all newly inserted rows */
.grid-canvas .new_row {
background: $color-success-light !important;
}
/* To highlight all the updated rows */
.grid-canvas .updated_row {
background: $color-gray-lighter;
}
/* To highlight row at fault */
.grid-canvas .new_row.error, .grid-canvas .updated_row.error {
background: $color-danger-light !important;
}
/* Disabled row */
.grid-canvas .disabled_row {
background: $color-gray-lighter;
}
/* Disabled cell */
.grid-canvas .disabled_cell {
color: $text-muted;
}
/* Highlighted (modified or new) cell */
.grid-canvas .highlighted_grid_cells {
background: $color-gray-lighter;
font-weight: bold;
}
/* Override selected row color */
#datagrid .slick-row .slick-cell.selected {
background-color: $table-bg-selected;
color: $datagrid-selected-color;
}
/* color the first column */
#datagrid .slick-row {
.slick-cell {
background-color: $sql-grid-data-cell-bg;
color: $sql-grid-data-cell-fg;
}
.slick-cell.l0.r0 {
background-color: $sql-grid-title-cell-bg;
color: $sql-grid-title-cell-fg;
}
}
#datagrid div.slick-header.ui-state-default {
background-color: $sql-grid-title-cell-bg;
color: $sql-grid-title-cell-fg;
border-bottom: none;
border-right: none;
border-top: none;
}
#datagrid .slick-row .slick-cell.l0.r0.selected {
background-color: $color-primary;
color: $color-primary-fg;
}
#datagrid .slick-row > .slick-cell:not(.l0):not(.r0).selected {
background-color: $table-hover-bg-color;
border-top: $table-hover-border;
border-bottom: $table-hover-border;
}
.pg-text-editor {
z-index:10000;
position:absolute;
background: $color-bg;
padding: 0.25rem;
border: $panel-border;
box-shadow: $dropdown-box-shadow;
& .pg-textarea {
width:250px;
height:80px;
border:0;
outline:0;
resize: both;
}
& .pg-text-invalid {
background: $color-danger-lighter;
}
& #pg-json-editor {
min-width:525px;
min-height:300px;
height:295px;
width:550px;
border: $panel-border;
outline:0;
resize: both;
overflow:auto
}
}
.pg_buttons {
padding-top: 3px;
}
.sql-editor-message {
white-space:pre-wrap;
font-family: $font-family-editor;
padding-top: 5px;
padding-left: 10px;
overflow: auto;
height: 100%;
font-size: 0.925em;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
.view-geometry-property-table .td-disabled{
color: $color-gray-lighter;
}
/* For leaflet map background */
.geometry-viewer-container-plain-background {
background: $color-bg;
}
div.strikeout:before {
content: " ";
position: absolute;
top: 50%;
left: 0;
border-top: 1px solid $color-danger;
width: 100%;
font-weight: 900;
}
div.strikeout:after {
content: "\00B7";
font-size: 1px;
font-weight: 900;
}
.sql-scratch {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow-y: hidden;
textarea {
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
resize: none;
}
}
.connection_status .obtaining-conn {
background-image: $loader-icon-small !important;
background-position: center center;
background-repeat: no-repeat;
&:before {
content:'';
}
min-width: 50%;
min-height: 100%;
}
.sql-editor-grid-container {
height: 100%;
overflow: auto;
.ui-widget-content {
background-color: $input-bg;
color: $input-color;
}
.ui-state-default {
color: $color-fg;
}
}
.sql-editor-grid-container.has-no-footer {
height: 100%;
}
.selected-connection {
background-color: $color-primary-light;
}
/* Setting it to hardcoded white as the SVG generated is having white bg
* Need to check what can be done.
*/
/* Css for psql */
.psql_terminal .terminal {
padding-top: 1%;
padding-left: 0.5%;
height: 100%;
}
.psql-icon-style {
font-size: inherit;
padding-left: 0em;
}
.psql-tab-style {
font-size: small;
padding-left: 0em;
}

View File

@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block title %}{{title}}{% endblock %}
{% block css_link %}
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
{% endblock %}
{% block body %}
<style>
body {padding: 0px;}
#sqleditor-container {
display: flex;
flex-direction: column;
height: 100%;
}
#sqleditor-container:not(:empty) + .pg-sp-container {
display: none;
}
{% if is_desktop_mode and is_linux %}
.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform: none;}
.alertify-notifier{-webkit-transform: none;}
.alertify-notifier .ajs-message{-webkit-transform: none;}
.alertify .ajs-dialog.ajs-shake{-webkit-animation-name: none;}
.sql-editor-busy-icon.fa-pulse{-webkit-animation: none;}
{% endif %}
</style>
<div id="sqleditor-container">
<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block init_script %}
try {
require(
['sources/generated/browser_nodes', 'sources/generated/codemirror'],
function() {
require(['sources/generated/sqleditor'], function(module) {
window.pgAdmin.Tools.SQLEditor.loadComponent(
document.getElementById('sqleditor-container'), {{ params|safe }});
if(window.opener) {
$(window).on('unload', function(ev) {
$.ajax({
method: 'DELETE',
url: '{{close_url}}'
});
});
} else {
$(window).on('beforeunload', function(ev) {
$.ajax({
method: 'DELETE',
url: '{{close_url}}'
});
});
}
}, function() {
console.log(arguments);
});
},
function() {
console.log(arguments);
});
} catch (err) {
console.log(err);
}
{% endblock %}

View File

@@ -27,7 +27,7 @@ class TestDownloadCSV(BaseTestGenerator):
'Download csv URL with valid query',
dict(
sql='SELECT 1 as "A",2 as "B",3 as "C"',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}',
init_url='/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns='"A","B","C"',
output_values='1,2,3',
@@ -41,7 +41,7 @@ class TestDownloadCSV(BaseTestGenerator):
'Download csv URL with wrong TX id',
dict(
sql='SELECT 1 as "A",2 as "B",3 as "C"',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}',
init_url='/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns=None,
output_values=None,
@@ -55,7 +55,7 @@ class TestDownloadCSV(BaseTestGenerator):
'Download csv URL with wrong query',
dict(
sql='SELECT * FROM this_table_does_not_exist',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}',
init_url='/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns=None,
output_values=None,
@@ -69,7 +69,7 @@ class TestDownloadCSV(BaseTestGenerator):
'Download as txt without filename parameter',
dict(
sql='SELECT 1 as "A",2 as "B",3 as "C"',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}',
init_url='/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns='"A";"B";"C"',
output_values='1;2;3',
@@ -83,7 +83,7 @@ class TestDownloadCSV(BaseTestGenerator):
'Download as csv without filename parameter',
dict(
sql='SELECT 1 as "A",2 as "B",3 as "C"',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}',
init_url='/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns='"A","B","C"',
output_values='1,2,3',
@@ -211,7 +211,7 @@ class TestDownloadCSV(BaseTestGenerator):
self.assertEqual(response.status_code, 500)
# Close query tool
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -70,7 +70,7 @@ class TestEditorHistory(BaseTestGenerator):
# Initialize query tool
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
@@ -101,7 +101,7 @@ class TestEditorHistory(BaseTestGenerator):
def tearDown(self):
# Close query tool
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -262,7 +262,7 @@ class TestEncodingCharset(BaseTestGenerator):
# Initialize query tool
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'\
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'\
.format(self.trans_id, test_utils.SERVER_GROUP, self.encode_sid,
self.encode_did)
response = self.tester.post(url)
@@ -283,7 +283,7 @@ class TestEncodingCharset(BaseTestGenerator):
self.assertEqual(result, self.test_str)
# Close query tool
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -34,7 +34,7 @@ class TestExplainPlan(BaseTestGenerator):
# Initialize query tool
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
@@ -68,7 +68,7 @@ class TestExplainPlan(BaseTestGenerator):
self.assertEqual(len(response_data['data']['result'][0]), 1)
# Close query tool
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -106,7 +106,7 @@ class TestMacros(BaseTestGenerator):
# Initialize query tool
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
@@ -150,7 +150,7 @@ class TestMacros(BaseTestGenerator):
def tearDown(self):
# Close query tool
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -77,7 +77,7 @@ NOTICE: Hello, world!
# Initialize query tool
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
@@ -110,7 +110,7 @@ NOTICE: Hello, world!
cnt += 1
# Close query tool
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -304,7 +304,7 @@ class TestTransactionControl(BaseTestGenerator):
def _initialize_query_tool(self):
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
@@ -339,6 +339,6 @@ class TestTransactionControl(BaseTestGenerator):
utils.create_table_with_query(self.server, self.db_name, create_sql)
def _close_query_tool(self):
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -97,7 +97,7 @@ class TestViewData(BaseTestGenerator):
# Initialize query tool
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/datagrid/{0}/3/table/{1}/{2}/{3}/{4}' \
url = '/sqleditor/initialize/viewdata/{0}/3/table/{1}/{2}/{3}/{4}' \
.format(self.trans_id, test_utils.SERVER_GROUP, self.server_id,
self.db_id, table_id)

View File

@@ -76,7 +76,8 @@ def get_user_macros():
macros = db.session.query(UserMacros.name,
Macros.id,
Macros.alt, Macros.control,
Macros.key, Macros.key_code
Macros.key, Macros.key_code,
UserMacros.sql
).outerjoin(
Macros, UserMacros.mid == Macros.id).filter(
UserMacros.uid == current_user.id).order_by(UserMacros.name).all()
@@ -87,7 +88,8 @@ def get_user_macros():
key_label = 'Ctrl + ' + m[4] if m[3] is True else 'Alt + ' + m[4]
data.append({'name': m[0], 'id': m[1], 'key': m[4],
'key_label': key_label, 'alt': 1 if m[2] else 0,
'control': 1 if m[3] else 0, 'key_code': m[5]})
'control': 1 if m[3] else 0, 'key_code': m[5],
'sql': m[6]})
return data
@@ -121,7 +123,8 @@ def set_macros():
return make_json_response(
status=410, success=0, errormsg=msg
)
return ajax_response(status=200)
return get_macros(None, True)
def create_macro(macro):

View File

@@ -1,6 +1,7 @@
from pgadmin.utils.ajax import make_json_response
from pgadmin.model import db, QueryHistoryModel
from config import MAX_QUERY_HIST_STORED
import json
class QueryHistory:
@@ -107,30 +108,34 @@ class QueryHistory:
)
@staticmethod
def clear_history(uid, sid, dbname=None):
def clear_history(uid, sid, dbname=None, filter=None):
try:
filters = [
QueryHistoryModel.uid == uid,
QueryHistoryModel.sid == sid
]
if dbname is not None:
db.session.query(QueryHistoryModel) \
.filter(QueryHistoryModel.uid == uid,
QueryHistoryModel.sid == sid,
QueryHistoryModel.dbname == dbname) \
.delete()
filters.append(QueryHistoryModel.dbname == dbname)
db.session.commit()
history = db.session.query(QueryHistoryModel) \
.filter(*filters)
if filter is not None:
for row in history:
query_info = json.loads(row.query_info.decode())
if query_info['query'] == filter['query'] and \
query_info['start_time'] == filter['start_time']:
db.session.delete(row)
else:
db.session.query(QueryHistoryModel) \
.filter(QueryHistoryModel.uid == uid,
QueryHistoryModel.sid == sid)\
.delete()
history.delete()
db.session.commit()
db.session.commit()
except Exception:
db.session.rollback()
# do not affect query execution if history clear fails
@staticmethod
def clear(uid, sid, dbname=None):
QueryHistory.clear_history(uid, sid, dbname)
def clear(uid, sid, dbname=None, filter=None):
QueryHistory.clear_history(uid, sid, dbname, filter)
return make_json_response(
data={
'status': True,

View File

@@ -18,22 +18,6 @@ from pgadmin.utils import SHORTCUT_FIELDS as shortcut_fields, \
def register_query_tool_preferences(self):
self.info_notifier_timeout = self.preference.register(
'display', 'info_notifier_timeout',
gettext("Query info notifier timeout"), 'integer', 5,
category_label=PREF_LABEL_DISPLAY,
min_val=-1,
max_val=999999,
help_str=gettext(
'The length of time to display the query info notifier after '
'execution has completed. A value of -1 disables the notifier'
' and a value of 0 displays it until clicked. Values greater'
' than 0 display the notifier for the number of seconds'
' specified.'
)
)
self.explain_verbose = self.preference.register(
'Explain', 'explain_verbose',
gettext("Verbose output?"), 'boolean', False,
@@ -128,7 +112,7 @@ def register_query_tool_preferences(self):
)
)
self.show_prompt_commit_transaction = self.preference.register(
self.copy_sql_to_query_tool = self.preference.register(
'Options', 'copy_sql_to_query_tool',
gettext("Copy SQL from main window to query tool?"), 'boolean',
False,
@@ -411,6 +395,24 @@ def register_query_tool_preferences(self):
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'clear_query',
gettext('Clear query'),
'keyboardshortcut',
{
'alt': True,
'shift': False,
'control': True,
'key': {
'key_code': 76,
'char': 'L'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'download_results',
@@ -614,19 +616,6 @@ def register_query_tool_preferences(self):
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts', 'btn_clear_options',
gettext('Accesskey (Clear editor options)'), 'keyboardshortcut',
{
'key': {
'key_code': 76,
'char': 'l'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts', 'btn_conn_status',
gettext('Accesskey (Connection status)'), 'keyboardshortcut',

View File

@@ -107,8 +107,6 @@ class StartRunningQuery:
data={
'status': status, 'result': result,
'can_edit': can_edit, 'can_filter': can_filter,
'info_notifier_timeout':
self.blueprint_object.info_notifier_timeout.get() * 1000,
'notifies': notifies,
'transaction_status': trans_status,
}

View File

@@ -203,7 +203,7 @@ class TestQueryUpdatableResultset(BaseTestGenerator):
def _initialize_query_tool(self):
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
@@ -235,7 +235,7 @@ class TestQueryUpdatableResultset(BaseTestGenerator):
utils.create_table_with_query(self.server, self.db_name, create_sql)
def _close_query_tool(self):
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -921,7 +921,7 @@ class TestSaveChangedData(BaseTestGenerator):
def _initialize_query_tool(self):
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
url = '/sqleditor/initialize/sqleditor/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
@@ -955,6 +955,6 @@ class TestSaveChangedData(BaseTestGenerator):
utils.create_table_with_query(self.server, self.db_name, create_sql)
def _close_query_tool(self):
url = '/datagrid/close/{0}'.format(self.trans_id)
url = '/sqleditor/close/{0}'.format(self.trans_id)
response = self.tester.delete(url)
self.assertEqual(response.status_code, 200)

View File

@@ -112,7 +112,6 @@ class StartRunningQueryTest(BaseTestGenerator):
'not found.',
can_edit=False,
can_filter=False,
info_notifier_timeout=5000,
notifies=None,
transaction_status=None
)
@@ -273,7 +272,6 @@ class StartRunningQueryTest(BaseTestGenerator):
result='async function result output',
can_edit=True,
can_filter=True,
info_notifier_timeout=5000,
notifies=None,
transaction_status=None
)
@@ -318,7 +316,6 @@ class StartRunningQueryTest(BaseTestGenerator):
result='async function result output',
can_edit=True,
can_filter=True,
info_notifier_timeout=5000,
notifies=None,
transaction_status=None
)
@@ -363,7 +360,6 @@ class StartRunningQueryTest(BaseTestGenerator):
result='async function result output',
can_edit=True,
can_filter=True,
info_notifier_timeout=5000,
notifies=None,
transaction_status=None
)
@@ -409,7 +405,6 @@ class StartRunningQueryTest(BaseTestGenerator):
result='async function result output',
can_edit=True,
can_filter=True,
info_notifier_timeout=5000,
notifies=None,
transaction_status=None
)
@@ -444,8 +439,7 @@ class StartRunningQueryTest(BaseTestGenerator):
if self.expect_internal_server_error_called_with is not None:
internal_server_error_mock.return_value = expected_response
pickle_mock.loads.return_value = self.pickle_load_return
blueprint_mock = MagicMock(
info_notifier_timeout=MagicMock(get=lambda: 5))
blueprint_mock = MagicMock()
# Save value for the later use
self.is_begin_required_for_sql_query = \