mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
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:
committed by
Akshay Joshi
parent
bf8e569bde
commit
b5b9ee46a1
@@ -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)
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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 %};">
|
||||
</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 %}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 |
424
web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js
Normal file
424
web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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]),
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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='© <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={
|
||||
'© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
|
||||
+ ' © <a href="http://viewfinderpanoramas.org" target="_blank">SRTM</a>,'
|
||||
+ ' © <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={
|
||||
'© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
|
||||
+ ' © <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={
|
||||
'© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
|
||||
+ ' © <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={
|
||||
'© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
|
||||
+ ' © <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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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}
|
||||
/>;
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
26
web/pgadmin/tools/sqleditor/static/js/index.js
Normal file
26
web/pgadmin/tools/sqleditor/static/js/index.js
Normal 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,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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'),
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
70
web/pgadmin/tools/sqleditor/templates/sqleditor/index.html
Normal file
70
web/pgadmin/tools/sqleditor/templates/sqleditor/index.html
Normal 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 %}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = \
|
||||
|
||||
Reference in New Issue
Block a user