Added support to launch PSQL for the connected database server. Fixes #2341
parent
37dece2cd8
commit
3ddf941cd7
|
@ -16,3 +16,4 @@ PL/SQL code.
|
|||
editgrid
|
||||
schema_diff
|
||||
erd_tool
|
||||
psql_tool
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
Binary file not shown.
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 100 KiB |
Binary file not shown.
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
@ -128,6 +128,8 @@ Use the *Tools* menu to access the following options (in alphabetical order):
|
|||
+---------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Search Objects...* | Click to open the :ref:`Search Objects... <search_objects>` and start searching any kind of objects in a database. |
|
||||
+---------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| *PSQL Tool* | Click to open the :ref:`PSQL Tool <psql_tool>` and start PSQL in the current database context. |
|
||||
+---------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
The Help Menu
|
||||
*************
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
.. _psql_tool:
|
||||
|
||||
******************
|
||||
`PSQL Tool`:index:
|
||||
******************
|
||||
|
||||
PSQL tool allows user to connect to PostgreSQL/EDB Advanced server using psql terminal.
|
||||
|
||||
* Open PSQL Tool from Tools menu or PSQL tool button from browser tree or from context menu.
|
||||
|
||||
* PSQL will connect to the current connected database from browser tree.
|
||||
|
||||
.. image:: images/psql_tool.png
|
||||
:alt: PSQL tool window
|
||||
:align: center
|
||||
|
||||
You can open multiple instance of the PSQL tool in individual tabs simultaneously.
|
||||
To close the PSQL tool, click the *X* in the upper-right hand corner of the tab bar.
|
||||
|
|
@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
|
|||
New features
|
||||
************
|
||||
|
||||
| `Issue #2341 <https://redmine.postgresql.org/issues/2341>`_ - Added support to launch PSQL for the connected database server.
|
||||
| `Issue #4064 <https://redmine.postgresql.org/issues/4064>`_ - Added window maximize/restore functionality for properties dialog.
|
||||
|
||||
Housekeeping
|
||||
|
|
|
@ -20,4 +20,6 @@ the selected browser node.
|
|||
* Use the :ref:`Filtered Rows <viewdata_filter>` button to access the Data Filter popup
|
||||
to apply a filter to a set of data for viewing/editing.
|
||||
* Use the :ref:`Search objects <search_objects>` button to access the search objects
|
||||
dialog. It helps you search any database object.
|
||||
dialog. It helps you search any database object.
|
||||
* Use the :ref:`PSQL Tool <psql_tool>` button to open the PSQL in the current
|
||||
database context.
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
#
|
||||
###############################################################################
|
||||
|
||||
cheroot==8.*
|
||||
Flask==1.*
|
||||
Flask-Gravatar==0.*
|
||||
Flask-Login==0.*
|
||||
|
@ -37,3 +36,5 @@ sshtunnel==0.*
|
|||
ldap3==2.*
|
||||
Flask-BabelEx==0.*
|
||||
gssapi==1.6.*
|
||||
flask-socketio>=5.0.1
|
||||
eventlet==0.30.2
|
||||
|
|
|
@ -156,8 +156,8 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
|
|||
# such as JavaScript, CSS, or pretty much anything that the browser loads.
|
||||
# see https://content-security-policy.com/#source_list for more info
|
||||
# e.g. "default-src https: data: 'unsafe-inline' 'unsafe-eval';"
|
||||
CONTENT_SECURITY_POLICY = "default-src http: data: blob: 'unsafe-inline' " \
|
||||
"'unsafe-eval';"
|
||||
CONTENT_SECURITY_POLICY = "default-src ws: http: data: blob: 'unsafe-inline'" \
|
||||
" 'unsafe-eval';"
|
||||
|
||||
# STRICT_TRANSPORT_SECURITY_ENABLED when set to True will set the
|
||||
# Strict-Transport-Security header
|
||||
|
@ -636,6 +636,21 @@ KRB_AUTO_CREATE_USER = True
|
|||
|
||||
KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache')
|
||||
|
||||
##########################################################################
|
||||
# PSQL tool settings
|
||||
##########################################################################
|
||||
# This will enable PSQL tool in pgAdmin. So user can execute the commands
|
||||
# using PSQL terminal in pgAdmin.
|
||||
ENABLE_PSQL = True
|
||||
|
||||
# ALLOW_PSQL_SHELL_COMMAND = True will disable the execution of os level
|
||||
# commands using meta command \! from PSQL terminal.
|
||||
# As PSQL allow user to execute the os level commands from the PSQL terminal
|
||||
# user can execute any system level command as per the system login user
|
||||
# privileges. Default this setting is set to False but if it set to True
|
||||
# User will able to execute the system level commands through PSQL terminal
|
||||
# in pgAdmin.
|
||||
ALLOW_PSQL_SHELL_COMMANDS = False
|
||||
|
||||
##########################################################################
|
||||
# Local config settings
|
||||
|
|
|
@ -117,13 +117,18 @@
|
|||
"shim-loader": "^1.0.1",
|
||||
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.16",
|
||||
"snapsvg-cjs": "^0.0.6",
|
||||
"socket.io-client": "^4.0.0",
|
||||
"split.js": "^1.5.10",
|
||||
"tablesorter": "^2.31.2",
|
||||
"tempusdominus-bootstrap-4": "^5.1.2",
|
||||
"tempusdominus-core": "^5.0.3",
|
||||
"underscore": "^1.13.1",
|
||||
"webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker/#89e006611f4d0fc24b0a098fa2041821d093be4f",
|
||||
"wkx": "^0.5.0"
|
||||
"wkx": "^0.5.0",
|
||||
"xterm": "^4.11.0",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"xterm-addon-search": "^0.8.0",
|
||||
"xterm-addon-web-links": "^0.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js --ext .jsx .",
|
||||
|
|
|
@ -13,7 +13,7 @@ to start a web server."""
|
|||
|
||||
|
||||
import sys
|
||||
from cheroot.wsgi import Server as CherootServer
|
||||
|
||||
|
||||
if sys.version_info < (3, 4):
|
||||
raise RuntimeError('This application must be run under Python 3.4 '
|
||||
|
@ -37,7 +37,7 @@ else:
|
|||
builtins.SERVER_MODE = None
|
||||
|
||||
import config
|
||||
from pgadmin import create_app
|
||||
from pgadmin import create_app, socketio
|
||||
from pgadmin.utils import u_encode, fs_encoding, file_quote
|
||||
from pgadmin.utils.constants import INTERNAL
|
||||
# Get the config database schema version. We store this in pgadmin.model
|
||||
|
@ -97,6 +97,8 @@ if not os.path.isfile(config.SQLITE_PATH):
|
|||
##########################################################################
|
||||
app = create_app()
|
||||
app.debug = False
|
||||
app.config['sessions'] = dict()
|
||||
|
||||
if config.SERVER_MODE:
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
|
||||
|
@ -206,17 +208,16 @@ def main():
|
|||
else:
|
||||
# Can use cheroot instead of flask dev server when not in debug
|
||||
# 10 is default thread count in CherootServer
|
||||
num_threads = 10 if config.THREADED_MODE else 1
|
||||
prod_server = CherootServer(
|
||||
(config.DEFAULT_SERVER, config.EFFECTIVE_SERVER_PORT),
|
||||
wsgi_app=app,
|
||||
numthreads=num_threads,
|
||||
server_name=config.APP_NAME)
|
||||
# num_threads = 10 if config.THREADED_MODE else 1
|
||||
try:
|
||||
print("Using production server...")
|
||||
prod_server.start()
|
||||
socketio.run(
|
||||
app,
|
||||
host=config.DEFAULT_SERVER,
|
||||
port=config.EFFECTIVE_SERVER_PORT,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
prod_server.stop()
|
||||
print("CLOSE SERVER")
|
||||
socketio.stop()
|
||||
|
||||
except IOError:
|
||||
app.logger.error("Error starting the app server: %s", sys.exc_info())
|
||||
|
|
|
@ -19,6 +19,7 @@ from collections import defaultdict
|
|||
from importlib import import_module
|
||||
|
||||
from flask import Flask, abort, request, current_app, session, url_for
|
||||
from flask_socketio import SocketIO
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from flask_babelex import Babel, gettext
|
||||
from flask_babelex import gettext as _
|
||||
|
@ -52,10 +53,15 @@ import mimetypes
|
|||
mimetypes.add_type('application/javascript', '.js')
|
||||
mimetypes.add_type('text/css', '.css')
|
||||
|
||||
|
||||
winreg = None
|
||||
if os.name == 'nt':
|
||||
import winreg
|
||||
|
||||
socketio = SocketIO(manage_session=False, async_mode='eventlet',
|
||||
logger=False, engineio_logger=False, debug=False,
|
||||
ping_interval=25, ping_timeout=120)
|
||||
|
||||
|
||||
class PgAdmin(Flask):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -811,4 +817,5 @@ def create_app(app_name=None):
|
|||
##########################################################################
|
||||
# All done!
|
||||
##########################################################################
|
||||
socketio.init_app(app)
|
||||
return app
|
||||
|
|
|
@ -10,6 +10,7 @@ from flask_babelex import gettext
|
|||
from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\
|
||||
PREF_LABEL_KEYBOARD_SHORTCUTS, PREF_LABEL_TABS_SETTINGS, \
|
||||
PREF_LABEL_OPTIONS
|
||||
from flask_security import current_user
|
||||
import config
|
||||
|
||||
LOCK_LAYOUT_LEVEL = {
|
||||
|
@ -511,10 +512,12 @@ def register_browser_preferences(self):
|
|||
options=[{'label': gettext('Query Tool'), 'value': 'qt'},
|
||||
{'label': gettext('Debugger'), 'value': 'debugger'},
|
||||
{'label': gettext('Schema Diff'), 'value': 'schema_diff'},
|
||||
{'label': gettext('ERD Tool'), 'value': 'erd_tool'}],
|
||||
help_str=gettext('Select Query Tool, Debugger, or Schema Diff from '
|
||||
'the drop-down to set open in new browser tab for '
|
||||
'that particular module.'),
|
||||
{'label': gettext('ERD Tool'), 'value': 'erd_tool'},
|
||||
{'label': gettext('PSQL Tool'), 'value': 'psql_tool'}],
|
||||
help_str=gettext('Select Query Tool, Debugger, Schema Diff, ERD Tool '
|
||||
'or PSQL Tool from the drop-down to set '
|
||||
'open in new browser tab for that particular module.'
|
||||
),
|
||||
select2={
|
||||
'multiple': True, 'allowClear': False,
|
||||
'tags': True, 'first_empty': False,
|
||||
|
@ -523,3 +526,16 @@ def register_browser_preferences(self):
|
|||
'placeholder': gettext('Select open new tab...')
|
||||
}
|
||||
)
|
||||
|
||||
self.psql_tab_title = self.preference.register(
|
||||
'tab_settings', 'psql_tab_title_placeholder',
|
||||
gettext("PSQL tool tab title"),
|
||||
'text', '%DATABASE%/%USERNAME%@%SERVER%',
|
||||
category_label=PREF_LABEL_DISPLAY,
|
||||
help_str=gettext(
|
||||
'Supported placeholders are %DATABASE%, %USERNAME%, and %SERVER%. '
|
||||
'Users can provide any string with or without placeholders of'
|
||||
' their choice. The blank title will be revert back to the'
|
||||
' default title with placeholders.'
|
||||
)
|
||||
)
|
||||
|
|
|
@ -34,6 +34,7 @@ from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
|
|||
SERVER_CONNECTION_CLOSED
|
||||
from sqlalchemy import or_
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from .... import socketio as sio
|
||||
|
||||
|
||||
def has_any(data, keys):
|
||||
|
@ -1499,6 +1500,13 @@ class ServerNode(PGChildNodeView):
|
|||
|
||||
# Release Connection
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
# Check if any psql terminal is running for the current disconnecting
|
||||
# server. If any terminate the psql tool connection.
|
||||
if 'sid_soid_mapping' in current_app.config and str(sid) in \
|
||||
current_app.config['sid_soid_mapping']:
|
||||
if str(sid) in current_app.config['sid_soid_mapping']:
|
||||
for i in current_app.config['sid_soid_mapping'][str(sid)]:
|
||||
sio.emit('disconnect-psql', namespace='/pty', to=i)
|
||||
|
||||
status = manager.release()
|
||||
|
||||
|
|
|
@ -122,6 +122,9 @@ define('pgadmin.node.database', [
|
|||
is_connected: function(node) {
|
||||
return (node && node.connected == true && node.canDisconn == true);
|
||||
},
|
||||
is_psql_enabled: function(node) {
|
||||
return (node && node.connected == true) && pgAdmin['enable_psql'];
|
||||
},
|
||||
is_conn_allow: function(node) {
|
||||
return (node && node.allowConn == true);
|
||||
},
|
||||
|
|
|
@ -101,7 +101,8 @@ define('pgadmin.node.server', [
|
|||
icon: 'fa fa-unlink', enable : 'is_connected',data: {
|
||||
data_disabled: gettext('Database is already disconnected.'),
|
||||
},
|
||||
},{
|
||||
},
|
||||
{
|
||||
name: 'reload_configuration', node: 'server', module: this,
|
||||
applies: ['tools', 'context'], callback: 'reload_configuration',
|
||||
category: 'reload', priority: 6, label: gettext('Reload Configuration'),
|
||||
|
@ -728,6 +729,14 @@ define('pgadmin.node.server', [
|
|||
|
||||
return false;
|
||||
},
|
||||
/* Open psql tool for server*/
|
||||
server_psql_tool: function(args) {
|
||||
var input = args || {},
|
||||
t = pgBrowser.tree,
|
||||
i = input.item || t.selected(),
|
||||
d = i && i.length == 1 ? t.itemData(i) : undefined;
|
||||
pgBrowser.psql.psql_tool(d, i, true);
|
||||
}
|
||||
},
|
||||
model: pgAdmin.Browser.Node.Model.extend({
|
||||
defaults: {
|
||||
|
|
|
@ -64,6 +64,14 @@ define([
|
|||
priority: 997, label: gettext('Search Objects...'),
|
||||
icon: 'fa fa-search',
|
||||
}]);
|
||||
|
||||
// show psql tool same as query tool.
|
||||
pgAdmin.Browser.add_menus([{
|
||||
name: 'show_psql_tool', node: this.type, module: this,
|
||||
applies: ['context'], callback: 'show_psql_tool',
|
||||
priority: 998, label: gettext('PSQL Tool (Beta)'),
|
||||
icon: 'fas fa-terminal',
|
||||
}]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -498,6 +506,13 @@ define([
|
|||
pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
|
||||
}
|
||||
},
|
||||
show_psql_tool: function(args) {
|
||||
var input = args || {},
|
||||
t = pgBrowser.tree,
|
||||
i = input.item || t.selected(),
|
||||
d = i && i.length == 1 ? t.itemData(i) : undefined;
|
||||
pgBrowser.psql.psql_tool(d, i, true);
|
||||
},
|
||||
});
|
||||
|
||||
return pgBrowser.Collection;
|
||||
|
|
|
@ -209,6 +209,16 @@ define('pgadmin.browser.node', [
|
|||
priority: 997, label: gettext('Search Objects...'),
|
||||
icon: 'fa fa-search', enable: enable,
|
||||
}]);
|
||||
|
||||
if(pgAdmin['enable_psql']) {
|
||||
// show psql tool same as query tool.
|
||||
pgAdmin.Browser.add_menus([{
|
||||
name: 'show_psql_tool', node: this.type, module: this,
|
||||
applies: ['context'], callback: 'show_psql_tool',
|
||||
priority: 998, label: gettext('PSQL Tool (Beta)'),
|
||||
icon: 'fas fa-terminal',
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
// This will add options of scripts eg:'CREATE Script'
|
||||
|
@ -903,6 +913,15 @@ define('pgadmin.browser.node', [
|
|||
pgAdmin.DataGrid.show_query_tool('', i);
|
||||
},
|
||||
|
||||
// Callback to render psql tool.
|
||||
show_psql_tool: function(args) {
|
||||
var input = args || {},
|
||||
t = pgBrowser.tree,
|
||||
i = input.item || t.selected(),
|
||||
d = i && i.length == 1 ? t.itemData(i) : undefined;
|
||||
pgBrowser.psql.psql_tool(d, i, true);
|
||||
},
|
||||
|
||||
// Logic to change the server background colour
|
||||
// There is no way of applying CSS to parent element so we have to
|
||||
// do it via JS code only
|
||||
|
|
|
@ -56,9 +56,23 @@ let _defaultToolBarButtons = [
|
|||
toggleClass: '',
|
||||
parentClass: 'pg-toolbar-btn btn-primary-icon',
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
if(pgAdmin['enable_psql']) {
|
||||
_defaultToolBarButtons.push({
|
||||
label: gettext('PSQL Tool'),
|
||||
ariaLabel: gettext('PSQL Tool'),
|
||||
btnClass: 'fas fa-terminal',
|
||||
text: '',
|
||||
toggled: false,
|
||||
toggleClass: '',
|
||||
parentClass: 'pg-toolbar-btn btn-primary-icon pg-toolbar-psql',
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Place holder for non default tool bar buttons.
|
||||
let _otherToolbarButtons = [];
|
||||
|
||||
|
@ -105,6 +119,13 @@ export function initializeToolbar(panel, wcDocker) {
|
|||
pgAdmin.DataGrid.show_filtered_row({mnuid: 4}, pgAdmin.Browser.tree.selected());
|
||||
else if ('name' in data && data.name === gettext('Search objects'))
|
||||
pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
|
||||
else if ('name' in data && data.name === gettext('PSQL Tool')){
|
||||
var input = {},
|
||||
t = pgAdmin.Browser.tree,
|
||||
i = input.item || t.selected(),
|
||||
d = i && i.length == 1 ? t.itemData(i) : undefined;
|
||||
pgAdmin.Browser.psql.psql_tool(d, i, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -53,3 +53,9 @@ samp,
|
|||
border-width: 1px;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
.pg-toolbar-psql {
|
||||
padding-top: 0em;
|
||||
font-size: inherit;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -52,6 +52,10 @@ define('pgadmin.browser.utils',
|
|||
pgAdmin['user_inactivity_timeout'] = {{ current_app.config.get('USER_INACTIVITY_TIMEOUT') }};
|
||||
pgAdmin['override_user_inactivity_timeout'] = '{{ current_app.config.get('OVERRIDE_USER_INACTIVITY_TIMEOUT') }}' == 'True';
|
||||
|
||||
/* GET PSQL Tool related config */
|
||||
pgAdmin['enable_psql'] = '{{ current_app.config.get('ENABLE_PSQL') }}' == 'True';
|
||||
pgAdmin['allow_psql_shell_commands'] = '{{ current_app.config.get('ALLOW_PSQL_SHELL_COMMANDS') }}' == 'True';
|
||||
|
||||
// Define list of nodes on which Query tool option doesn't appears
|
||||
var unsupported_nodes = pgAdmin.unsupported_nodes = [
|
||||
'server_group', 'server', 'coll-tablespace', 'tablespace',
|
||||
|
|
|
@ -62,7 +62,8 @@ def underscore_unescape(text):
|
|||
">": '>',
|
||||
""": '"',
|
||||
"`": '`',
|
||||
"'": "'"
|
||||
"'": "'",
|
||||
"'": "'"
|
||||
}
|
||||
|
||||
# always replace & first
|
||||
|
|
|
@ -11,6 +11,7 @@ define('bundled_browser',[
|
|||
'pgadmin.browser',
|
||||
'sources/browser/index',
|
||||
'top/tools/erd/static/js/index',
|
||||
'top/tools/psql/static/js/index',
|
||||
], function(pgBrowser) {
|
||||
pgBrowser.init();
|
||||
});
|
||||
|
|
|
@ -21,3 +21,5 @@
|
|||
|
||||
@import '../vendor/backgrid/backgrid.css';
|
||||
@import '../vendor/backgrid/backgrid-select-all.css';
|
||||
|
||||
@import 'node_modules/xterm/css/xterm.css';
|
||||
|
|
|
@ -10,6 +10,11 @@ $theme-colors: (
|
|||
--color-fg: #{$color-fg};
|
||||
--color-bg: #{$color-bg};
|
||||
--border-color: #{$border-color};
|
||||
--psql-background: #{$psql-background};
|
||||
--psql-foreground: #{$psql-foreground};
|
||||
--psql-cursor: #{$psql-cursor};
|
||||
--psql-cursorAccent: #{$psql-cursorAccent};
|
||||
--psql-selection: #{$psql-selection};
|
||||
}
|
||||
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
|
|
|
@ -371,3 +371,10 @@ $erd-link-selected-color: $color-fg !default;
|
|||
$erd-bg-grid: url("data:image/svg+xml, %3Csvg width='100%25' viewBox='0 0 45 45' style='background-color:#{url-friendly-colour($erd-canvas-bg)}' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='smallGrid' width='15' height='15' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 15 0 L 0 0 0 15' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='0.5'/%3E%3C/pattern%3E%3Cpattern id='grid' width='45' height='45' patternUnits='userSpaceOnUse'%3E%3Crect width='100' height='100' fill='url(%23smallGrid)'/%3E%3Cpath d='M 100 0 L 0 0 0 100' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='1'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)' /%3E%3C/svg%3E%0A");
|
||||
|
||||
$select2-readonly: $color-gray-lighter !default;
|
||||
|
||||
// psql tool variables
|
||||
$psql-background: $color-bg !default;
|
||||
$psql-foreground: $color-fg !default;
|
||||
$psql-cursor: $color-fg !default;
|
||||
$psql-cursorAccent: $color-fg !default;
|
||||
$psql-selection: #326690 !default;
|
||||
|
|
|
@ -136,3 +136,10 @@ $erd-link-color: $color-fg;
|
|||
$erd-link-selected-color: $color-fg;
|
||||
|
||||
$select2-readonly: $color-bg;
|
||||
|
||||
// psql tool variables
|
||||
$psql-background: $color-bg;
|
||||
$psql-foreground: $color-fg;
|
||||
$psql-cursor: $color-fg;
|
||||
$psql-cursorAccent: $color-fg;
|
||||
$psql-selection: #d6effc;
|
||||
|
|
|
@ -207,3 +207,10 @@ $quick-search-a-text-color: $black !default;
|
|||
$quick-search-info-icon: #8A8A8A !default;
|
||||
|
||||
$select2-readonly: $color-gray;
|
||||
|
||||
// psql tool variables
|
||||
$psql-background: $color-bg;
|
||||
$psql-foreground: $color-fg;
|
||||
$psql-cursor: $color-fg;
|
||||
$psql-cursorAccent: $color-fg;
|
||||
$psql-selection: $color-primary-light;
|
||||
|
|
|
@ -100,6 +100,10 @@ export function generateTitle(title_placeholder, title_data) {
|
|||
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));
|
||||
}
|
||||
|
||||
return _.escape(title_placeholder);
|
||||
|
|
|
@ -0,0 +1,686 @@
|
|||
#!/usr/bin/env python3
|
||||
import fcntl
|
||||
import os
|
||||
import pty
|
||||
import re
|
||||
import select
|
||||
import struct
|
||||
import termios
|
||||
import config
|
||||
import eventlet.green.subprocess as subprocess
|
||||
|
||||
from config import PG_DEFAULT_DRIVER
|
||||
from flask import Response, url_for, request
|
||||
from flask import render_template, copy_current_request_context, \
|
||||
current_app as app
|
||||
from flask_babelex import gettext
|
||||
from flask_security import login_required, current_user
|
||||
from pgadmin.browser.utils import underscore_unescape
|
||||
from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from ... import socketio as sio
|
||||
from pgadmin.utils import get_complete_file_path
|
||||
|
||||
|
||||
session_input = dict()
|
||||
session_input_cursor = dict()
|
||||
session_last_cmd = dict()
|
||||
pdata = dict()
|
||||
cdata = dict()
|
||||
|
||||
|
||||
class PSQLModule(PgAdminModule):
|
||||
"""
|
||||
class PSQLModule(PgAdminModule)
|
||||
A module class for PSQL derived from PgAdminModule.
|
||||
"""
|
||||
|
||||
LABEL = gettext("PSQL")
|
||||
|
||||
def get_own_menuitems(self):
|
||||
return {}
|
||||
|
||||
def get_own_javascripts(self):
|
||||
return [{
|
||||
'name': 'pgadmin.psql',
|
||||
'path': url_for('psql.index') + "psql",
|
||||
'when': None
|
||||
}]
|
||||
|
||||
def get_panels(self):
|
||||
return []
|
||||
|
||||
def get_exposed_url_endpoints(self):
|
||||
"""
|
||||
Returns:
|
||||
list: URL endpoints for PSQL module
|
||||
"""
|
||||
return [
|
||||
'psql.panel'
|
||||
]
|
||||
|
||||
|
||||
blueprint = PSQLModule('psql', __name__, static_url_path='/static')
|
||||
|
||||
|
||||
@blueprint.route("/psql.js")
|
||||
@login_required
|
||||
def script():
|
||||
"""render the required javascript"""
|
||||
return Response(
|
||||
response=render_template("psql/js/psql.js", _=gettext),
|
||||
status=200,
|
||||
mimetype=MIMETYPE_APP_JS
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route('/panel/<int:trans_id>',
|
||||
methods=["POST"],
|
||||
endpoint="panel")
|
||||
@login_required
|
||||
def panel(trans_id):
|
||||
"""
|
||||
Return panel template for PSQL tools.
|
||||
:param trans_id:
|
||||
"""
|
||||
params = {
|
||||
'trans_id': trans_id,
|
||||
'title': request.form['title']
|
||||
}
|
||||
if 'sid_soid_mapping' not in app.config:
|
||||
app.config['sid_soid_mapping'] = dict()
|
||||
if request.args:
|
||||
params.update({k: v for k, v in request.args.items()})
|
||||
# Set TERM env for xterm.
|
||||
os.environ['TERM'] = 'xterm'
|
||||
|
||||
return render_template('editor_template.html',
|
||||
sid=params['sid'],
|
||||
db=underscore_unescape(params['db']) if params[
|
||||
'db'] else 'postgres',
|
||||
server_type=params['server_type'],
|
||||
is_enable=config.ENABLE_PSQL,
|
||||
title=underscore_unescape(params['title']),
|
||||
theme=params['theme']
|
||||
)
|
||||
|
||||
|
||||
def set_term_size(fd, row, col, xpix=0, ypix=0):
|
||||
"""
|
||||
Set the terminal size as per UI xterm size.
|
||||
:param fd:
|
||||
:param row:
|
||||
:param col:
|
||||
:param xpix:
|
||||
:param ypix:
|
||||
"""
|
||||
term_size = struct.pack('HHHH', row, col, xpix, ypix)
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, term_size)
|
||||
|
||||
|
||||
@sio.on('connect', namespace='/pty')
|
||||
def connect():
|
||||
"""
|
||||
Connect to the server through socket.
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
if config.ENABLE_PSQL:
|
||||
sio.emit('connected', {'sid': request.sid}, namespace='/pty',
|
||||
to=request.sid)
|
||||
|
||||
if request.sid in session_last_cmd:
|
||||
session_last_cmd[request.sid]['is_new_connection'] = False
|
||||
else:
|
||||
session_last_cmd[request.sid] = {'cmd': '', 'arrow_up': False,
|
||||
'invalid_cmd': False,
|
||||
'is_new_connection': False}
|
||||
else:
|
||||
sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
|
||||
to=request.sid)
|
||||
|
||||
|
||||
def create_pty_terminal(connection_data):
|
||||
# Create the pty terminal process, parent and fd are file descriptors
|
||||
# for parent and child.
|
||||
parent, fd = pty.openpty()
|
||||
p = None
|
||||
if parent is not None:
|
||||
# Child process
|
||||
p = subprocess.Popen(connection_data,
|
||||
preexec_fn=os.setsid,
|
||||
stdin=fd,
|
||||
stdout=fd,
|
||||
stderr=fd,
|
||||
universal_newlines=True
|
||||
)
|
||||
|
||||
app.config['sessions'][request.sid] = parent
|
||||
pdata[request.sid] = p
|
||||
cdata[request.sid] = fd
|
||||
else:
|
||||
app.config['sessions'][request.sid] = parent
|
||||
cdata[request.sid] = fd
|
||||
set_term_size(fd, 50, 50)
|
||||
|
||||
return p, parent, fd
|
||||
|
||||
|
||||
def read_terminal_data(parent, data_ready, max_read_bytes, sid):
|
||||
"""
|
||||
Read the terminal output.
|
||||
:param parent:
|
||||
:param data_ready:
|
||||
:param max_read_bytes:
|
||||
:param sid:
|
||||
:return:
|
||||
"""
|
||||
if parent in data_ready:
|
||||
# Read the output from parent fd (terminal).
|
||||
output = os.read(parent, max_read_bytes)
|
||||
emit_output = True
|
||||
|
||||
if sid in session_last_cmd and session_last_cmd[sid][
|
||||
'arrow_up'] and not session_last_cmd[request.sid][
|
||||
'arrow_left_right']:
|
||||
session_last_cmd[sid]['cmd'] = output.decode()
|
||||
session_input_cursor[request.sid] = len(
|
||||
session_last_cmd[sid]['cmd'])
|
||||
session_last_cmd[sid]['arrow_up'] = True
|
||||
|
||||
if sid in session_last_cmd and session_last_cmd[sid]['invalid_cmd']:
|
||||
# If command is invalid then emit error to user.
|
||||
emit_output = False
|
||||
sio.emit(
|
||||
'pty-output',
|
||||
{
|
||||
'result': gettext(
|
||||
"ERROR: Shell commands are disabled "
|
||||
"in psql for security\r\n"),
|
||||
'error': True
|
||||
},
|
||||
namespace='/pty', room=sid)
|
||||
# If command is valid then emit output to user.
|
||||
if emit_output:
|
||||
sio.emit('pty-output',
|
||||
{'result': output.decode(),
|
||||
'error': False},
|
||||
namespace='/pty', room=sid)
|
||||
else:
|
||||
session_last_cmd[request.sid]['invalid_cmd'] = False
|
||||
|
||||
|
||||
@sio.on('start_process', namespace='/pty')
|
||||
def start_process(data):
|
||||
"""
|
||||
Start the pty terminal and execute psql command and emit results to user.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
@copy_current_request_context
|
||||
def read_and_forward_pty_output(sid, data):
|
||||
max_read_bytes = 1024 * 20
|
||||
|
||||
p, parent, fd = create_pty_terminal(connection_data)
|
||||
|
||||
while p and p.poll() is None:
|
||||
if request.sid in app.config['sessions']:
|
||||
# This code is added to make this unit testable.
|
||||
if "is_test" not in data:
|
||||
sio.sleep(0.01)
|
||||
else:
|
||||
data['count'] += 1
|
||||
if data['count'] == 5:
|
||||
break
|
||||
|
||||
timeout = 0
|
||||
# module provides access to platform-specific I/O
|
||||
# monitoring functions
|
||||
(data_ready, _, _) = select.select([parent, fd], [], [],
|
||||
timeout)
|
||||
|
||||
read_terminal_data(parent, data_ready, max_read_bytes, sid)
|
||||
|
||||
# Check user is authenticated and PSQL is enabled in config.
|
||||
if current_user.is_authenticated and config.ENABLE_PSQL:
|
||||
connection_data = []
|
||||
try:
|
||||
db = ''
|
||||
if data['db']:
|
||||
db = underscore_unescape(data['db']).replace('\\', "\\\\")
|
||||
|
||||
conn, manager = _get_connection(int(data['sid']), data)
|
||||
psql_utility = manager.utility('sql')
|
||||
connection_data = get_connection_str(psql_utility, db,
|
||||
manager)
|
||||
except Exception as e:
|
||||
# If any error raised during the start the PSQL emit error to UI.
|
||||
# request.sid: This sid is socket id.
|
||||
sio.emit(
|
||||
'conn_error',
|
||||
{
|
||||
'error': 'Error while running psql command: {0}'.format(e),
|
||||
}, namespace='/pty', room=request.sid)
|
||||
|
||||
try:
|
||||
if str(data['sid']) not in app.config['sid_soid_mapping']:
|
||||
# request.sid: refer request.sid as socket id.
|
||||
app.config['sid_soid_mapping'][str(data['sid'])] = list()
|
||||
app.config['sid_soid_mapping'][str(data['sid'])].append(
|
||||
request.sid)
|
||||
else:
|
||||
app.config['sid_soid_mapping'][str(data['sid'])].append(
|
||||
request.sid)
|
||||
|
||||
sio.start_background_task(read_and_forward_pty_output,
|
||||
request.sid, data)
|
||||
except Exception as e:
|
||||
sio.emit(
|
||||
'conn_error',
|
||||
{
|
||||
'error': 'Error while running psql command: {0}'.format(e),
|
||||
}, namespace='/pty', room=request.sid)
|
||||
else:
|
||||
# Show error if user is not authenticated.
|
||||
sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
|
||||
to=request.sid)
|
||||
|
||||
|
||||
def _get_connection(sid, data):
|
||||
"""
|
||||
Get the connection object of ERD.
|
||||
:param sid:
|
||||
:param did:
|
||||
:param trans_id:
|
||||
:return:
|
||||
"""
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
try:
|
||||
conn = manager.connection()
|
||||
# This is added for unit test only, no use in normal execution.
|
||||
if 'pwd' in data:
|
||||
kwargs = {'password': data['pwd'], "user": data['user']}
|
||||
status, msg = conn.connect(**kwargs)
|
||||
else:
|
||||
status, msg = conn.connect()
|
||||
if not status:
|
||||
app.logger.error(msg)
|
||||
sio.emit(sio.emit(
|
||||
'conn_error',
|
||||
{
|
||||
'error': 'Error while running psql command: {0}'
|
||||
''.format('Server connection not present.'),
|
||||
}, namespace='/pty', room=request.sid))
|
||||
raise RuntimeError('Server is not connected.')
|
||||
|
||||
return conn, manager
|
||||
except Exception as e:
|
||||
app.logger.error(e)
|
||||
raise
|
||||
|
||||
|
||||
def get_connection_str(psql_utility, db, manager):
|
||||
"""
|
||||
Get connection string(through connection dsn)
|
||||
:param psql_utility: PostgreSQL binary path.
|
||||
:param db: database name to connect specific db.
|
||||
:return: connection attribute list for PSQL connection.
|
||||
"""
|
||||
conn_attr = get_conn_str(manager, db)
|
||||
conn_attr_list = list()
|
||||
conn_attr_list.append(psql_utility)
|
||||
conn_attr_list.append(conn_attr)
|
||||
return conn_attr_list
|
||||
|
||||
|
||||
def get_conn_str(manager, db):
|
||||
"""
|
||||
Get connection attributes for psql connection.
|
||||
:param manager:
|
||||
:param db:
|
||||
:return:
|
||||
"""
|
||||
manager.export_password_env('PGPASSWORD')
|
||||
conn_attr =\
|
||||
"host={0} port={1} dbname={2} user={3} sslmode={4} " \
|
||||
"sslcompression={5} " \
|
||||
"".format(
|
||||
manager.local_bind_host if manager.use_ssh_tunnel else
|
||||
manager.host,
|
||||
manager.local_bind_port if manager.use_ssh_tunnel else
|
||||
manager.port,
|
||||
db if db != '' else 'postgres',
|
||||
manager.user if manager.user else 'postgres',
|
||||
manager.ssl_mode,
|
||||
True if manager.sslcompression else False,
|
||||
)
|
||||
|
||||
if manager.hostaddr:
|
||||
conn_attr = " {0} hostaddr={1}".format(conn_attr, manager.hostaddr)
|
||||
|
||||
if manager.passfile:
|
||||
conn_attr = " {0} passfile={1}".format(conn_attr,
|
||||
get_complete_file_path(
|
||||
manager.passfile))
|
||||
|
||||
if get_complete_file_path(manager.sslcert):
|
||||
conn_attr = " {0} sslcert={1}".format(
|
||||
conn_attr, get_complete_file_path(manager.sslcert))
|
||||
|
||||
if get_complete_file_path(manager.sslkey):
|
||||
conn_attr = " {0} sslkey={1}".format(
|
||||
conn_attr, get_complete_file_path(manager.sslkey))
|
||||
|
||||
if get_complete_file_path(manager.sslrootcert):
|
||||
conn_attr = " {0} sslrootcert={1}".format(
|
||||
conn_attr, get_complete_file_path(manager.sslrootcert))
|
||||
|
||||
if get_complete_file_path(manager.sslcrl):
|
||||
conn_attr = " {0} sslcrl={1}".format(
|
||||
conn_attr, get_complete_file_path(manager.sslcrl))
|
||||
|
||||
if manager.service:
|
||||
conn_attr = " {0} service={1}".format(
|
||||
conn_attr, get_complete_file_path(manager.service))
|
||||
|
||||
return conn_attr
|
||||
|
||||
|
||||
def check_last_exe_cmd(data):
|
||||
"""
|
||||
Check the is user try to execute last executed command.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
# If user get previous executed command from history then set
|
||||
# current command as previous executed command.
|
||||
if session_last_cmd[request.sid]['cmd'] and session_last_cmd[request.sid][
|
||||
'arrow_up']:
|
||||
user_input = str(
|
||||
session_last_cmd[request.sid]['cmd']).strip()
|
||||
session_last_cmd[request.sid]['arrow_up'] = False
|
||||
session_last_cmd[request.sid]['cmd'] = ''
|
||||
else:
|
||||
if request.sid not in session_input:
|
||||
session_input[request.sid] = data['input']
|
||||
user_input = str(session_input[request.sid]).strip()
|
||||
else:
|
||||
user_input = str(session_input[request.sid]).strip()
|
||||
|
||||
return user_input
|
||||
|
||||
|
||||
def invalid_cmd():
|
||||
"""
|
||||
Invalid command
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
session_last_cmd[request.sid]['invalid_cmd'] = True
|
||||
|
||||
for i in range(len(session_input[request.sid])):
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
'\b \b'.encode())
|
||||
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
'\n'.encode())
|
||||
session_input[request.sid] = ''
|
||||
|
||||
|
||||
def check_valid_cmd(user_input):
|
||||
"""
|
||||
Check if user entered a valid cmd and \\! command is preset as a string
|
||||
only in current executing command. if \\! is present as command don't
|
||||
allow the execution of command.
|
||||
:param user_input:
|
||||
:return:
|
||||
"""
|
||||
stop_execution = True
|
||||
# Check \! is passed as string or not.
|
||||
double_quote_strs = re.findall('"([^"]*)"', user_input)
|
||||
if not double_quote_strs:
|
||||
double_quote_strs = re.findall("'([^']*)'", user_input)
|
||||
|
||||
if double_quote_strs:
|
||||
for sub_str in double_quote_strs:
|
||||
if re.search("\\\!", sub_str):
|
||||
stop_execution = False
|
||||
# break
|
||||
|
||||
if stop_execution:
|
||||
session_last_cmd[request.sid]['invalid_cmd'] = True
|
||||
# Remove already added command from terminal.
|
||||
for i in range(len(user_input)):
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
'\b \b'.encode())
|
||||
# Add Enter event to execute the command.
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
'\n'.encode())
|
||||
else:
|
||||
session_last_cmd[request.sid]['invalid_cmd'] = False
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
'\n'.encode())
|
||||
|
||||
|
||||
def enter_key_press(data):
|
||||
"""
|
||||
Handel the Enter key press event.
|
||||
:param data:
|
||||
"""
|
||||
user_input = check_last_exe_cmd(data)
|
||||
session_input_cursor[request.sid] = 0
|
||||
|
||||
# If ALLOW_PSQL_SHELL_COMMANDS is False then user can't execute
|
||||
# \! meta command to run shell commands through PSQL terminal.
|
||||
# Check before executing the user entered command does not
|
||||
# contains \! in input.
|
||||
is_new_connection = session_last_cmd[request.sid][
|
||||
'is_new_connection']
|
||||
if user_input.startswith('\\!') and re.match("^\\\!$", user_input) and len(
|
||||
user_input) == 2 and not config.ALLOW_PSQL_SHELL_COMMANDS \
|
||||
and not is_new_connection:
|
||||
invalid_cmd()
|
||||
elif re.search("\\\!", user_input) and \
|
||||
not config.ALLOW_PSQL_SHELL_COMMANDS and\
|
||||
not session_last_cmd[request.sid]['is_new_connection']:
|
||||
check_valid_cmd(user_input)
|
||||
elif user_input == '\q' or user_input == 'q\\q':
|
||||
# If user enter \q to terminate the PSQL, emit the msg to
|
||||
# notify user connection is terminated.
|
||||
sio.emit('pty-output',
|
||||
{
|
||||
'result': gettext(
|
||||
'Connection terminated, To create new '
|
||||
'connection please open another psql'
|
||||
' tool.'),
|
||||
'error': True},
|
||||
namespace='/pty', room=request.sid)
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
'\n'.encode())
|
||||
|
||||
else:
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
data['input'].encode())
|
||||
session_input[request.sid] = ''
|
||||
session_last_cmd[request.sid]['is_new_connection'] = False
|
||||
|
||||
|
||||
def backspace_key_press():
|
||||
"""
|
||||
Handel the backspace key press event.
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
session_last_cmd[request.sid]['arrow_left_right'] = True
|
||||
|
||||
if session_last_cmd[request.sid]['cmd']:
|
||||
session_input[request.sid] = \
|
||||
session_last_cmd[request.sid]['cmd']
|
||||
|
||||
user_input = list(session_input[request.sid])
|
||||
|
||||
if session_input_cursor[request.sid] == 1:
|
||||
index = 0
|
||||
session_input_cursor[request.sid] -= 1
|
||||
else:
|
||||
if session_input_cursor[request.sid] > 0:
|
||||
index = (session_input_cursor[request.sid]) - 1
|
||||
session_input_cursor[request.sid] -= 1
|
||||
else:
|
||||
index = session_input_cursor[request.sid]
|
||||
session_input_cursor[request.sid] = 0
|
||||
|
||||
if len(user_input):
|
||||
del user_input[index]
|
||||
session_input[request.sid] = "".join(user_input)
|
||||
|
||||
if len(session_input[request.sid]) == 0:
|
||||
session_input_cursor[request.sid] = 0
|
||||
session_last_cmd[request.sid]['cmd'] = ''
|
||||
|
||||
|
||||
def set_user_input(data):
|
||||
"""
|
||||
Check and set current input as user input in session_input.
|
||||
:param data:
|
||||
"""
|
||||
if session_last_cmd[request.sid]['cmd'] and \
|
||||
session_input[request.sid] == '':
|
||||
session_input[request.sid] = \
|
||||
session_last_cmd[request.sid]['cmd']
|
||||
session_input_cursor[request.sid] = len(
|
||||
session_input[request.sid])
|
||||
else:
|
||||
session_last_cmd[request.sid]['arrow_up'] = False
|
||||
session_last_cmd[request.sid]['cmd'] = ''
|
||||
user_input = list(session_input[request.sid])
|
||||
user_input.insert(session_input_cursor[request.sid],
|
||||
data['input'])
|
||||
session_input[request.sid] = ''.join(user_input)
|
||||
session_input_cursor[request.sid] += 1
|
||||
session_last_cmd[request.sid]['arrow_left_right'] = False
|
||||
|
||||
|
||||
def other_key_press(data):
|
||||
"""
|
||||
Handel the other key press from psql tool.
|
||||
:param data:
|
||||
:type data:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
if data['key_name'] == 'ArrowLeft':
|
||||
session_last_cmd[request.sid]['arrow_left_right'] = True
|
||||
if session_input_cursor[request.sid] > 0:
|
||||
session_input_cursor[request.sid] -= 1
|
||||
|
||||
elif data['key_name'] == 'ArrowRight':
|
||||
session_last_cmd[request.sid]['arrow_left_right'] = True
|
||||
if session_input_cursor[request.sid] < len(
|
||||
session_input[request.sid]):
|
||||
session_input_cursor[request.sid] += 1
|
||||
|
||||
elif data['key_name'] == 'ArrowUp':
|
||||
session_last_cmd[request.sid]['arrow_up'] = True
|
||||
session_last_cmd[request.sid]['arrow_left_right'] = False
|
||||
session_input[request.sid] = session_last_cmd[request.sid][
|
||||
'cmd']
|
||||
session_input_cursor[request.sid] = len(
|
||||
session_last_cmd[request.sid]['cmd'])
|
||||
|
||||
elif request.sid in session_input and \
|
||||
data['key_name'] == 'Backspace' and \
|
||||
(len(session_input[request.sid]) or
|
||||
len(session_last_cmd[request.sid])):
|
||||
backspace_key_press()
|
||||
elif request.sid in session_input:
|
||||
set_user_input(data)
|
||||
else:
|
||||
session_input_cursor[request.sid] = 0
|
||||
session_input[request.sid] = data['input']
|
||||
session_input_cursor[request.sid] += 1
|
||||
|
||||
# Write user input to terminal parent fd.
|
||||
os.write(app.config['sessions'][request.sid],
|
||||
data['input'].encode())
|
||||
|
||||
|
||||
@sio.on('socket_input', namespace='/pty')
|
||||
def socket_input(data):
|
||||
"""
|
||||
This get the user input through socket.
|
||||
:param data: User input from socket.
|
||||
"""
|
||||
try:
|
||||
# request.sid: refer request.sid as socket id.
|
||||
# Check PSQL enabled setting from config.
|
||||
enable_psql = True if config.ENABLE_PSQL else False
|
||||
|
||||
if request.sid in app.config['sessions']:
|
||||
if data['key_name'] == 'Enter' and enable_psql:
|
||||
enter_key_press(data)
|
||||
else:
|
||||
other_key_press(data)
|
||||
except Exception as e:
|
||||
# Delete socket id from sessions.
|
||||
# request.sid: refer request.sid as socket id.
|
||||
sio.emit('pty-output',
|
||||
{
|
||||
'result': gettext('Invalid session.\r\n'),
|
||||
'error': True
|
||||
},
|
||||
namespace='/pty', room=request.sid)
|
||||
del app.config['sessions'][request.sid]
|
||||
|
||||
|
||||
@sio.on('resize', namespace='/pty')
|
||||
def resize(data):
|
||||
"""
|
||||
Resize the pty terminal as per the UI terminal.
|
||||
:param data: UI terminal rows and cols data
|
||||
"""
|
||||
# request.sid: refer request.sid as socket id.
|
||||
if request.sid in app.config['sessions']:
|
||||
set_term_size(app.config['sessions'][request.sid], data['rows'],
|
||||
data['cols'])
|
||||
|
||||
|
||||
@sio.on('disconnect', namespace='/pty')
|
||||
def disconnect():
|
||||
"""
|
||||
Disconnect the socket and terminate the process
|
||||
"""
|
||||
# request.sid: refer request.sid as socket id.
|
||||
if request.sid in pdata:
|
||||
# On disconnect socket manually exit the psql terminal and close the
|
||||
# parend and child fd then kill the subprocess.
|
||||
disconnect_socket()
|
||||
|
||||
|
||||
@sio.on('server-disconnect', namespace='/pty')
|
||||
def server_disconnect(data):
|
||||
"""
|
||||
Disconnect the socket and terminate the process after user disconnect
|
||||
the server. we can't use disconnect event name as it is reserved for socket
|
||||
internal use.
|
||||
"""
|
||||
# request.sid: refer request.sid as socket id.
|
||||
if request.sid in pdata and request.sid in app.config['sid_soid_mapping'][
|
||||
data['sid']]:
|
||||
# On disconnect socket manually exit the psql terminal and close the
|
||||
# parend and child fd then kill the subprocess.
|
||||
app.config['sid_soid_mapping'][data['sid']] = [soid for soid in
|
||||
app.config[
|
||||
'sid_soid_mapping'][
|
||||
data['sid']] if
|
||||
soid != request.sid]
|
||||
disconnect_socket()
|
||||
|
||||
|
||||
def disconnect_socket():
|
||||
os.write(app.config['sessions'][request.sid], '\q\n'.encode())
|
||||
sio.sleep(1)
|
||||
os.close(app.config['sessions'][request.sid])
|
||||
os.close(cdata[request.sid])
|
||||
del app.config['sessions'][request.sid]
|
|
@ -0,0 +1,23 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import pgBrowser from 'top/browser/static/js/browser';
|
||||
import * as csrfToken from 'sources/csrf';
|
||||
import {initialize} from './psql_module';
|
||||
|
||||
let pgBrowserOut = initialize(gettext, url_for, $, _, pgAdmin, csrfToken, pgBrowser);
|
||||
|
||||
module.exports = {
|
||||
pgBrowser: pgBrowserOut,
|
||||
};
|
|
@ -0,0 +1,429 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
import { SearchAddon } from 'xterm-addon-search';
|
||||
import { io } from 'socketio';
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import {enable} from 'pgadmin.browser.toolbar';
|
||||
import clipboard from 'sources/selection/clipboard';
|
||||
import 'wcdocker';
|
||||
import {getRandomInt} from 'sources/utils';
|
||||
|
||||
import {getTreeNodeHierarchyFromIdentifier} from 'sources/tree/pgadmin_tree_node';
|
||||
import {generateTitle} from 'tools/datagrid/static/js/datagrid_panel_title';
|
||||
|
||||
|
||||
export function setPanelTitle(psqlToolPanel, panelTitle) {
|
||||
psqlToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
|
||||
}
|
||||
|
||||
var wcDocker = window.wcDocker;
|
||||
|
||||
export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser) {
|
||||
var pgBrowser = Browser;
|
||||
var terminal = Terminal;
|
||||
var parentData = null;
|
||||
/* Return back, this has been called more than once */
|
||||
if (pgBrowser.psql)
|
||||
return pgBrowser.psql;
|
||||
|
||||
|
||||
// Create an Object Restore of pgBrowser class
|
||||
pgBrowser.psql = {
|
||||
init: function() {
|
||||
this.initialized = true;
|
||||
csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
|
||||
// Define the nodes on which the menus to be appear
|
||||
var menus = [{
|
||||
name: 'psql',
|
||||
module: this,
|
||||
applies: ['tools'],
|
||||
callback: 'psql_tool',
|
||||
priority: 1,
|
||||
label: gettext('PSQL Tool (Beta)'),
|
||||
enable: this.psqlToolEnabled,
|
||||
}];
|
||||
|
||||
this.enable_psql_tool = pgAdmin['enable_psql'];
|
||||
if(pgAdmin['enable_psql']) {
|
||||
pgBrowser.add_menus(menus);
|
||||
}
|
||||
|
||||
// Creating a new pgBrowser frame to show the data.
|
||||
var psqlFrameType = new pgBrowser.Frame({
|
||||
name: 'frm_psqltool',
|
||||
showTitle: true,
|
||||
isCloseable: true,
|
||||
isPrivate: true,
|
||||
url: 'about:blank',
|
||||
});
|
||||
|
||||
var 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('psql');
|
||||
clearInterval(cacheIntervalId);
|
||||
}
|
||||
},0);
|
||||
|
||||
pgBrowser.onPreferencesChange('psql', function() {
|
||||
self.preferences = pgBrowser.get_preferences_for_module('psql');
|
||||
});
|
||||
|
||||
// Load the newly created frame
|
||||
psqlFrameType.load(pgBrowser.docker);
|
||||
return this;
|
||||
},
|
||||
/* Enable/disable PSQL tool menu in tools based
|
||||
* on node selected. if selected node is present
|
||||
* in unsupported_nodes, menu will be disabled
|
||||
* otherwise enabled.
|
||||
*/
|
||||
psqlToolEnabled: function(obj) {
|
||||
|
||||
var isEnabled = (() => {
|
||||
if (!_.isUndefined(obj) && !_.isNull(obj) && pgAdmin['enable_psql']) {
|
||||
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;
|
||||
}
|
||||
})();
|
||||
|
||||
enable(gettext('PSQL Tool'), isEnabled);
|
||||
return isEnabled;
|
||||
},
|
||||
retrieveAncestorOfTypeServer: function(item) {
|
||||
let serverInformation = null;
|
||||
// let aciTreeItem = item || pgBrowser.treeMenu.selected();
|
||||
let treeNode = pgBrowser.treeMenu.findNodeByDomElement(item);
|
||||
|
||||
if (treeNode) {
|
||||
let nodeData;
|
||||
let databaseNode = treeNode.ancestorNode(
|
||||
(node) => {
|
||||
nodeData = node.getData();
|
||||
return (nodeData._type === 'database');
|
||||
}
|
||||
);
|
||||
let isServerNode = (node) => {
|
||||
nodeData = node.getData();
|
||||
return nodeData._type === 'server';
|
||||
};
|
||||
|
||||
if (databaseNode !== null) {
|
||||
if (nodeData._label.indexOf('=') >= 0) {
|
||||
this.alertify.alert(
|
||||
gettext(this.errorAlertTitle),
|
||||
gettext(
|
||||
'Databases with = symbols in the name cannot be backed up or restored using this utility.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if (databaseNode.anyParent(isServerNode))
|
||||
serverInformation = nodeData;
|
||||
}
|
||||
} else {
|
||||
if (treeNode.anyFamilyMember(isServerNode))
|
||||
serverInformation = nodeData;
|
||||
}
|
||||
}
|
||||
|
||||
if (serverInformation === null) {
|
||||
this.alertify.alert(
|
||||
gettext(this.errorAlertTitle),
|
||||
gettext('Please select server or child node from the browser tree.')
|
||||
);
|
||||
}
|
||||
return serverInformation;
|
||||
},
|
||||
psql_tool: function(data, aciTreeIdentifier, gen=false) {
|
||||
const module = 'paths';
|
||||
let preference_name = 'pg_bin_dir';
|
||||
let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
|
||||
const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeIdentifier);
|
||||
|
||||
if ((serverInformation.type && serverInformation.type === 'ppas') ||
|
||||
serverInformation.server_type === 'ppas') {
|
||||
preference_name = 'ppas_bin_dir';
|
||||
msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
|
||||
}
|
||||
const preference = pgBrowser.get_preference(module, preference_name);
|
||||
if (preference) {
|
||||
if (!preference.value) {
|
||||
Alertify.alert(gettext('Configuration required'), msg);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Alertify.alert(
|
||||
gettext(this.errorAlertTitle),
|
||||
gettext('Failed to load preference %s of module %s', preference_name, module)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
|
||||
if (node === undefined || !node.getData()) {
|
||||
Alertify.alert(
|
||||
gettext('PSQL Error'),
|
||||
gettext('No object selected.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
parentData = getTreeNodeHierarchyFromIdentifier.call(
|
||||
pgBrowser,
|
||||
aciTreeIdentifier
|
||||
);
|
||||
|
||||
if(_.isUndefined(parentData.server)) {
|
||||
Alertify.alert(
|
||||
gettext('PSQL Error'),
|
||||
gettext('Please select a server/database object.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const transId = getRandomInt(1, 9999999);
|
||||
|
||||
var panelTitle = '';
|
||||
// Set psql tab title as per prefrences setting.
|
||||
var title_data = {
|
||||
'database': parentData.database ? parentData.database.label : 'postgres' ,
|
||||
'username': parentData.server.user_name,
|
||||
'server': parentData.server.label,
|
||||
'type': 'psql_tool',
|
||||
};
|
||||
var tab_title_placeholder = pgBrowser.get_preferences_for_module('browser').psql_tab_title_placeholder;
|
||||
panelTitle = generateTitle(tab_title_placeholder, title_data);
|
||||
|
||||
const [panelUrl, panelCloseUrl] = this.getPanelUrls(transId, panelTitle, parentData, gen);
|
||||
|
||||
let psqlToolForm = `
|
||||
<form id="psqlToolForm" action="${panelUrl}" method="post">
|
||||
<input id="title" name="title" hidden />
|
||||
<input name="close_url" value="${panelCloseUrl}" hidden />
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById("title").value = "${_.escape(panelTitle)}";
|
||||
document.getElementById("psqlToolForm").submit();
|
||||
</script>
|
||||
`;
|
||||
var open_new_tab = pgBrowser.get_preferences_for_module('browser').new_browser_tab_open;
|
||||
if (open_new_tab && open_new_tab.includes('psql_tool')) {
|
||||
var newWin = window.open('', '_blank');
|
||||
newWin.document.write(psqlToolForm);
|
||||
newWin.document.title = panelTitle;
|
||||
} else {
|
||||
/* On successfully initialization find the properties panel,
|
||||
* create new panel and add it to the dashboard panel.
|
||||
*/
|
||||
var propertiesPanel = pgBrowser.docker.findPanels('properties');
|
||||
var psqlToolPanel = pgBrowser.docker.addPanel('frm_psqltool', wcDocker.DOCK.STACKED, propertiesPanel[0]);
|
||||
|
||||
// Set panel title and icon
|
||||
setPanelTitle(psqlToolPanel, panelTitle);
|
||||
psqlToolPanel.icon('fas fa-terminal psql-tab-style');
|
||||
psqlToolPanel.focus();
|
||||
|
||||
var openPSQLToolURL = 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(psqlToolForm);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
openPSQLToolURL(psqlToolPanel);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
getPanelUrls: function(transId, panelTitle, parentData) {
|
||||
let openUrl = url_for('psql.panel', {
|
||||
trans_id: transId,
|
||||
});
|
||||
const misc_preferences = pgBrowser.get_preferences_for_module('misc');
|
||||
var theme = misc_preferences.theme;
|
||||
|
||||
openUrl += `?sgid=${parentData.server_group._id}`
|
||||
+`&sid=${parentData.server._id}`
|
||||
+`&server_type=${parentData.server.server_type}`
|
||||
+ `&theme=${theme}`;
|
||||
|
||||
if(parentData.database && parentData.database._id) {
|
||||
let db_label = parentData.database._label.replace('\\', '\\\\');
|
||||
openUrl += `&db=${db_label}`;
|
||||
} else {
|
||||
openUrl += `&db=${''}`;
|
||||
}
|
||||
|
||||
let closeUrl = url_for('psql.close', {
|
||||
trans_id: transId,
|
||||
});
|
||||
return [openUrl, closeUrl];
|
||||
},
|
||||
psql_terminal: function() {
|
||||
// theme colors
|
||||
var term = new terminal({
|
||||
cursorBlink: true,
|
||||
macOptionIsMeta: true,
|
||||
scrollback: 5000,
|
||||
});
|
||||
|
||||
return term;
|
||||
},
|
||||
psql_Addon: function(term) {
|
||||
const fitAddon = this.psql_fit_screen();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
const webLinksAddon = this.psql_web_link();
|
||||
term.loadAddon(webLinksAddon);
|
||||
|
||||
const searchAddon = this.psql_search();
|
||||
term.loadAddon(searchAddon);
|
||||
|
||||
fitAddon.fit();
|
||||
term.resize(15, 50);
|
||||
fitAddon.fit();
|
||||
return fitAddon;
|
||||
},
|
||||
psql_fit_screen: function() {
|
||||
return new FitAddon();
|
||||
},
|
||||
psql_web_link: function() {
|
||||
return new WebLinksAddon();
|
||||
},
|
||||
psql_search: function() {
|
||||
return new SearchAddon();
|
||||
},
|
||||
psql_socket: function() {
|
||||
return io('/pty', {pingTimeout: 120000, pingInterval: 25000});
|
||||
},
|
||||
set_theme: function(term) {
|
||||
var theme = {
|
||||
background: getComputedStyle(document.documentElement).getPropertyValue('--psql-background'),
|
||||
foreground: getComputedStyle(document.documentElement).getPropertyValue('--psql-foreground'),
|
||||
cursor: getComputedStyle(document.documentElement).getPropertyValue('--psql-cursor'),
|
||||
cursorAccent: getComputedStyle(document.documentElement).getPropertyValue('--psql-cursorAccent'),
|
||||
selection: getComputedStyle(document.documentElement).getPropertyValue('--psql-selection'),
|
||||
};
|
||||
term.setOption('theme', theme);
|
||||
},
|
||||
psql_socket_io: function(socket, is_enable, sid, db, server_type, fitAddon, term) {
|
||||
// Listen all the socket events emit from server.
|
||||
socket.on('pty-output', function(data){
|
||||
if(data.error) {
|
||||
term.write('\r\n');
|
||||
}
|
||||
term.write(data.result);
|
||||
if(data.error) {
|
||||
term.write('\r\n');
|
||||
}
|
||||
});
|
||||
// Connect socket
|
||||
socket.on('connect', () => {
|
||||
if(is_enable == 'True'){
|
||||
socket.emit('start_process', {'sid': sid, 'db': db, 'stype': server_type });
|
||||
}
|
||||
fitAddon.fit();
|
||||
socket.emit('resize', {'cols': term.cols, 'rows': term.rows});
|
||||
});
|
||||
|
||||
socket.on('conn_error', (response) => {
|
||||
term.write(response.error);
|
||||
fitAddon.fit();
|
||||
socket.emit('resize', {'cols': term.cols, 'rows': term.rows});
|
||||
});
|
||||
|
||||
socket.on('conn_not_allow', () => {
|
||||
term.write('PSQL connection not allowed');
|
||||
fitAddon.fit();
|
||||
socket.emit('resize', {'cols': term.cols, 'rows': term.rows});
|
||||
});
|
||||
|
||||
socket.on('disconnect-psql', () => {
|
||||
socket.emit('server-disconnect', {'sid': sid});
|
||||
term.write('\r\nServer disconnected, Connection terminated, To create new connection please open another psql tool.');
|
||||
});
|
||||
},
|
||||
psql_terminal_io: function(term, socket) {
|
||||
// Listen key press event from terminal and emit socket event.
|
||||
let selected_text = '';
|
||||
term.attachCustomKeyEventHandler(e => {
|
||||
e.stopPropagation();
|
||||
if(e.type=='keydown' && e.metaKey &&(e.key == 'v' || e.key == 'V')) {
|
||||
if(selected_text != '') {
|
||||
if (selected_text.length > 0) {
|
||||
socket.emit('socket_input', {'input': selected_text, 'key_name': e.code});
|
||||
selected_text = '';
|
||||
}
|
||||
} else {
|
||||
navigator.clipboard.readText().then( clipText => {
|
||||
selected_text = clipText;
|
||||
if (selected_text.length > 0) {
|
||||
socket.emit('socket_input', {'input': selected_text, 'key_name': e.code});
|
||||
selected_text = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}else if(e.type=='keydown' && e.metaKey && (e.key == 'c' || e.key == 'C')) {
|
||||
if (term.hasSelection()) {
|
||||
selected_text = term.getSelection();
|
||||
} else {
|
||||
selected_text = clipboard.readText();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
term.onKey(function (ev) {
|
||||
if (pgAdmin['allow_psql_shell_commands']) {
|
||||
socket.emit('socket_input', {'input': ev.key, 'key_name': ev.domEvent.code});
|
||||
} else {
|
||||
socket.emit('socket_input', {'input': ev.key, 'key_name': ev.domEvent.code});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return pgBrowser.psql;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{% 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;}
|
||||
{% 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 style="width: 100%; height: 100%;" id="psql-terminal" class="psql_terminal"></div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block init_script %}
|
||||
require(
|
||||
['sources/generated/psql_tool'],
|
||||
function(pgBrowser) {
|
||||
const term = self.pgAdmin.Browser.psql.psql_terminal();
|
||||
<!--Addon for fitAddon, webLinkAddon, SearchAddon -->
|
||||
const fitAddon = self.pgAdmin.Browser.psql.psql_Addon(term);
|
||||
<!-- Update the theme for terminal as per pgAdmin 4 theme.-->
|
||||
self.pgAdmin.Browser.psql.set_theme(term);
|
||||
<!-- Open the terminal -->
|
||||
term.open(document.getElementById('psql-terminal'));
|
||||
<!-- Socket-->
|
||||
const socket = self.pgAdmin.Browser.psql.psql_socket();
|
||||
self.pgAdmin.Browser.psql.psql_socket_io(socket, '{{is_enable}}', '{{sid}}', '{{db}}', '{{server_type}}', fitAddon, term);
|
||||
self.pgAdmin.Browser.psql.psql_terminal_io(term, socket);
|
||||
|
||||
<!-- Resize the terminal -->
|
||||
function fitToscreen(){
|
||||
fitAddon.fit()
|
||||
socket.emit("resize", {"cols": term.cols, "rows": term.rows})
|
||||
}
|
||||
|
||||
function debounce(func, wait_ms) {
|
||||
let timeout
|
||||
return function(...args) {
|
||||
const context = this
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func.apply(context, args), wait_ms)
|
||||
}
|
||||
}
|
||||
|
||||
const wait_ms = 50;;
|
||||
window.onresize = debounce(fitToscreen, wait_ms)
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
{
|
||||
"psql_user_input": [
|
||||
{
|
||||
"name": "Enter Select 1;",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Enter Backspace",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"is_backspace": true,
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},{
|
||||
"name": "Enter Backspace",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"is_backspace": true,
|
||||
"move_cursor_up": true,
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Enter ArrowUp",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"is_arrowUp": true,
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Enter ArrowUp",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"is_arrowUp": true,
|
||||
"is_history": true,
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Enter ArrowLeft",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"is_arrowLeft": true,
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Enter ArrowRight",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"is_arrowRight": true,
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},{
|
||||
"name": "Read previous executed command",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select 1;",
|
||||
"is_arrowRight": true,
|
||||
"move_cursor_right": true,
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Meta command \\! not allowed",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "\\!",
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Meta command \\! with other cmd not allowed",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "\\! ls",
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Valid commands",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "select \"\\!\"",
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},{
|
||||
"name": "First command as enter",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "",
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Exist psql terminal by using \\q",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "\\q",
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
}
|
||||
],
|
||||
"resize_terminal": [
|
||||
{
|
||||
"name": "Resize psql terminal as per UI.",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_data": {
|
||||
"cols": 141,
|
||||
"rows": 39
|
||||
},
|
||||
"mock_data": {
|
||||
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
}
|
||||
],
|
||||
"backend_task": [
|
||||
{
|
||||
"name": "Backend Task",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"input_cmd": "Select 1;",
|
||||
"is_backend_task": true,
|
||||
"mock_data": {
|
||||
"is_test": true
|
||||
},
|
||||
"expected_data": {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.utils import server_utils as server_utils
|
||||
from pgAdmin4 import app
|
||||
from . import utils as psql_utils
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLBackend(BaseTestGenerator):
|
||||
scenarios = utils.generate_scenarios('backend_task',
|
||||
psql_utils.test_cases)
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
database_info = parent_node_dict["database"][-1]
|
||||
self.did = database_info["db_id"]
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = True
|
||||
self.server_con = server_utils.connect_server(self, self.sid)
|
||||
|
||||
def runTest(self):
|
||||
# Fetch flask client to access current user and other cookies.
|
||||
flask_client = app.test_client()
|
||||
flask_client.get('/')
|
||||
self.test_client = socketio.test_client(app, namespace='/pty',
|
||||
flask_test_client=flask_client)
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'connected'
|
||||
assert received[0]['args'][0]['sid'] != ''
|
||||
|
||||
data = {
|
||||
'sid': self.sid,
|
||||
'db': 'postgres',
|
||||
'pwd': self.server['db_password'],
|
||||
'user': self.server['username'],
|
||||
'is_test': True,
|
||||
'count': 0
|
||||
}
|
||||
|
||||
self.test_client.emit('start_process', data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
for p in self.server['db_password']:
|
||||
input_data = {
|
||||
'input': p,
|
||||
'key_name': 'Key{0}'.format(p)
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
input_data = {
|
||||
'input': '\\n',
|
||||
'key_name': 'Enter'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
for ip in self.input_cmd:
|
||||
input_data = {
|
||||
'input': ip,
|
||||
'key_name': 'Key{0}'.format(ip)
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
input_data = {
|
||||
'input': '\\n',
|
||||
'key_name': 'Enter'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
self.test_client.disconnect(namespace='/pty')
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,36 @@
|
|||
import uuid
|
||||
import random
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
|
||||
|
||||
class PSQLPanel(BaseTestGenerator):
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
self.theme = 'standard'
|
||||
|
||||
def runTest(self):
|
||||
trans_id = random.randint(1, 9999999)
|
||||
url = '/psql/panel/{trans_id}?sgid={sgid}&sid={sid}&server_type=pg' \
|
||||
'&db={db_name}&theme={theme}'.\
|
||||
format(trans_id=trans_id, sgid=self.sgid, sid=self.sid,
|
||||
db_name=self.db_name, theme=self.theme)
|
||||
|
||||
response = self.tester.post(
|
||||
url, data={"title": "panel_title"},
|
||||
content_type="application/x-www-form-urlencoded")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,34 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgAdmin4 import app
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLSocketDisabled(BaseTestGenerator):
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = False
|
||||
|
||||
def runTest(self):
|
||||
self.test_client = socketio.test_client(app, namespace='/pty')
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'conn_not_allow'
|
||||
self.test_client.disconnect(namespace='/pty')
|
||||
self.assertFalse(self.test_client.is_connected('/pty'))
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,148 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.utils import server_utils as server_utils
|
||||
from pgAdmin4 import app
|
||||
from . import utils as psql_utils
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLInput(BaseTestGenerator):
|
||||
scenarios = utils.generate_scenarios('psql_user_input',
|
||||
psql_utils.test_cases)
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
database_info = parent_node_dict["database"][-1]
|
||||
self.did = database_info["db_id"]
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = True
|
||||
self.server_con = server_utils.connect_server(self, self.sid)
|
||||
|
||||
def runTest(self):
|
||||
# Fetch flask client to access current user and other cookies.
|
||||
flask_client = app.test_client()
|
||||
flask_client.get('/')
|
||||
self.test_client = socketio.test_client(app, namespace='/pty',
|
||||
flask_test_client=flask_client)
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'connected'
|
||||
assert received[0]['args'][0]['sid'] != ''
|
||||
|
||||
data = {
|
||||
'sid': self.sid,
|
||||
'db': 'postgres',
|
||||
'pwd': self.server['db_password'],
|
||||
'user': self.server['username']
|
||||
}
|
||||
|
||||
self.test_client.emit('start_process', data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
input_data = {
|
||||
'input': '\\n',
|
||||
'key_name': 'Enter'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
for ip in self.input_cmd:
|
||||
input_data = {
|
||||
'input': ip,
|
||||
'key_name': 'Key{0}'.format(ip)
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
if hasattr(self, 'is_backspace') and self.is_backspace:
|
||||
if hasattr(self, 'move_cursor_up') and self.move_cursor_up:
|
||||
input_data = {
|
||||
'input': '',
|
||||
'key_name': 'ArrowUp'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
|
||||
for ip in self.input_cmd:
|
||||
input_data = {
|
||||
'input': ip,
|
||||
'key_name': 'Backspace'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
if hasattr(self, 'is_arrowUp') and self.is_arrowUp:
|
||||
if hasattr(self, 'is_history') and self.is_history:
|
||||
for ip in self.input_cmd:
|
||||
input_data = {
|
||||
'input': ip,
|
||||
'key_name': 'Key{0}'.format(ip)
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
input_data = {
|
||||
'input': '',
|
||||
'key_name': 'ArrowUp'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
if hasattr(self, 'is_arrowLeft') and self.is_arrowLeft:
|
||||
for ip in self.input_cmd:
|
||||
input_data = {
|
||||
'input': ip,
|
||||
'key_name': 'ArrowLeft'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
if hasattr(self, 'is_arrowRight') and self.is_arrowRight:
|
||||
for ip in self.input_cmd:
|
||||
input_data = {
|
||||
'input': ip,
|
||||
'key_name': 'ArrowRight'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
if hasattr(self, 'move_cursor_right') and self.is_arrowRight:
|
||||
for i in range(2):
|
||||
input_data = {
|
||||
'input': '',
|
||||
'key_name': 'ArrowLeft'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
input_data = {
|
||||
'input': '',
|
||||
'key_name': 'ArrowRight'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data,
|
||||
namespace='/pty')
|
||||
|
||||
input_data = {
|
||||
'input': '\\n',
|
||||
'key_name': 'Enter'
|
||||
}
|
||||
self.test_client.emit('socket_input', input_data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,58 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.utils import server_utils as server_utils
|
||||
from pgAdmin4 import app
|
||||
from . import utils as psql_utils
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLResizeTerminal(BaseTestGenerator):
|
||||
scenarios = utils.generate_scenarios('resize_terminal',
|
||||
psql_utils.test_cases)
|
||||
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
database_info = parent_node_dict["database"][-1]
|
||||
self.did = database_info["db_id"]
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = True
|
||||
self.server_con = server_utils.connect_server(self, self.sid)
|
||||
|
||||
def runTest(self):
|
||||
# Fetch flask client to access current user and other cookies.
|
||||
flask_client = app.test_client()
|
||||
flask_client.get('/')
|
||||
self.test_client = socketio.test_client(app, namespace='/pty',
|
||||
flask_test_client=flask_client)
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'connected'
|
||||
assert received[0]['args'][0]['sid'] != ''
|
||||
|
||||
data = {
|
||||
'sid': self.sid,
|
||||
'db': 'postgres',
|
||||
'pwd': self.server['db_password'],
|
||||
'user': self.server['username']
|
||||
}
|
||||
|
||||
self.test_client.emit('start_process', data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
self.test_client.emit('resize', self.input_data, namespace='/pty')
|
||||
|
||||
self.test_client.disconnect(namespace='/pty')
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,35 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgAdmin4 import app
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLSocketConnect(BaseTestGenerator):
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = True
|
||||
|
||||
def runTest(self):
|
||||
self.test_client = socketio.test_client(app, namespace='/pty')
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'connected'
|
||||
assert received[0]['args'][0]['sid'] != ''
|
||||
self.test_client.disconnect(namespace='/pty')
|
||||
self.assertFalse(self.test_client.is_connected('/pty'))
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,52 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgAdmin4 import app
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLSocketDisconnect(BaseTestGenerator):
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = True
|
||||
|
||||
def runTest(self):
|
||||
# Fetch flask client to access current user and other cookies.
|
||||
flask_test_client = app.test_client()
|
||||
flask_test_client.get('/')
|
||||
|
||||
self.test_client = socketio.test_client(
|
||||
app,
|
||||
flask_test_client=flask_test_client,
|
||||
namespace='/pty')
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'connected'
|
||||
assert received[0]['args'][0]['sid'] != ''
|
||||
|
||||
data = {
|
||||
'sid': self.sid,
|
||||
'db': 'postgres',
|
||||
'pwd': self.server['db_password'],
|
||||
'user': self.server['username']
|
||||
}
|
||||
|
||||
self.test_client.emit('start_process', data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
self.test_client.disconnect(namespace='/pty')
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,57 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.utils import server_utils as server_utils
|
||||
from pgAdmin4 import app
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLStartProcess(BaseTestGenerator):
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
database_info = parent_node_dict["database"][-1]
|
||||
self.did = database_info["db_id"]
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = True
|
||||
|
||||
self.server_con = server_utils.connect_server(self, self.sid)
|
||||
|
||||
def runTest(self):
|
||||
# Fetch flask client to access current user and other cookies.
|
||||
flask_client = app.test_client()
|
||||
flask_client.get('/')
|
||||
self.test_client = socketio.test_client(app, namespace='/pty',
|
||||
flask_test_client=flask_client)
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'connected'
|
||||
assert received[0]['args'][0]['sid'] != ''
|
||||
|
||||
import random
|
||||
trans_id = random.randint(1, 9999999)
|
||||
|
||||
data = {
|
||||
'sid': self.sid,
|
||||
'db': 'postgres',
|
||||
'pwd': self.server['db_password'],
|
||||
'user': self.server['username']
|
||||
}
|
||||
|
||||
self.test_client.emit('start_process', data, namespace='/pty')
|
||||
self.test_client.get_received('/pty')
|
||||
|
||||
self.test_client.disconnect(namespace='/pty')
|
||||
self.assertFalse(self.test_client.is_connected('/pty'))
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,48 @@
|
|||
import uuid
|
||||
import config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression import parent_node_dict
|
||||
from regression.test_setup import config_data
|
||||
from pgAdmin4 import app
|
||||
from .... import socketio
|
||||
|
||||
|
||||
class PSQLStartProcessFail(BaseTestGenerator):
|
||||
def setUp(self):
|
||||
self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
|
||||
self.sid = parent_node_dict["server"][-1]["server_id"]
|
||||
self.did = utils.create_database(self.server, self.db_name)
|
||||
self.sgid = config_data["server_group"]
|
||||
config.ENABLE_PSQL = True
|
||||
|
||||
def runTest(self):
|
||||
self.test_client = socketio.test_client(app, namespace='/pty')
|
||||
self.assertTrue(self.test_client.is_connected('/pty'))
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'connected'
|
||||
assert received[0]['args'][0]['sid'] != ''
|
||||
|
||||
data = {
|
||||
'sid': self.sid,
|
||||
'db': 'postgres',
|
||||
'pwd': self.server['db_password'],
|
||||
'user': self.server['username']
|
||||
}
|
||||
config.ENABLE_PSQL = False
|
||||
self.test_client.emit('start_process', data, namespace='/pty')
|
||||
received = self.test_client.get_received('/pty')
|
||||
|
||||
assert received[0]['name'] == 'conn_not_allow'
|
||||
|
||||
self.test_client.disconnect(namespace='/pty')
|
||||
self.assertFalse(self.test_client.is_connected('/pty'))
|
||||
|
||||
def tearDown(self):
|
||||
connection = utils.get_db_connection(self.server['db'],
|
||||
self.server['username'],
|
||||
self.server['db_password'],
|
||||
self.server['host'],
|
||||
self.server['port'])
|
||||
utils.drop_database(connection, self.db_name)
|
|
@ -0,0 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
with open(CURRENT_PATH + "/psql_test_data.json") as data_file:
|
||||
test_cases = json.load(data_file)
|
|
@ -374,3 +374,20 @@ div.strikeout:after {
|
|||
/* 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;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ class _PGCSRFProtect(CSRFProtect):
|
|||
'pgadmin.tools.schema_diff.ddl_compare',
|
||||
'pgadmin.authenticate.login',
|
||||
'pgadmin.tools.erd.panel',
|
||||
'pgadmin.tools.psql.panel',
|
||||
]
|
||||
|
||||
for exempt in exempt_views:
|
||||
|
|
|
@ -72,6 +72,7 @@ class _Preference(object):
|
|||
self.select2 = kwargs.get('select2', None)
|
||||
self.fields = kwargs.get('fields', None)
|
||||
self.allow_blanks = kwargs.get('allow_blanks', None)
|
||||
self.disabled = kwargs.get('disabled', False)
|
||||
|
||||
# Look into the configuration table to find out the id of the specific
|
||||
# preference.
|
||||
|
@ -252,6 +253,7 @@ class _Preference(object):
|
|||
'select2': self.select2,
|
||||
'value': self.get(),
|
||||
'fields': self.fields,
|
||||
'disabled': self.disabled,
|
||||
}
|
||||
return res
|
||||
|
||||
|
@ -414,6 +416,7 @@ class Preferences(object):
|
|||
:param fields: field schema (if preference has more than one field to
|
||||
take input from user e.g. keyboardshortcut preference)
|
||||
:param allow_blanks: Flag specify whether to allow blank value.
|
||||
:param disabled: Flag specify whether to disable the setting or not.
|
||||
"""
|
||||
min_val = kwargs.get('min_val', None)
|
||||
max_val = kwargs.get('max_val', None)
|
||||
|
@ -423,6 +426,7 @@ class Preferences(object):
|
|||
select2 = kwargs.get('select2', None)
|
||||
fields = kwargs.get('fields', None)
|
||||
allow_blanks = kwargs.get('allow_blanks', None)
|
||||
disabled = kwargs.get('disabled', False)
|
||||
|
||||
cat = self.__category(category, category_label)
|
||||
if name in cat['preferences']:
|
||||
|
@ -439,7 +443,8 @@ class Preferences(object):
|
|||
(cat['preferences'])[name] = res = _Preference(
|
||||
cat['id'], name, label, _type, default, help_str=help_str,
|
||||
min_val=min_val, max_val=max_val, options=options,
|
||||
select2=select2, fields=fields, allow_blanks=allow_blanks
|
||||
select2=select2, fields=fields, allow_blanks=allow_blanks,
|
||||
disabled=disabled
|
||||
)
|
||||
|
||||
return res
|
||||
|
|
|
@ -355,6 +355,7 @@ module.exports = [{
|
|||
debugger_direct: './pgadmin/tools/debugger/static/js/direct.js',
|
||||
schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js',
|
||||
erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js',
|
||||
psql_tool: './pgadmin/tools/psql/static/js/index.js',
|
||||
file_utils: './pgadmin/misc/file_manager/static/js/utility.js',
|
||||
'pgadmin.style': pgadminCssStyles,
|
||||
pgadmin: pgadminScssStyles,
|
||||
|
@ -493,7 +494,7 @@ module.exports = [{
|
|||
],
|
||||
},
|
||||
},
|
||||
}, {
|
||||
},{
|
||||
test: require.resolve('./node_modules/acitree/js/jquery.aciTree.min'),
|
||||
use: {
|
||||
loader: 'imports-loader',
|
||||
|
@ -532,6 +533,7 @@ module.exports = [{
|
|||
'pure|pgadmin.tools.storage_manager',
|
||||
'pure|pgadmin.tools.search_objects',
|
||||
'pure|pgadmin.tools.erd_module',
|
||||
'pure|pgadmin.tools.psql_module',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -159,6 +159,15 @@ var webpackShimConfig = {
|
|||
'jquery.acisortable': path.join(__dirname, './node_modules/acitree/js/jquery.aciSortable.min'),
|
||||
'jquery.acifragment': path.join(__dirname, './node_modules/acitree/js/jquery.aciFragment.min'),
|
||||
|
||||
//xterm
|
||||
'xterm': path.join(__dirname, './node_modules/xterm/lib/xterm.js'),
|
||||
'xterm-addon-fit': path.join(__dirname, './node_modules/xterm-addon-fit/lib/xterm-addon-fit.js'),
|
||||
'xterm-addon-web-links': path.join(__dirname, './node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js'),
|
||||
'xterm-addon-search': path.join(__dirname, './node_modules/xterm-addon-search/lib/xterm-addon-search.js'),
|
||||
|
||||
//socket
|
||||
'socketio': path.join(__dirname, './node_modules/socket.io-client/dist/socket.io.js'),
|
||||
|
||||
// Backbone and Backgrid
|
||||
'backbone': path.join(__dirname, './node_modules/backbone/backbone'),
|
||||
'backbone.undo': path.join(__dirname, './node_modules/backbone-undo/Backbone.Undo'),
|
||||
|
@ -288,6 +297,8 @@ var webpackShimConfig = {
|
|||
'pgadmin.tools.storage_manager': path.join(__dirname, './pgadmin/tools/storage_manager/static/js/storage_manager'),
|
||||
'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'),
|
||||
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
|
||||
'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'),
|
||||
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
|
||||
'pgadmin.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'),
|
||||
'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
|
||||
'pgadmin.user_management.current_user': '/user_management/current_user',
|
||||
|
|
|
@ -177,6 +177,7 @@ module.exports = {
|
|||
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
|
||||
'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'),
|
||||
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
|
||||
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
|
||||
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
|
||||
'tools': path.join(__dirname, './pgadmin/tools/'),
|
||||
'pgadmin.user_management.current_user': regressionDir + '/javascript/fake_current_user',
|
||||
|
|
|
@ -2031,6 +2031,11 @@ backgrid@~0.3.7:
|
|||
backbone "1.1.2 || 1.2.3 || ~1.3.2"
|
||||
underscore "^1.8.0"
|
||||
|
||||
backo2@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
|
||||
integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
|
@ -3638,7 +3643,22 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
|
|||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
engine.io-parser@~4.0.0:
|
||||
engine.io-client@~5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.1.1.tgz#f5c3aaaef1bdc9443aac6ffde48b3b2fb2dc56fc"
|
||||
integrity sha512-jPFpw2HLL0lhZ2KY0BpZhIJdleQcUO9W1xkIpo0h3d6s+5D6+EV/xgQw9qWOmymszv2WXef/6KUUehyxEKomlQ==
|
||||
dependencies:
|
||||
base64-arraybuffer "0.1.4"
|
||||
component-emitter "~1.3.0"
|
||||
debug "~4.3.1"
|
||||
engine.io-parser "~4.0.1"
|
||||
has-cors "1.1.0"
|
||||
parseqs "0.0.6"
|
||||
parseuri "0.0.6"
|
||||
ws "~7.4.2"
|
||||
yeast "0.1.2"
|
||||
|
||||
engine.io-parser@~4.0.0, engine.io-parser@~4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
|
||||
integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
|
||||
|
@ -4643,6 +4663,11 @@ has-bigints@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
|
||||
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
|
||||
|
||||
has-cors@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
|
||||
integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
|
@ -6734,6 +6759,16 @@ parse5@^6.0.1:
|
|||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||
|
||||
parseqs@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
|
||||
integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
|
||||
|
||||
parseuri@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
|
||||
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
|
||||
|
||||
parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
|
@ -7985,7 +8020,20 @@ socket.io-adapter@~2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527"
|
||||
integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
|
||||
|
||||
socket.io-parser@~4.0.3:
|
||||
socket.io-client@^4.0.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.2.tgz#95ad7113318ea01fba0860237b96d71e1b1fd2eb"
|
||||
integrity sha512-RDpWJP4DQT1XeexmeDyDkm0vrFc0+bUsHDKiVGaNISJvJonhQQOMqV9Vwfg0ZpPJ27LCdan7iqTI92FRSOkFWQ==
|
||||
dependencies:
|
||||
"@types/component-emitter" "^1.2.10"
|
||||
backo2 "~1.0.2"
|
||||
component-emitter "~1.3.0"
|
||||
debug "~4.3.1"
|
||||
engine.io-client "~5.1.1"
|
||||
parseuri "0.0.6"
|
||||
socket.io-parser "~4.0.4"
|
||||
|
||||
socket.io-parser@~4.0.3, socket.io-parser@~4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
|
||||
integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
|
||||
|
@ -9218,6 +9266,26 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
|
|||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
xterm-addon-fit@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
||||
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
|
||||
|
||||
xterm-addon-search@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0.tgz#e33eab918df7eac7e7baf95dd2b3d14133754881"
|
||||
integrity sha512-MPJGPVPpHRUw9cLIuqQbrVepmENMOybVUSxIALz5h1ryyQBrVqVujq2hL5aroX5/dZJoHx9lGHQTVLQ07SKgKA==
|
||||
|
||||
xterm-addon-web-links@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0.tgz#265cbf8221b9b315d0a748e1323bee331cd5da03"
|
||||
integrity sha512-xv8GeiINmx0zENO9hf5k+5bnkaE8mRzF+OBAr9WeFq2eLaQSudioQSiT34M1ofKbzcdjSsKiZm19Rw3i4eXamg==
|
||||
|
||||
xterm@^4.11.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0.tgz#db09b425b4dcae5b96f8cbbaaa93b3bc60997ca9"
|
||||
integrity sha512-K5mF/p3txUV18mjiZFlElagoHFpqXrm5OYHeoymeXSu8GG/nMaOO/+NRcNCwfdjzAbdQ5VLF32hEHiWWKKm0bw==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
|
@ -9273,6 +9341,11 @@ yauzl@^2.4.2:
|
|||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
|
||||
yeast@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
|
|
Loading…
Reference in New Issue