From 617e9dbb3a466507a7cfdd6ba144c0dca5955dec Mon Sep 17 00:00:00 2001 From: Murtuza Zabuawala Date: Mon, 24 Jul 2017 12:13:24 +0100 Subject: [PATCH] Allow queries to be cancelled from the dashboards. Fixes #1812 --- web/pgadmin/dashboard/__init__.py | 27 +++ .../templates/dashboard/js/dashboard.js | 165 +++++++++++++++++- .../static/css/bootstrap.overrides.css | 10 +- .../static/js/alerts/alertify_wrapper.js | 17 +- web/pgadmin/static/scss/_alert.scss | 35 +--- .../static/scss/_alertify.overrides.scss | 8 +- 6 files changed, 221 insertions(+), 41 deletions(-) diff --git a/web/pgadmin/dashboard/__init__.py b/web/pgadmin/dashboard/__init__.py index b91ccb2ae..8ebab16c1 100644 --- a/web/pgadmin/dashboard/__init__.py +++ b/web/pgadmin/dashboard/__init__.py @@ -456,3 +456,30 @@ def config(sid=None): :return: """ return get_data(sid, None, 'config.sql') + + +@blueprint.route( + '/cancel_query//', methods=['DELETE'] +) +@blueprint.route( + '/cancel_query///', methods=['DELETE'] +) +@login_required +@check_precondition +def cancel_query(sid=None, did=None, pid=None): + """ + This function cancel the specific session + :param sid: server id + :param did: database id + :param pid: session/process id + :return: Response + """ + sql = "SELECT pg_cancel_backend({0});".format(pid) + status, res = g.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + + return ajax_response( + response=gettext("Success") if res else gettext("Failed"), + status=200 + ) diff --git a/web/pgadmin/dashboard/templates/dashboard/js/dashboard.js b/web/pgadmin/dashboard/templates/dashboard/js/dashboard.js index 8bd62d6b8..1eee1da1a 100644 --- a/web/pgadmin/dashboard/templates/dashboard/js/dashboard.js +++ b/web/pgadmin/dashboard/templates/dashboard/js/dashboard.js @@ -1,9 +1,11 @@ define('pgadmin.dashboard', [ 'sources/url_for', 'sources/gettext', 'require', 'jquery', 'underscore', - 'pgadmin', 'backbone', 'backgrid', 'flotr2', 'backgrid.filter', + 'pgadmin', 'backbone', 'backgrid', 'flotr2', 'alertify', + 'sources/alerts/alertify_wrapper', 'backgrid.filter', 'pgadmin.browser', 'bootstrap', 'wcdocker' ], -function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr) { +function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr, + alertify, AlertifyWrapper) { var wcDocker = window.wcDocker, pgBrowser = pgAdmin.Browser; @@ -12,7 +14,81 @@ function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr) { if (pgAdmin.Dashboard) return; - var dashboardVisible = true; + var dashboardVisible = true, + cancel_query_url = '', + is_super_user = false, + current_user, maintenance_database, + is_server_dashboard = false, + is_database_dashboard = false; + + // Custom BackGrid cell, Responsible for cancelling active sessions + var cancelQueryCell = Backgrid.Extension.DeleteCell.extend({ + render: function () { + this.$el.empty(); + this.$el.html( + "" + ); + this.delegateEvents(); + return this; + }, + deleteRow: function(e) { + var self = this; + e.preventDefault(); + + var canDeleteRow = Backgrid.callByNeed( + self.column.get('canDeleteRow'), self.column, self.model + ); + // If we are not allowed to cancel the query, return from here + if(!canDeleteRow) + return; + + // This will refresh the grid + var refresh_grid = function() { + if(is_server_dashboard) { + $('#btn_server_activity_refresh').click(); + } else if(is_database_dashboard) { + $('#btn_database_activity_refresh').click(); + } + }; + + var title = gettext('Cancel Active Query?'), + txtConfirm = gettext('Are you sure you wish to cancel the active query?'); + + alertify.confirm( + title, + txtConfirm, + function(evt) { + $.ajax({ + url: cancel_query_url + self.model.get('pid'), + type:'DELETE', + success: function(res) { + var alertifyWrapper = new AlertifyWrapper(); + if (res == gettext('Success')) { + alertifyWrapper.success(gettext('Active query cancelled successfully.')); + refresh_grid(); + } else { + alertifyWrapper.error(gettext('An error occurred whilst cancelling the active query.')); + } + }, + error: function(xhr, status, error) { + try { + var err = $.parseJSON(xhr.responseText); + if (err.success == 0) { + var alertifyWrapper = new AlertifyWrapper(); + alertifyWrapper.error(err.errormsg); + } + } catch (e) {} + } + }); + }, + function(evt) { + return true; + } + ); + } + }); pgAdmin.Dashboard = { init: function() { @@ -73,6 +149,20 @@ function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr) { sid = -1, did = -1, b = pgAdmin.Browser, m = b && b.Nodes[itemData._type]; + cancel_query_url = url_for('dashboard.index') + 'cancel_query/'; + + // Check if user is super user + var server = treeHierarchy['server']; + maintenance_database = (server && server.db) || null; + + if(server && server.user && server.user.is_superuser) { + is_super_user = true; + } else { + is_super_user = false; + // Set current user + current_user = (server && server.user) ? server.user.name : null; + } + if (m && m.dashboard) { if (_.isFunction(m.dashboard)) { url = m.dashboard.apply( @@ -85,10 +175,16 @@ function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr) { if ('database' in treeHierarchy) { sid = treeHierarchy.server._id; did = treeHierarchy.database._id; + is_server_dashboard = false; + is_database_dashboard = true; url += sid + '/' + did; + cancel_query_url += sid + '/' + did + '/'; } else if ('server' in treeHierarchy) { sid = treeHierarchy.server._id; + is_server_dashboard = true; + is_database_dashboard = false; url += sid; + cancel_query_url += sid + '/'; } } @@ -486,6 +582,15 @@ function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr) { }]); } + // Add cancel active query button + server_activity_columns.unshift({ + name: "pg-backform-delete", label: "", + cell: cancelQueryCell, + editable: false, cell_priority: -1, + canDeleteRow: pgAdmin.Dashboard.can_cancel_active_query, + postgres_version: version + }); + var server_locks_columns = [{ name: "pid", label: gettext('PID'), @@ -790,6 +895,14 @@ function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr) { }]); } + database_activity_columns.unshift({ + name: "pg-backform-delete", label: "", + cell: cancelQueryCell, + editable: false, cell_priority: -1, + canDeleteRow: pgAdmin.Dashboard.can_cancel_active_query, + postgres_version: version + }); + var database_locks_columns = [{ name: "pid", label: gettext('PID'), @@ -954,6 +1067,52 @@ function(url_for, gettext, r, $, _, pgAdmin, Backbone, Backgrid, Flotr) { }, toggleVisibility: function(flag) { dashboardVisible = flag; + }, + can_cancel_active_query: function(m) { + // We will validate if user is allowed to cancel the active query + // If there is only one active session means it probably our main + // connection session + var active_sessions = m.collection.where({'state': 'active'}), + alertifyWrapper = new AlertifyWrapper(), + pg_version = this.get('postgres_version') || null; + + // With PG10, We have background process showing on dashboard + // We will not allow user to cancel them as they will fail with error + // anyway, so better usability we will throw our on notification + + // Background processes do not have database field populated + if (pg_version && pg_version >= 100000 && !m.get('datname')) { + alertifyWrapper.info( + gettext('You cannot cancel background worker processes.') + ); + return false; + // If it is the last active connection on maintenance db then error out + } else if (maintenance_database == m.get('datname') && + m.get('state') == 'active' && active_sessions.length == 1) { + alertifyWrapper.error( + gettext('You are not allowed to cancel the main active session.') + ); + return false; + } else if(m.get('state') == 'idle') { + // If this session is already idle then do nothing + alertifyWrapper.info( + gettext('The session is already in idle state.') + ); + return false; + } else if(is_super_user) { + // Super user can do anything + return true; + } else if (current_user && current_user == m.get('usename')) { + // Non-super user can cancel only their active queries + return true; + } else { + // Do not allow to cancel someone else session to non-super user + var alertifyWrapper = new AlertifyWrapper(); + alertifyWrapper.error( + gettext('Superuser privileges are required to cancel another users query.') + ); + return false; + } } }; diff --git a/web/pgadmin/static/css/bootstrap.overrides.css b/web/pgadmin/static/css/bootstrap.overrides.css index a590aa76f..6274c7473 100755 --- a/web/pgadmin/static/css/bootstrap.overrides.css +++ b/web/pgadmin/static/css/bootstrap.overrides.css @@ -71,7 +71,7 @@ iframe { } /* - * Bootstrap 3 remove submenus as they don't work overly well on Mobile. The + * Bootstrap 3 remove submenus as they don't work overly well on Mobile. The * following CSS adds them back in - for our purposes they actually work fine * on Mobile and are perfectly responsive */ @@ -570,7 +570,7 @@ fieldset[disabled] .form-control { } .backgrid.presentation td.renderable { - padding: 3px; + padding: 6px 3px 3px 3px; font-size: 12px; } @@ -984,7 +984,7 @@ ul.nav.nav-tabs { background-position: 0px 2px; } -/* This rule will stop Chrome apply highlighting to elements such as DIV's used as modals */ +/* This rule will stop Chrome apply highlighting to elements such as DIV's used as modals */ *:focus { outline: none; } @@ -993,7 +993,7 @@ ul.nav.nav-tabs { .alert-info-panel { border: 2px solid #a1a1a1; margin-top: 2em; - padding: 5px 5px; + padding: 5px 5px; background: #dddddd; border-radius: 5px; height: 8em; @@ -1334,4 +1334,4 @@ body { color: #333; font-size: 14px; font-weight: normal; -} \ No newline at end of file +} diff --git a/web/pgadmin/static/js/alerts/alertify_wrapper.js b/web/pgadmin/static/js/alerts/alertify_wrapper.js index d3e7ed642..10d5f6017 100644 --- a/web/pgadmin/static/js/alerts/alertify_wrapper.js +++ b/web/pgadmin/static/js/alerts/alertify_wrapper.js @@ -31,10 +31,25 @@ define([ return alert; }; + var info = function(message, timeout) { + var alertMessage = '\ +
\ +
\ +
\ + \ +
\ +
' + message + '
\ +
\ +
'; + var alert = alertify.notify(alertMessage, timeout); + return alert; + }; + $.extend(this, { 'success': success, 'error': error, + 'info': info, }); }; return AlertifyWrapper; -}); \ No newline at end of file +}); diff --git a/web/pgadmin/static/scss/_alert.scss b/web/pgadmin/static/scss/_alert.scss index 28b5f2a2e..fdd4546c7 100644 --- a/web/pgadmin/static/scss/_alert.scss +++ b/web/pgadmin/static/scss/_alert.scss @@ -103,37 +103,6 @@ category: alerts padding: 15px; } -.alert-icon { - display: inline-block; - color: white; - padding: 15px; - width: 50px; - height: 50px; - font-size: 14px; -} - -.success-icon { - background: #3a773a; -} - -.error-icon { - background: #d0021b; -} - -.alert-text { - display: inline-block; - padding: 0 12px 0 10px; -} - -.alert-row { - display: block; -} - -.alert-box { - padding: 0px; - display: inline-block; -} - .alert-icon { display: inline-block; color: white; @@ -152,6 +121,10 @@ category: alerts background: #d0021b; } +.info-icon { + background: #2c76b4; +} + .alert-text { display: inline-block; padding: 0 12px 0 10px; diff --git a/web/pgadmin/static/scss/_alertify.overrides.scss b/web/pgadmin/static/scss/_alertify.overrides.scss index f1987f966..8344956b2 100644 --- a/web/pgadmin/static/scss/_alertify.overrides.scss +++ b/web/pgadmin/static/scss/_alertify.overrides.scss @@ -170,6 +170,12 @@ button.pg-alertify-button { @extend .ajs-text-smoothing; } +.ajs-message.ajs-visible { + @extend .bg-blue-1; + @extend .border-blue-2; + @extend .ajs-text-smoothing; +} + .media-body { display: table-row; -} \ No newline at end of file +}