diff --git a/docs/en_US/images/preferences_dashboard_display.png b/docs/en_US/images/preferences_dashboard_display.png index 251da4d7e..b7a0f1a36 100644 Binary files a/docs/en_US/images/preferences_dashboard_display.png and b/docs/en_US/images/preferences_dashboard_display.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 1e82c6c44..81d305eac 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -154,6 +154,9 @@ the graphs on the *Dashboard* tab: :alt: Preferences dialog dashboard display options :align: center +* Set the warning and alert threshold value to highlight the long-running + queries on the dashboard. + * When the *Show activity?* switch is set to *True*, activity tables will be displayed on dashboards. diff --git a/docs/en_US/release_notes_5_5.rst b/docs/en_US/release_notes_5_5.rst index 6a930fa46..5ce817034 100644 --- a/docs/en_US/release_notes_5_5.rst +++ b/docs/en_US/release_notes_5_5.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #1975 `_ - Highlighted long running queries on the dashboards. | `Issue #3893 `_ - Added support for Reassign/Drop Owned for login roles. | `Issue #3920 `_ - Do not block the query editor window when running a query. | `Issue #6559 `_ - Added option to provide maximum width of the column when 'Resize by data?’ option in the preferences is set to True. diff --git a/web/pgadmin/dashboard/__init__.py b/web/pgadmin/dashboard/__init__.py index 018087c2b..8ebf4c918 100644 --- a/web/pgadmin/dashboard/__init__.py +++ b/web/pgadmin/dashboard/__init__.py @@ -8,6 +8,7 @@ ########################################################################## """A blueprint module implementing the dashboard frame.""" +import math from functools import wraps from flask import render_template, url_for, Response, g, request from flask_babelex import gettext @@ -150,6 +151,15 @@ class DashboardModule(PgAdminModule): 'details') ) + self.long_running_query_threshold = self.dashboard_preference.register( + 'display', 'long_running_query_threshold', + gettext('Long running query thresholds'), 'threshold', + None, category_label=PREF_LABEL_DISPLAY, + help_str=gettext('Set the warning and alert threshold value to ' + 'highlight the long-running queries on the ' + 'dashboard.') + ) + def get_exposed_url_endpoints(self): """ Returns: @@ -300,13 +310,14 @@ def index(sid=None, did=None): ) -def get_data(sid, did, template): +def get_data(sid, did, template, check_long_running_query=False): """ Generic function to get server stats based on an SQL template Args: sid: The server ID did: The database ID template: The SQL template name + check_long_running_query: Returns: @@ -324,12 +335,47 @@ def get_data(sid, did, template): if not status: return internal_server_error(errormsg=res) + # Check the long running query status and set the row type. + if check_long_running_query: + get_long_running_query_status(res['rows']) + return ajax_response( response=res['rows'], status=200 ) +def get_long_running_query_status(activities): + """ + This function is used to check the long running query and set the + row type to highlight the row color accordingly + """ + dash_preference = Preferences.module('dashboards') + long_running_query_threshold = \ + dash_preference.preference('long_running_query_threshold').get() + + if long_running_query_threshold is not None: + long_running_query_threshold = long_running_query_threshold.split('|') + + warning_value = float(long_running_query_threshold[0]) \ + if long_running_query_threshold[0] != '' else math.inf + alert_value = float(long_running_query_threshold[1]) \ + if long_running_query_threshold[1] != '' else math.inf + + for row in activities: + row['row_type'] = None + + # We care for only those queries which are in active state. + if row['state'] != 'active': + continue + + active_since = float(row['active_since']) + if active_since > warning_value: + row['row_type'] = 'warning' + if active_since > alert_value: + row['row_type'] = 'alert' + + @blueprint.route('/dashboard_stats', endpoint='dashboard_stats') @blueprint.route('/dashboard_stats/', @@ -376,7 +422,7 @@ def activity(sid=None, did=None): :param sid: server id :return: """ - return get_data(sid, did, 'activity.sql') + return get_data(sid, did, 'activity.sql', True) @blueprint.route('/locks/', endpoint='locks') diff --git a/web/pgadmin/dashboard/static/js/dashboard.js b/web/pgadmin/dashboard/static/js/dashboard.js index cdf122a0c..8d7e42ecc 100644 --- a/web/pgadmin/dashboard/static/js/dashboard.js +++ b/web/pgadmin/dashboard/static/js/dashboard.js @@ -437,12 +437,31 @@ define('pgadmin.dashboard', [ var data = new Data(); + var HighlightedRow = Backgrid.Row.extend({ + render: function() { + Backgrid.Row.prototype.render.call(this); + var row_type = this.model.get('row_type'); + + if (_.isUndefined(row_type) || _.isNull(row_type)) { + this.$el.removeClass('alert'); + this.$el.removeClass('warning'); + } else if (row_type === 'warning') { + this.$el.addClass('warning'); + } else if (row_type === 'alert') { + this.$el.addClass('alert'); + } + + return this; + } + }); + // Set up the grid var grid = new Backgrid.Grid({ emptyText: gettext('No data found'), columns: columns, collection: data, className: 'backgrid presentation table table-bordered table-noouter-border table-hover', + row: HighlightedRow }); // Render the grid diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/10_plus/activity.sql b/web/pgadmin/dashboard/templates/dashboard/sql/10_plus/activity.sql index 7f273e4ef..a9379f67b 100644 --- a/web/pgadmin/dashboard/templates/dashboard/sql/10_plus/activity.sql +++ b/web/pgadmin/dashboard/templates/dashboard/sql/10_plus/activity.sql @@ -12,7 +12,8 @@ SELECT query, pg_catalog.to_char(state_change, 'YYYY-MM-DD HH24:MI:SS TZ') AS state_change, pg_catalog.to_char(query_start, 'YYYY-MM-DD HH24:MI:SS TZ') AS query_start, - backend_type + backend_type, + CASE WHEN state = 'active' THEN ROUND((extract(epoch from now() - query_start) / 60)::numeric, 2) ELSE 0 END AS active_since FROM pg_catalog.pg_stat_activity {% if did %}WHERE diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/9.6_plus/activity.sql b/web/pgadmin/dashboard/templates/dashboard/sql/9.6_plus/activity.sql index 2fb5e839d..b0a9c5847 100644 --- a/web/pgadmin/dashboard/templates/dashboard/sql/9.6_plus/activity.sql +++ b/web/pgadmin/dashboard/templates/dashboard/sql/9.6_plus/activity.sql @@ -11,7 +11,8 @@ SELECT pg_catalog.pg_blocking_pids(pid) AS blocking_pids, query, pg_catalog.to_char(state_change, 'YYYY-MM-DD HH24:MI:SS TZ') AS state_change, - pg_catalog.to_char(query_start, 'YYYY-MM-DD HH24:MI:SS TZ') AS query_start + pg_catalog.to_char(query_start, 'YYYY-MM-DD HH24:MI:SS TZ') AS query_start, + CASE WHEN state = 'active' THEN ROUND((extract(epoch from now() - query_start) / 60)::numeric, 2) ELSE 0 END AS active_since FROM pg_catalog.pg_stat_activity {% if did %}WHERE diff --git a/web/pgadmin/dashboard/templates/dashboard/sql/default/activity.sql b/web/pgadmin/dashboard/templates/dashboard/sql/default/activity.sql index f82cdb689..494b5adbb 100644 --- a/web/pgadmin/dashboard/templates/dashboard/sql/default/activity.sql +++ b/web/pgadmin/dashboard/templates/dashboard/sql/default/activity.sql @@ -10,7 +10,8 @@ SELECT CASE WHEN waiting THEN '{{ _('yes') }}' ELSE '{{ _('no') }}' END AS waiting, query, pg_catalog.to_char(state_change, 'YYYY-MM-DD HH24:MI:SS TZ') AS state_change, - pg_catalog.to_char(query_start, 'YYYY-MM-DD HH24:MI:SS TZ') AS query_start + pg_catalog.to_char(query_start, 'YYYY-MM-DD HH24:MI:SS TZ') AS query_start, + CASE WHEN state = 'active' THEN ROUND((extract(epoch from now() - query_start) / 60)::numeric, 2) ELSE 0 END AS active_since FROM pg_catalog.pg_stat_activity {% if did %}WHERE diff --git a/web/pgadmin/preferences/static/js/preferences.js b/web/pgadmin/preferences/static/js/preferences.js index d65eb5c26..7020f3ff1 100644 --- a/web/pgadmin/preferences/static/js/preferences.js +++ b/web/pgadmin/preferences/static/js/preferences.js @@ -309,6 +309,11 @@ define('pgadmin.preferences', [ return 'radioModern'; case 'selectFile': return 'binary-paths-grid'; + case 'threshold': + p.warning_label = gettext('Warning'); + p.alert_label = gettext('Alert'); + p.unit = gettext('(in minutes)'); + return 'threshold'; default: if (console && console.warn) { // Warning for developer only. diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index 612e1ab6d..e7a30a22f 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -3584,5 +3584,61 @@ define([ }, }); + Backform.ThresholdControl = Backform.Control.extend({ + template: _.template([ + '', + '
', + ' <%=warning_label%>', + ' ', + ' <%=alert_label%>', + ' ', + ' <%=unit%>', + ' <% if (helpMessage && helpMessage.length) { %>', + ' <%=helpMessage%>', + ' <% } %>', + '
', + ].join('\n')), + + events: { + 'change input#warning_threshold': 'onChange', + 'change input#alert_threshold': 'onChange', + }, + initialize: function() { + Backform.Control.prototype.initialize.apply(this, arguments); + }, + + render: function() { + var field = _.defaults(this.field.toJSON(), this.defaults), + attributes = this.model.toJSON(), + threshold_val = []; + + if (!_.isUndefined(this.model.get('value')) && !_.isNull(this.model.get('value'))){ + threshold_val = this.model.get('value').split('|'); + } + + var data = _.extend(field, { + attributes: attributes, + 'warning_value': threshold_val.length > 0 ? threshold_val[0] : '', + 'alert_value': threshold_val.length > 1 ? threshold_val[1] : '' + }); + + this.$el.html(this.template(data)); + + return this; + }, + + onChange: function() { + // Get the value from raw jquery and concat it using | + // and set the value + var warning_threshold = $('input#warning_threshold').val(), + alert_threshold = $('input#alert_threshold').val(); + + var threshold_val = warning_threshold + '|' + alert_threshold; + this.model.set(this.field.get('name'), threshold_val, { + silent: false + }); + } + }); + return Backform; }); diff --git a/web/pgadmin/static/scss/_backgrid.overrides.scss b/web/pgadmin/static/scss/_backgrid.overrides.scss index 908c3d1b5..0729bf715 100644 --- a/web/pgadmin/static/scss/_backgrid.overrides.scss +++ b/web/pgadmin/static/scss/_backgrid.overrides.scss @@ -339,6 +339,14 @@ table.backgrid { border-bottom-color: $color-gray-light !important; } } + + tr.warning { + background-color: $color-warning !important; + } + + tr.alert { + background-color: $color-danger-light !important; + } } table tr th button { diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py index 33b6a5627..e36c756f1 100644 --- a/web/pgadmin/utils/preferences.py +++ b/web/pgadmin/utils/preferences.py @@ -437,7 +437,7 @@ class Preferences(object): assert _type in ( 'boolean', 'integer', 'numeric', 'date', 'datetime', 'options', 'multiline', 'switch', 'node', 'text', 'radioModern', - 'keyboardshortcut', 'select2', 'selectFile', + 'keyboardshortcut', 'select2', 'selectFile', 'threshold' ), "Type cannot be found in the defined list!" (cat['preferences'])[name] = res = _Preference(