public inbox for [email protected]  
help / color / mirror / Atom feed
From: Nikhil Mohite <[email protected]>
To: pgadmin-hackers <[email protected]>
Subject: [pgAdmin][RM-2341]: Add menu option for starting PSQL
Date: Mon, 10 May 2021 18:15:17 +0530
Message-ID: <CAOBg0AO6Tjksb+EOA_-O13yRzdL_2d3y6G74m=GGQS+Ymjv=0g@mail.gmail.com> (raw)

Hi Hackers,

Please find the attached patch for RM-2341
<https://redmine.postgresql.org/issues/2341;: Add Menu option for starting
PSQL.
1. Added new Option PSQL Tool in Tools menu.
2. Added the same option for Server and Database nodes from the tree view.





-- 
*Thanks & Regards,*
*Nikhil Mohite*
*Software Engineer.*
*EDB Postgres* <https://www.enterprisedb.com/;
*Mob.No: +91-7798364578.*


Attachments:

  [application/octet-stream] RM_2341.patch (111.9K, 3-RM_2341.patch)
  download | inline diff:
diff --git a/docs/en_US/developer_tools.rst b/docs/en_US/developer_tools.rst
index 1ff442cc..bb67e33a 100644
--- a/docs/en_US/developer_tools.rst
+++ b/docs/en_US/developer_tools.rst
@@ -16,3 +16,4 @@ PL/SQL code.
    editgrid
    schema_diff
    erd_tool
+   psql_tool
diff --git a/docs/en_US/images/psql_tool.png b/docs/en_US/images/psql_tool.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5f88ba5a573aa60bf7e08b8a38526dc73e8fcd5
GIT binary patch
literal 23736
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
P=p;Q)S3j3^P6<r_JTn~)

literal 0
HcmV?d00001

diff --git a/docs/en_US/psql_tool.rst b/docs/en_US/psql_tool.rst
new file mode 100644
index 00000000..e0b3287e
--- /dev/null
+++ b/docs/en_US/psql_tool.rst
@@ -0,0 +1,18 @@
+.. _psql_tool:
+
+******************
+`PSQL Tool`:index:
+******************
+
+PSQL tool allows user to connect to PostgreSQL server using psql terminal.
+
+* Login to PostgreSQL server through terminal in pgAdmin.
+* execute all type of commands.
+
+.. 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.
+
diff --git a/requirements.txt b/requirements.txt
index edd7000b..fb8f25cd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -36,3 +36,6 @@ sshtunnel==0.*
 ldap3==2.*
 Flask-BabelEx==0.*
 gssapi==1.6.*
+
+flask-socketio>=5.0.1
+eventlet==0.30.2
diff --git a/web/config.py b/web/config.py
index 2643ef19..002a9951 100644
--- a/web/config.py
+++ b/web/config.py
@@ -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,19 @@ 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
diff --git a/web/package.json b/web/package.json
index cc1e15ab..0d0f79b9 100644
--- a/web/package.json
+++ b/web/package.json
@@ -103,6 +103,7 @@
     "json-bignumber": "^1.0.1",
     "karma-coverage": "^2.0.3",
     "leaflet": "^1.5.1",
+
     "lodash": "4.*",
     "ml-matrix": "^6.5.0",
     "moment": "^2.24.0",
@@ -118,13 +119,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.9.1",
     "webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker/#c4a3398b89588408dc705895675bce7bd7660d36",
-    "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 .",
diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py
index d2bd1af6..6e1e4ea6 100644
--- a/web/pgAdmin4.py
+++ b/web/pgAdmin4.py
@@ -15,6 +15,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 '
                        'or later.')
@@ -37,7 +38,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 +98,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 +209,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())
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index a7333537..a641ea66 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -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=True, engineio_logger=True, 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
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 235db027..b093e3a4 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -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,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
index 01ab89c5..c4784d51 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
@@ -91,6 +91,11 @@ define('pgadmin.node.database', [
           name: 'generate_erd', node: 'database', module: this,
           applies: ['object', 'context'], callback: 'generate_erd',
           category: 'erd', priority: 5, label: gettext('Generate ERD (Beta)'),
+        },{
+          name: 'psql_tool', node: 'database', module: this,
+          applies: ['object', 'context'], callback: 'db_psql_tool',
+          category: 'psql_tool', priority: 5, label: gettext('PSQL Tool (Beta)'),
+          enable: 'is_psql_enabled'
         }]);
 
         _.bindAll(this, 'connection_lost');
@@ -122,6 +127,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);
       },
@@ -266,6 +274,15 @@ define('pgadmin.node.database', [
           pgBrowser.erd.showErdTool(d, i, true);
         },
 
+        /* Open psql tool for db*/
+        db_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);
+        },
+
         /* Connect the database (if not connected), before opening this node */
         beforeopen: function(item, data) {
           if(!data || data._type != 'database' || data.label == 'template0') {
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index b21cba43..1a7a0cef 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -102,6 +102,12 @@ define('pgadmin.node.server', [
             data_disabled: gettext('Database is already disconnected.'),
           },
         },{
+          name: 'server_psql', node: 'server', module: this,
+          applies: ['object', 'context'], callback: 'server_psql_tool',
+          category: 'psql_tool', priority: 5, label: gettext('PSQL Tool (Beta)'),
+          enable : 'is_psql_enabled',
+        },
+        {
           name: 'reload_configuration', node: 'server', module: this,
           applies: ['tools', 'context'], callback: 'reload_configuration',
           category: 'reload', priority: 6, label: gettext('Reload Configuration'),
@@ -183,6 +189,9 @@ define('pgadmin.node.server', [
       is_connected: function(node) {
         return (node && node.connected == true);
       },
+      is_psql_enabled: function(node) {
+        return (node && node.connected == true) && pgAdmin['enable_psql'];
+      },
       enable_reload_config: function(node) {
         // Must be connected & is Super user
         if (node && node._type == 'server' &&
@@ -728,6 +737,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: {
diff --git a/web/pgadmin/browser/static/js/panel.js b/web/pgadmin/browser/static/js/panel.js
index c9e64132..3068aabe 100644
--- a/web/pgadmin/browser/static/js/panel.js
+++ b/web/pgadmin/browser/static/js/panel.js
@@ -122,10 +122,30 @@ define(
                     myPanel.on(ev, that.handleVisibility.bind(myPanel, ev));
                   });
               }
+
+              // Listen on detach panel event
+              myPanel.on(wcDocker.EVENT.DETACHED, function(obj) {
+                that.setCodeMirrorHeight(obj);
+              });
             },
           });
         }
       },
+      setCodeMirrorHeight: function() {
+        // Fix for mac os code-mirror showing black screen.
+        var txtArea = $('.pg-panel-content .sql_textarea > textarea').first();
+        txtArea.css('z-index', -1);
+        var $tabContent = $('.pg-panel-content > .sql_textarea').first();
+        var $sqlPane = $tabContent.find('.CodeMirror > div > textarea');
+        for(let i=0; i<$sqlPane.length; i++) {$($sqlPane[i]).css('z-index', -1);}
+
+        $tabContent = $('.pg-panel-content > .sql_textarea').first();
+        $sqlPane = $tabContent.find('.pg-panel-content');
+        $sqlPane.find('.CodeMirror').css(
+          'cssText',
+          'height: ' + ($tabContent.height()) + 'px !important;'
+        );
+      },
       eventFunc: function(eventName) {
         var name = $(this).data('pgAdminName');
 
diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js
index 8597df48..b0a317a5 100644
--- a/web/pgadmin/browser/templates/browser/js/utils.js
+++ b/web/pgadmin/browser/templates/browser/js/utils.js
@@ -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',
diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js
index 14140a2f..0b3ad81b 100644
--- a/web/pgadmin/static/bundle/browser.js
+++ b/web/pgadmin/static/bundle/browser.js
@@ -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();
 });
diff --git a/web/pgadmin/static/css/style.css b/web/pgadmin/static/css/style.css
index c2a776c8..5b4a9f2d 100644
--- a/web/pgadmin/static/css/style.css
+++ b/web/pgadmin/static/css/style.css
@@ -21,3 +21,5 @@
 
 @import '../vendor/backgrid/backgrid.css';
 @import '../vendor/backgrid/backgrid-select-all.css';
+
+@import 'node_modules/xterm/css/xterm.css';
diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py
new file mode 100644
index 00000000..27ae7e2a
--- /dev/null
+++ b/web/pgadmin/tools/psql/__init__.py
@@ -0,0 +1,503 @@
+#!/usr/bin/env python3
+import os
+import re
+import select
+import signal
+import termios
+import struct
+import fcntl
+import pty
+import config
+import eventlet.green.subprocess as subprocess
+from flask import Response, session, url_for, request
+from pgadmin.browser.utils import underscore_unescape
+from flask import render_template, copy_current_request_context, \
+    current_app as app
+from flask_babelex import gettext
+from pgadmin.utils import PgAdminModule
+from flask_security import login_required, current_user
+from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \
+    ERROR_MSG_TRANS_ID_NOT_FOUND
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from pgadmin.model import Server
+from pgadmin.utils import get_complete_file_path
+
+from ... import socketio as sio
+
+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')
+
+
[email protected]("/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
+    )
+
+
[email protected]('/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 request.args:
+        params.update({k: v for k, v in request.args.items()})
+
+    return render_template('editor_template.html',
+                           sid=params['sid'],
+                           db=params['db'],
+                           server_type=params['server_type'],
+                           is_enable=config.ENABLE_PSQL,
+                           title=underscore_unescape(params['title']))
+
+
+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)
+
+
[email protected]('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': True}
+    else:
+        sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
+                 to=request.sid)
+
+
[email protected]('start_process', namespace='/pty')
+@login_required
+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
+        # 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)
+
+        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)
+
+                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:
+                        # If command is invalid then emit error to user.
+                        if session_last_cmd[sid]['invalid_cmd']:
+                            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
+
+    # 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 = data['db']
+            # driver = get_driver(PG_DEFAULT_DRIVER)
+            # manager = driver.connection_manager(int(data['sid']))
+            # conn = manager.connection()
+            conn, manager = _get_connection(int(data['sid']), data)
+            psql_utility = manager.utility('sql')
+            print("psql_utility: ", psql_utility)
+            connection_data = get_connection_str(psql_utility, conn, db,
+                                                 manager)
+        except Exception as e:
+            # If any error raised during the start the PSQL emit error to UI.
+            sio.emit(
+                'conn_error',
+                {
+                    'error': 'Error while running psql command: {0}'.format(e),
+                }, namespace='/pty', room=request.sid)
+
+        try:
+            sio.start_background_task(read_and_forward_pty_output,
+                                      request.sid, data)
+        except Exception as e:
+            print(e)
+    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()
+        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)
+            # raise ConnectionLost(sid, conn.db, trans_id)
+
+        return conn, manager
+    except Exception as e:
+        app.logger.error(e)
+        raise
+
+
+def get_connection_str(psql_utility, conn, db, manager):
+    """
+    Get connection string(through connection dsn)
+    :param psql_utility: PostgreSQL binary path.
+    :param conn: Connection data
+    :param db: database name to connect specific db.
+    :return: connection attribute list for PSQL connection.
+    """
+
+    conn_attr = conn.conn.dsn
+    if 'password=xxx' in conn_attr:
+        conn_attr = conn_attr.replace('password=xxx', '')
+
+    if db != '':
+        conn_attr = conn_attr.replace('dbname=postgres',
+                                      'dbname={0}'.format(db))
+    # Add application name to new created psql terminal instance
+    # through pgAdmin 4.
+    conn_attr = "{0} {1}".format(conn_attr,
+                                 " application_name='pgAdmin4: psql'")
+
+    conn_attr_list = list()
+    conn_attr_list.append(psql_utility)
+    conn_attr_list.append(conn_attr)
+    return conn_attr_list
+
+
[email protected]('socket_input', namespace='/pty')
+def socket_input(data):
+    """
+    This get the user input through socket.
+    :param data: User input from socket.
+    """
+    try:
+        # 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:
+                # 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:
+                    user_input = str(session_input[request.sid]).strip()
+                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:
+
+                    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] = ''
+                elif re.search("\\\!", user_input) and \
+                    not config.ALLOW_PSQL_SHELL_COMMANDS and \
+                        not session_last_cmd[request.sid]['is_new_connection']:
+                    stop_execution = True
+                    # Check \! is passed as string or not.
+                    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())
+                elif user_input == '\q':
+                    # If user enter \q to terminate the PSQL, emit the msg to
+                    # notify user connection is terminated.
+                    os.write(app.config['sessions'][request.sid],
+                             '\n'.encode())
+                    sio.emit('pty-output',
+                             {
+                                 'result': gettext(
+                                     'Connection terminated, TO create new '
+                                     'connection please open another psql'
+                                     ' tool.'),
+                                 'error': True},
+                             namespace='/pty', room=request.sid)
+                else:
+                    os.write(app.config['sessions'][request.sid],
+                             data['input'].encode())
+                session_input[request.sid] = ''
+                session_last_cmd[request.sid]['is_new_connection'] = False
+            else:
+                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])):
+
+                    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'] = ''
+                elif request.sid in session_input:
+                    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
+                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())
+    except Exception as e:
+        # Delete socket id from sessions.
+        del app.config['sessions'][request.sid]
+
+
[email protected]('resize', namespace='/pty')
+def resize(data):
+    """
+    Resize the pty terminal as per the UI terminal.
+    :param data: UI terminal rows and cols data
+    """
+    if request.sid in app.config['sessions']:
+        set_term_size(app.config['sessions'][request.sid], data['rows'],
+                      data['cols'])
+
+
[email protected]('disconnect', namespace='/pty')
+def disconnect():
+    """
+    Disconnect the socket and terminate the process
+    """
+    if request.sid in pdata:
+        # On disconnect socket manually exit the psql terminal and close the
+        # parend and child fd then kill the subprocess.
+        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]
+        os.kill(pdata[request.sid].pid, signal.SIGSTOP)
diff --git a/web/pgadmin/tools/psql/static/js/index.js b/web/pgadmin/tools/psql/static/js/index.js
new file mode 100644
index 00000000..59ae8b4b
--- /dev/null
+++ b/web/pgadmin/tools/psql/static/js/index.js
@@ -0,0 +1,22 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 * as csrfToken from 'sources/csrf';
+import {initialize} from './psql_module';
+
+let pgBrowserOut = initialize(gettext, url_for, $, _, pgAdmin, csrfToken);
+
+module.exports = {
+  pgBrowser: pgBrowserOut,
+};
diff --git a/web/pgadmin/tools/psql/static/js/psql_module.js b/web/pgadmin/tools/psql/static/js/psql_module.js
new file mode 100644
index 00000000..eca623cd
--- /dev/null
+++ b/web/pgadmin/tools/psql/static/js/psql_module.js
@@ -0,0 +1,334 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 LocalEchoController from 'local-echo-controller';
+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 clipboard from 'sources/selection/clipboard';
+
+import 'wcdocker';
+import {getRandomInt} from 'sources/utils';
+import {getTreeNodeHierarchyFromIdentifier} from 'sources/tree/pgadmin_tree_node';
+
+
+export function setPanelTitle(psqlToolPanel, panelTitle) {
+  psqlToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
+}
+
+var wcDocker = window.wcDocker;
+
+export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken) {
+  var pgBrowser = pgAdmin.Browser;
+  var terminal = Terminal;
+  var parentData = null;
+  var localControl = LocalEchoController;
+  /* 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',
+      });
+
+      let self = this;
+      /* Cache may take time to load for the first time
+       * Keep trying till available
+       */
+      let cacheIntervalId = setInterval(function() {
+        if(pgBrowser.preference_version() > 0) {
+          self.preferences = pgBrowser.get_preferences_for_module('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;
+    },
+
+    psqlToolEnabled: function(obj) {
+      //Same as query tool
+      var isEnabled = (() => {
+        if (!_.isUndefined(obj) && !_.isNull(obj)) {
+          if ((this.enable_psql_tool == true) && ((obj._type == 'server' && obj.connected == true )|| obj._type == 'database')) {
+            return true;
+          } else {
+            return false;
+          }
+        } else {
+          return false;
+        }
+      })();
+      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);
+
+      // const node1 = pgBrowser.treeMenu.findNodeByDomElement(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 = '';
+      if (parentData.database) {
+        panelTitle = parentData.database.label + '/' + parentData.server.user_name + '@' + parentData.server.label;
+      } else {
+        panelTitle = parentData.server.user_name + '@' + parentData.server.label;
+      }
+      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');
+        psqlToolPanel.focus();
+
+        // Listen on the panel closed event.
+        /*psqlToolPanel.on(wcDocker.EVENT.CLOSED, function() {
+          $.ajax({
+            url: panelCloseUrl,
+            method: 'DELETE',
+          });
+        });*/
+
+        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);
+
+      }
+
+      //      var url_params = {
+      //        'sid': parentData.server._id,
+      //      };
+      //      var  baseUrl = url_for('psql.initialize', url_params);
+      //
+      //      window.open(baseUrl, '_blank');
+    },
+    getPanelUrls: function(transId, panelTitle, parentData) {
+      let openUrl = url_for('psql.panel', {
+        trans_id: transId,
+      });
+
+      openUrl += `?sgid=${parentData.server_group._id}`
+        +`&sid=${parentData.server._id}`
+        +`&server_type=${parentData.server.server_type}`;
+
+      if(parentData.database && parentData.database._id) {
+        openUrl += `&db=${parentData.database._label}`;
+      } else {
+        openUrl += `&db=${''}`;
+      }
+
+      let closeUrl = url_for('psql.close', {
+        trans_id: transId,
+        //        sgid: parentData.server_group._id,
+        //        sid: parentData.server._id,
+        //        did: parentData.database._id,
+      });
+      return [openUrl, closeUrl];
+    },
+    psql_terminal: function() {
+      return new terminal({
+        cursorBlink: true,
+        macOptionIsMeta: true,
+        scrollback: 10000,
+      });
+    },
+    psql_local_controller: function() {
+      return new localControl();
+    },
+    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});
+    },
+    get_parent_db: function(){
+      return parentData.server;
+    },
+    _clipboard: function() {
+      return clipboard;
+    }
+  };
+
+  return pgBrowser.psql;
+}
+
diff --git a/web/pgadmin/tools/psql/templates/editor_template.html b/web/pgadmin/tools/psql/templates/editor_template.html
new file mode 100644
index 00000000..e5f50a29
--- /dev/null
+++ b/web/pgadmin/tools/psql/templates/editor_template.html
@@ -0,0 +1,137 @@
+{% 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();
+
+        const fitAddon = self.pgAdmin.Browser.psql.psql_fit_screen();
+        term.loadAddon(fitAddon);
+        let clipboard = self.pgAdmin.Browser.psql._clipboard();
+
+        const webLinksAddon = self.pgAdmin.Browser.psql.psql_web_link();
+        term.loadAddon(webLinksAddon);
+
+        const searchAddon = self.pgAdmin.Browser.psql.psql_search();
+        term.loadAddon(searchAddon);
+
+        term.open(document.getElementById('psql-terminal'));
+        fitAddon.fit()
+        term.resize(15, 50)
+        fitAddon.fit()
+        let selected_text = '';
+        let user_input = '';
+        let is_pwd = true;
+        let cursor_position = 0;
+
+        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();
+                }
+            }
+<!--            else if (e.type=='keydown' && e.keyCode == 32) {-->
+<!--                user_input += e.key;-->
+<!--                console.log('SPACEBAR');-->
+<!--                socket.emit("socket_input", {"input": e.key, 'key_name': e.code});-->
+<!--            }-->
+            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 {
+                console.log("socket_input" + ev.key);
+                socket.emit("socket_input", {"input": ev.key, 'key_name': ev.domEvent.code});
+            }
+        });
+
+        const socket = self.pgAdmin.Browser.psql.psql_socket();
+
+        socket.on("pty-output", function(data){
+            if(data.error) {
+                term.write('\r\n');
+            }
+            term.write(data.result);
+            if(data.error) {
+                term.write('\r\n');
+            }
+        })
+
+        socket.on("connect", () => {
+            if('{{is_enable}}' == 'True'){
+                socket.emit('start_process', {"sid": {{sid}}, "db": '{{db}}', 'stype': '{{server_type}}' });
+            }
+            fitToscreen();
+        });
+
+        socket.on("conn_error", (response) => {
+            term.write(response.error);
+            fitToscreen();
+        });
+
+        socket.on("conn_not_allow", () => {
+            term.write('PSQL connection not allowed');
+            fitToscreen();
+        });
+
+        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 %}
+
+
diff --git a/web/pgadmin/tools/psql/tests/__init__.py b/web/pgadmin/tools/psql/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/tools/psql/tests/psql_test_data.json b/web/pgadmin/tools/psql/tests/psql_test_data.json
new file mode 100644
index 00000000..9b2cfaf5
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/psql_test_data.json
@@ -0,0 +1,174 @@
+{
+  "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": "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": {
+      }
+    }
+  ]
+}
diff --git a/web/pgadmin/tools/psql/tests/test_backend_task.py b/web/pgadmin/tools/psql/tests/test_backend_task.py
new file mode 100644
index 00000000..99073c0a
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_backend_task.py
@@ -0,0 +1,88 @@
+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('/session')
+        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)
diff --git a/web/pgadmin/tools/psql/tests/test_panel.py b/web/pgadmin/tools/psql/tests/test_panel.py
new file mode 100644
index 00000000..83480681
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_panel.py
@@ -0,0 +1,35 @@
+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"]
+
+    def runTest(self):
+        trans_id = random.randint(1, 9999999)
+        url = '/psql/panel/{trans_id}?sgid={sgid}&sid={sid}&server_type=pg' \
+              '&db={db_name}'.\
+            format(trans_id=trans_id, sgid=self.sgid, sid=self.sid,
+                   db_name=self.db_name)
+
+        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)
diff --git a/web/pgadmin/tools/psql/tests/test_psql_disabled.py b/web/pgadmin/tools/psql/tests/test_psql_disabled.py
new file mode 100644
index 00000000..32eaa623
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_psql_disabled.py
@@ -0,0 +1,36 @@
+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 flask import current_app as app
+
+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')
+        print("received:", received)
+        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)
diff --git a/web/pgadmin/tools/psql/tests/test_psql_input.py b/web/pgadmin/tools/psql/tests/test_psql_input.py
new file mode 100644
index 00000000..8fe0b47a
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_psql_input.py
@@ -0,0 +1,159 @@
+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('/session')
+        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')
+        received = self.test_client.get_received('/pty')
+        print("received values: ", received)
+
+        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')
+            received = self.test_client.get_received('/pty')
+            print("user I/P:: ", received)
+
+        input_data = {
+            'input': '\\n',
+            'key_name': 'Enter'
+        }
+        self.test_client.emit('socket_input', input_data, namespace='/pty')
+        received = 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')
+        received = 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)
diff --git a/web/pgadmin/tools/psql/tests/test_resize_terminal.py b/web/pgadmin/tools/psql/tests/test_resize_terminal.py
new file mode 100644
index 00000000..9ce7d004
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_resize_terminal.py
@@ -0,0 +1,60 @@
+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('/session')
+        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')
+        received = self.test_client.get_received('/pty')
+
+        self.test_client.emit('resize', self.input_data, namespace='/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)
diff --git a/web/pgadmin/tools/psql/tests/test_socket_connect.py b/web/pgadmin/tools/psql/tests/test_socket_connect.py
new file mode 100644
index 00000000..7b8031bd
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_socket_connect.py
@@ -0,0 +1,36 @@
+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 flask import current_app as app
+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')
+        print("received:", received)
+        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)
diff --git a/web/pgadmin/tools/psql/tests/test_socket_disconnect.py b/web/pgadmin/tools/psql/tests/test_socket_disconnect.py
new file mode 100644
index 00000000..cd2cf5ac
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_socket_disconnect.py
@@ -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 flask import current_app as app
+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):
+        self.test_client = socketio.test_client(app, namespace='/pty')
+        self.assertTrue(self.test_client.is_connected('/pty'))
+        received = self.test_client.get_received('/pty')
+        print("received:", received)
+        assert received[0]['name'] == 'connected'
+        assert received[0]['args'][0]['sid'] != ''
+        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)
diff --git a/web/pgadmin/tools/psql/tests/test_start_process.py b/web/pgadmin/tools/psql/tests/test_start_process.py
new file mode 100644
index 00000000..055f7ce3
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_start_process.py
@@ -0,0 +1,63 @@
+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 pgadmin.browser.server_groups.servers.databases.tests import utils as \
+    database_utils
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+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)
+        print(self.server_con)
+
+    def runTest(self):
+        # Fetch flask client to access current user and other cookies.
+        flask_client = app.test_client()
+        flask_client.get('/session')
+        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')
+        received = self.test_client.get_received('/pty')
+        print("received values: ", received)
+
+        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)
diff --git a/web/pgadmin/tools/psql/tests/test_start_process_fail.py b/web/pgadmin/tools/psql/tests/test_start_process_fail.py
new file mode 100644
index 00000000..895fc533
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_start_process_fail.py
@@ -0,0 +1,60 @@
+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 pgadmin.browser.server_groups.servers.databases.tests import utils as \
+    database_utils
+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
+
+        db_con = database_utils.connect_database(self,
+                                                 self.sgid,
+                                                 self.sid,
+                                                 self.did)
+        print("DB connection")
+
+    def runTest(self):
+        # Fetch flask client to access current user and other cookies.
+        flask_client = app.test_client()
+        flask_client.get('/session')
+        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')
+        print("received:", received)
+        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']
+        }
+        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)
diff --git a/web/pgadmin/tools/psql/tests/utils.py b/web/pgadmin/tools/psql/tests/utils.py
new file mode 100644
index 00000000..85bd5373
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/utils.py
@@ -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)
diff --git a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
index 0bb40802..5ffc8241 100644
--- a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
+++ b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
@@ -374,3 +374,9 @@ 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%;
+}
diff --git a/web/pgadmin/utils/csrf.py b/web/pgadmin/utils/csrf.py
index 23abfffa..71ae82ea 100644
--- a/web/pgadmin/utils/csrf.py
+++ b/web/pgadmin/utils/csrf.py
@@ -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:
diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py
index 15ceeb1a..e9efe479 100644
--- a/web/pgadmin/utils/preferences.py
+++ b/web/pgadmin/utils/preferences.py
@@ -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
diff --git a/web/webpack.config.js b/web/webpack.config.js
index e906a567..104a3b20 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -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',
           ],
         },
       },
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 074b2580..b860d21f 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -159,6 +159,16 @@ 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'),
+    'local-echo-controller': path.join(__dirname, './node_modules/local-echo'),
+
+    //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 +298,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',
diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js
index e0b6fd69..3cc079f9 100644
--- a/web/webpack.test.config.js
+++ b/web/webpack.test.config.js
@@ -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',


view thread (54+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected]
  Subject: Re: [pgAdmin][RM-2341]: Add menu option for starting PSQL
  In-Reply-To: <CAOBg0AO6Tjksb+EOA_-O13yRzdL_2d3y6G74m=GGQS+Ymjv=0g@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox