Add support for LISTEN/NOTIFY in the query tool. Fixes #3204
parent
2b4605a9d3
commit
38ee39ae7a
|
@ -11,6 +11,7 @@ Features
|
||||||
********
|
********
|
||||||
|
|
||||||
| `Bug #1447 <https://redmine.postgresql.org/issues/1447>`_ - Add support for SSH tunneled connections
|
| `Bug #1447 <https://redmine.postgresql.org/issues/1447>`_ - Add support for SSH tunneled connections
|
||||||
|
| `Bug #3204 <https://redmine.postgresql.org/issues/3204>`_ - Add support for LISTEN/NOTIFY in the query tool
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
*********
|
*********
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from selenium.common.exceptions import StaleElementReferenceException
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from selenium.webdriver import ActionChains
|
from selenium.webdriver import ActionChains
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
@ -55,7 +58,7 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
||||||
# explain query with verbose and cost
|
# explain query with verbose and cost
|
||||||
print("Explain query with verbose and cost... ",
|
print("Explain query with verbose and cost... ",
|
||||||
file=sys.stderr, end="")
|
file=sys.stderr, end="")
|
||||||
if self._test_explain_plan_feature():
|
if self._supported_server_version():
|
||||||
self._query_tool_explain_with_verbose_and_cost()
|
self._query_tool_explain_with_verbose_and_cost()
|
||||||
print("OK.", file=sys.stderr)
|
print("OK.", file=sys.stderr)
|
||||||
self._clear_query_tool()
|
self._clear_query_tool()
|
||||||
|
@ -65,7 +68,7 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
||||||
# explain analyze query with buffers and timing
|
# explain analyze query with buffers and timing
|
||||||
print("Explain analyze query with buffers and timing... ",
|
print("Explain analyze query with buffers and timing... ",
|
||||||
file=sys.stderr, end="")
|
file=sys.stderr, end="")
|
||||||
if self._test_explain_plan_feature():
|
if self._supported_server_version():
|
||||||
self._query_tool_explain_analyze_with_buffers_and_timing()
|
self._query_tool_explain_analyze_with_buffers_and_timing()
|
||||||
print("OK.", file=sys.stderr)
|
print("OK.", file=sys.stderr)
|
||||||
self._clear_query_tool()
|
self._clear_query_tool()
|
||||||
|
@ -96,6 +99,11 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
||||||
print("OK.", file=sys.stderr)
|
print("OK.", file=sys.stderr)
|
||||||
self._clear_query_tool()
|
self._clear_query_tool()
|
||||||
|
|
||||||
|
# Notify Statements.
|
||||||
|
print("Capture Notify Statements... ", file=sys.stderr, end="")
|
||||||
|
self._query_tool_notify_statements()
|
||||||
|
self._clear_query_tool()
|
||||||
|
|
||||||
def after(self):
|
def after(self):
|
||||||
self.page.remove_server(self.server)
|
self.page.remove_server(self.server)
|
||||||
connection = test_utils.get_db_connection(
|
connection = test_utils.get_db_connection(
|
||||||
|
@ -144,8 +152,8 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
||||||
self.page.click_element(
|
self.page.click_element(
|
||||||
self.page.find_by_xpath("//*[@id='btn-clear-dropdown']")
|
self.page.find_by_xpath("//*[@id='btn-clear-dropdown']")
|
||||||
)
|
)
|
||||||
ActionChains(self.driver)\
|
ActionChains(self.driver) \
|
||||||
.move_to_element(self.page.find_by_xpath("//*[@id='btn-clear']"))\
|
.move_to_element(self.page.find_by_xpath("//*[@id='btn-clear']")) \
|
||||||
.perform()
|
.perform()
|
||||||
self.page.click_element(
|
self.page.click_element(
|
||||||
self.page.find_by_xpath("//*[@id='btn-clear']")
|
self.page.find_by_xpath("//*[@id='btn-clear']")
|
||||||
|
@ -579,7 +587,7 @@ SELECT 1, pg_sleep(300)"""
|
||||||
# have 'auto-rollback fa fa-check visibility-hidden' classes
|
# have 'auto-rollback fa fa-check visibility-hidden' classes
|
||||||
|
|
||||||
if 'auto-rollback fa fa-check' == str(
|
if 'auto-rollback fa fa-check' == str(
|
||||||
auto_rollback_check.get_attribute('class')):
|
auto_rollback_check.get_attribute('class')):
|
||||||
auto_rollback_btn.click()
|
auto_rollback_btn.click()
|
||||||
|
|
||||||
auto_commit_btn = self.page.find_by_id("btn-auto-commit")
|
auto_commit_btn = self.page.find_by_id("btn-auto-commit")
|
||||||
|
@ -592,7 +600,7 @@ SELECT 1, pg_sleep(300)"""
|
||||||
# have 'auto-commit fa fa-check visibility-hidden' classes
|
# have 'auto-commit fa fa-check visibility-hidden' classes
|
||||||
|
|
||||||
if 'auto-commit fa fa-check visibility-hidden' == str(
|
if 'auto-commit fa fa-check visibility-hidden' == str(
|
||||||
auto_commit_check.get_attribute('class')):
|
auto_commit_check.get_attribute('class')):
|
||||||
auto_commit_btn.click()
|
auto_commit_btn.click()
|
||||||
|
|
||||||
self.page.find_by_id("btn-flash").click()
|
self.page.find_by_id("btn-flash").click()
|
||||||
|
@ -605,7 +613,7 @@ SELECT 1, pg_sleep(300)"""
|
||||||
'contains(string(), "canceling statement due to user request")]'
|
'contains(string(), "canceling statement due to user request")]'
|
||||||
)
|
)
|
||||||
|
|
||||||
def _test_explain_plan_feature(self):
|
def _supported_server_version(self):
|
||||||
connection = test_utils.get_db_connection(
|
connection = test_utils.get_db_connection(
|
||||||
self.server['db'],
|
self.server['db'],
|
||||||
self.server['username'],
|
self.server['username'],
|
||||||
|
@ -615,3 +623,58 @@ SELECT 1, pg_sleep(300)"""
|
||||||
self.server['sslmode']
|
self.server['sslmode']
|
||||||
)
|
)
|
||||||
return connection.server_version > 90100
|
return connection.server_version > 90100
|
||||||
|
|
||||||
|
def _query_tool_notify_statements(self):
|
||||||
|
wait = WebDriverWait(self.page.driver, 60)
|
||||||
|
|
||||||
|
print("\n\tListen on an event... ", file=sys.stderr, end="")
|
||||||
|
self.page.fill_codemirror_area_with("LISTEN foo;")
|
||||||
|
self.page.find_by_id("btn-flash").click()
|
||||||
|
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
||||||
|
self.page.click_tab('Messages')
|
||||||
|
|
||||||
|
wait.until(EC.text_to_be_present_in_element(
|
||||||
|
(By.CSS_SELECTOR, ".sql-editor-message"), "LISTEN")
|
||||||
|
)
|
||||||
|
print("OK.", file=sys.stderr)
|
||||||
|
self._clear_query_tool()
|
||||||
|
|
||||||
|
print("\tNotify event without data... ", file=sys.stderr, end="")
|
||||||
|
self.page.fill_codemirror_area_with("NOTIFY foo;")
|
||||||
|
self.page.find_by_id("btn-flash").click()
|
||||||
|
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
||||||
|
self.page.click_tab('Notifications')
|
||||||
|
wait.until(EC.text_to_be_present_in_element(
|
||||||
|
(By.CSS_SELECTOR, "td.channel"), "foo")
|
||||||
|
)
|
||||||
|
print("OK.", file=sys.stderr)
|
||||||
|
self._clear_query_tool()
|
||||||
|
|
||||||
|
print("\tNotify event with data... ", file=sys.stderr, end="")
|
||||||
|
if self._supported_server_version():
|
||||||
|
self.page.fill_codemirror_area_with("SELECT pg_notify('foo', "
|
||||||
|
"'Hello')")
|
||||||
|
self.page.find_by_id("btn-flash").click()
|
||||||
|
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
||||||
|
self.page.click_tab('Notifications')
|
||||||
|
wait.until(WaitForAnyElementWithText(
|
||||||
|
(By.CSS_SELECTOR, 'td.payload'), "Hello"))
|
||||||
|
print("OK.", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print("Skipped.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
class WaitForAnyElementWithText(object):
|
||||||
|
def __init__(self, locator, text):
|
||||||
|
self.locator = locator
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
def __call__(self, driver):
|
||||||
|
try:
|
||||||
|
elements = EC._find_elements(driver, self.locator)
|
||||||
|
for elem in elements:
|
||||||
|
if self.text in elem.text:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except StaleElementReferenceException:
|
||||||
|
return False
|
||||||
|
|
|
@ -82,6 +82,8 @@ class ExecuteQuery {
|
||||||
self.loadingScreen.hide();
|
self.loadingScreen.hide();
|
||||||
self.enableSQLEditorButtons();
|
self.enableSQLEditorButtons();
|
||||||
self.sqlServerObject.update_msg_history(false, httpMessageData.data.result);
|
self.sqlServerObject.update_msg_history(false, httpMessageData.data.result);
|
||||||
|
if ('notifies' in httpMessageData.data)
|
||||||
|
self.sqlServerObject.update_notifications(httpMessageData.data.notifies);
|
||||||
|
|
||||||
// Highlight the error in the sql panel
|
// Highlight the error in the sql panel
|
||||||
self.sqlServerObject._highlight_error(httpMessageData.data.result);
|
self.sqlServerObject._highlight_error(httpMessageData.data.result);
|
||||||
|
@ -116,6 +118,8 @@ class ExecuteQuery {
|
||||||
self.loadingScreen.setMessage('Loading data from the database server and rendering...');
|
self.loadingScreen.setMessage('Loading data from the database server and rendering...');
|
||||||
|
|
||||||
self.sqlServerObject.call_render_after_poll(httpMessage.data.data);
|
self.sqlServerObject.call_render_after_poll(httpMessage.data.data);
|
||||||
|
if ('notifies' in httpMessage.data.data)
|
||||||
|
self.sqlServerObject.update_notifications(httpMessage.data.data.notifies);
|
||||||
} else if (ExecuteQuery.isQueryStillRunning(httpMessage)) {
|
} else if (ExecuteQuery.isQueryStillRunning(httpMessage)) {
|
||||||
// If status is Busy then poll the result by recursive call to the poll function
|
// If status is Busy then poll the result by recursive call to the poll function
|
||||||
this.delayedPoll();
|
this.delayedPoll();
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
import gettext from 'sources/gettext';
|
||||||
|
import Backgrid from 'pgadmin.backgrid';
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
import Alertify from 'pgadmin.alertifyjs';
|
||||||
|
|
||||||
|
let NotificationsModel = Backbone.Model.extend({
|
||||||
|
defaults: {
|
||||||
|
recorded_time: undefined,
|
||||||
|
event: undefined,
|
||||||
|
pid: undefined,
|
||||||
|
payload: undefined,
|
||||||
|
},
|
||||||
|
schema: [{
|
||||||
|
id: 'recorded_time',
|
||||||
|
label: gettext('Recorded time'),
|
||||||
|
cell: 'string',
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cellHeaderClasses: 'width_percent_20',
|
||||||
|
headerCell: Backgrid.Extension.CustomHeaderCell,
|
||||||
|
},{
|
||||||
|
id: 'channel',
|
||||||
|
label: gettext('Event'),
|
||||||
|
cell: 'string',
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cellHeaderClasses: 'width_percent_20',
|
||||||
|
headerCell: Backgrid.Extension.CustomHeaderCell,
|
||||||
|
},{
|
||||||
|
id: 'pid',
|
||||||
|
label: gettext('Process ID'),
|
||||||
|
cell: 'string',
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cellHeaderClasses: 'width_percent_20',
|
||||||
|
headerCell: Backgrid.Extension.CustomHeaderCell,
|
||||||
|
},{
|
||||||
|
id: 'payload',
|
||||||
|
label: gettext('Payload'),
|
||||||
|
cell: 'string',
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cellHeaderClasses: 'width_percent_40',
|
||||||
|
headerCell: Backgrid.Extension.CustomHeaderCell,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let NotificationCollection = Backbone.Collection.extend({
|
||||||
|
model: NotificationsModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
let queryToolNotifications = {
|
||||||
|
|
||||||
|
collection: null,
|
||||||
|
|
||||||
|
/* This function is responsible to create and render the
|
||||||
|
* new backgrid for the notification tab.
|
||||||
|
*/
|
||||||
|
renderNotificationsGrid: function(notifications_panel) {
|
||||||
|
if (!queryToolNotifications.collection)
|
||||||
|
queryToolNotifications.collection = new NotificationCollection();
|
||||||
|
|
||||||
|
let gridCols = [{
|
||||||
|
name: 'recorded_time',
|
||||||
|
label: gettext('Recorded time'),
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cell: 'string',
|
||||||
|
}, {
|
||||||
|
name: 'channel',
|
||||||
|
label: gettext('Event'),
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cell: 'string',
|
||||||
|
}, {
|
||||||
|
name: 'pid',
|
||||||
|
label: gettext('Process ID'),
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cell: 'string',
|
||||||
|
}, {
|
||||||
|
name: 'payload',
|
||||||
|
label: gettext('Payload'),
|
||||||
|
type: 'text',
|
||||||
|
editable: false,
|
||||||
|
cell: 'string',
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Set up the grid
|
||||||
|
let notifications_grid = new Backgrid.Grid({
|
||||||
|
columns: gridCols,
|
||||||
|
collection: queryToolNotifications.collection,
|
||||||
|
className: 'backgrid table-bordered presentation table backgrid-striped',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render the grid
|
||||||
|
if (notifications_grid)
|
||||||
|
notifications_panel.$container.append(notifications_grid.render().el);
|
||||||
|
},
|
||||||
|
|
||||||
|
// This function is used to raise notify messages and update the
|
||||||
|
// notification grid.
|
||||||
|
updateNotifications: function(notify_messages) {
|
||||||
|
if (notify_messages != null && notify_messages.length > 0) {
|
||||||
|
for (let i in notify_messages) {
|
||||||
|
let notify_msg = '';
|
||||||
|
if (notify_messages[i].payload != '') {
|
||||||
|
notify_msg = gettext('Asynchronous notification "')
|
||||||
|
+ notify_messages[i].channel
|
||||||
|
+ gettext('" with payload "')
|
||||||
|
+ notify_messages[i].payload
|
||||||
|
+ gettext('" received from server process with PID ')
|
||||||
|
+ notify_messages[i].pid;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notify_msg = gettext('Asynchronous notification "')
|
||||||
|
+ notify_messages[i].channel
|
||||||
|
+ gettext('" received from server process with PID ')
|
||||||
|
+ notify_messages[i].pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
Alertify.info(notify_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add notify messages to the collection.
|
||||||
|
queryToolNotifications.collection.add(notify_messages);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = queryToolNotifications;
|
|
@ -72,6 +72,9 @@ define(['jquery', 'sources/gettext', 'sources/url_for'],
|
||||||
$el.data('panel-visible') !== 'visible' ) {
|
$el.data('panel-visible') !== 'visible' ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sqleditor_obj = target;
|
||||||
|
|
||||||
// Start polling..
|
// Start polling..
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
|
@ -82,6 +85,9 @@ define(['jquery', 'sources/gettext', 'sources/url_for'],
|
||||||
msg = res.data.message,
|
msg = res.data.message,
|
||||||
is_status_changed = false;
|
is_status_changed = false;
|
||||||
|
|
||||||
|
// Raise notify messages comes from database server.
|
||||||
|
sqleditor_obj.update_notifications(res.data.notifies);
|
||||||
|
|
||||||
// Inject CSS as required
|
// Inject CSS as required
|
||||||
switch(status) {
|
switch(status) {
|
||||||
// Busy
|
// Busy
|
||||||
|
|
|
@ -543,10 +543,12 @@ def poll(trans_id):
|
||||||
# There may be additional messages even if result is present
|
# There may be additional messages even if result is present
|
||||||
# eg: Function can provide result as well as RAISE messages
|
# eg: Function can provide result as well as RAISE messages
|
||||||
additional_messages = None
|
additional_messages = None
|
||||||
|
notifies = None
|
||||||
if status == 'Success':
|
if status == 'Success':
|
||||||
messages = conn.messages()
|
messages = conn.messages()
|
||||||
if messages:
|
if messages:
|
||||||
additional_messages = ''.join(messages)
|
additional_messages = ''.join(messages)
|
||||||
|
notifies = conn.get_notifies()
|
||||||
|
|
||||||
# Procedure/Function output may comes in the form of Notices from the
|
# Procedure/Function output may comes in the form of Notices from the
|
||||||
# database server, so we need to append those outputs with the
|
# database server, so we need to append those outputs with the
|
||||||
|
@ -564,6 +566,7 @@ def poll(trans_id):
|
||||||
'rows_fetched_from': rows_fetched_from,
|
'rows_fetched_from': rows_fetched_from,
|
||||||
'rows_fetched_to': rows_fetched_to,
|
'rows_fetched_to': rows_fetched_to,
|
||||||
'additional_messages': additional_messages,
|
'additional_messages': additional_messages,
|
||||||
|
'notifies': notifies,
|
||||||
'has_more_rows': has_more_rows,
|
'has_more_rows': has_more_rows,
|
||||||
'colinfo': columns_info,
|
'colinfo': columns_info,
|
||||||
'primary_keys': primary_keys,
|
'primary_keys': primary_keys,
|
||||||
|
@ -1476,12 +1479,18 @@ def query_tool_status(trans_id):
|
||||||
|
|
||||||
if conn and trans_obj and session_obj:
|
if conn and trans_obj and session_obj:
|
||||||
status = conn.transaction_status()
|
status = conn.transaction_status()
|
||||||
|
|
||||||
|
# Check for the asynchronous notifies statements.
|
||||||
|
conn.check_notifies(True)
|
||||||
|
notifies = conn.get_notifies()
|
||||||
|
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
data={
|
data={
|
||||||
'status': status,
|
'status': status,
|
||||||
'message': gettext(
|
'message': gettext(
|
||||||
CONNECTION_STATUS_MESSAGE_MAPPING.get(status)
|
CONNECTION_STATUS_MESSAGE_MAPPING.get(status),
|
||||||
)
|
),
|
||||||
|
'notifies': notifies
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -20,6 +20,7 @@ define('tools.querytool', [
|
||||||
'react', 'react-dom',
|
'react', 'react-dom',
|
||||||
'sources/keyboard_shortcuts',
|
'sources/keyboard_shortcuts',
|
||||||
'sources/sqleditor/query_tool_actions',
|
'sources/sqleditor/query_tool_actions',
|
||||||
|
'sources/sqleditor/query_tool_notifications',
|
||||||
'pgadmin.datagrid',
|
'pgadmin.datagrid',
|
||||||
'sources/modify_animation',
|
'sources/modify_animation',
|
||||||
'sources/sqleditor/calculate_query_run_time',
|
'sources/sqleditor/calculate_query_run_time',
|
||||||
|
@ -36,8 +37,8 @@ define('tools.querytool', [
|
||||||
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
|
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
|
||||||
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
|
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
|
||||||
HistoryBundle, queryHistory, React, ReactDOM,
|
HistoryBundle, queryHistory, React, ReactDOM,
|
||||||
keyboardShortcuts, queryToolActions, Datagrid, modifyAnimation,
|
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
|
||||||
calculateQueryRunTime, callRenderAfterPoll) {
|
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll) {
|
||||||
/* Return back, this has been called more than once */
|
/* Return back, this has been called more than once */
|
||||||
if (pgAdmin.SqlEditor)
|
if (pgAdmin.SqlEditor)
|
||||||
return pgAdmin.SqlEditor;
|
return pgAdmin.SqlEditor;
|
||||||
|
@ -242,19 +243,32 @@ define('tools.querytool', [
|
||||||
content: '<div id ="history_grid" class="sql-editor-history-container" tabindex: "0"></div>',
|
content: '<div id ="history_grid" class="sql-editor-history-container" tabindex: "0"></div>',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var notifications = new pgAdmin.Browser.Panel({
|
||||||
|
name: 'notifications',
|
||||||
|
title: gettext('Notifications'),
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
isCloseable: false,
|
||||||
|
isPrivate: true,
|
||||||
|
content: '<div id ="notification_grid" class="sql-editor-notifications" tabindex: "0"></div>',
|
||||||
|
});
|
||||||
|
|
||||||
// Load all the created panels
|
// Load all the created panels
|
||||||
data_output.load(main_docker);
|
data_output.load(main_docker);
|
||||||
explain.load(main_docker);
|
explain.load(main_docker);
|
||||||
messages.load(main_docker);
|
messages.load(main_docker);
|
||||||
history.load(main_docker);
|
history.load(main_docker);
|
||||||
|
notifications.load(main_docker);
|
||||||
|
|
||||||
// Add all the panels to the docker
|
// Add all the panels to the docker
|
||||||
self.data_output_panel = main_docker.addPanel('data_output', wcDocker.DOCK.BOTTOM, sql_panel_obj);
|
self.data_output_panel = main_docker.addPanel('data_output', wcDocker.DOCK.BOTTOM, sql_panel_obj);
|
||||||
self.explain_panel = main_docker.addPanel('explain', wcDocker.DOCK.STACKED, self.data_output_panel);
|
self.explain_panel = main_docker.addPanel('explain', wcDocker.DOCK.STACKED, self.data_output_panel);
|
||||||
self.messages_panel = main_docker.addPanel('messages', wcDocker.DOCK.STACKED, self.data_output_panel);
|
self.messages_panel = main_docker.addPanel('messages', wcDocker.DOCK.STACKED, self.data_output_panel);
|
||||||
self.history_panel = main_docker.addPanel('history', wcDocker.DOCK.STACKED, self.data_output_panel);
|
self.history_panel = main_docker.addPanel('history', wcDocker.DOCK.STACKED, self.data_output_panel);
|
||||||
|
self.notifications_panel = main_docker.addPanel('notifications', wcDocker.DOCK.STACKED, self.data_output_panel);
|
||||||
|
|
||||||
self.render_history_grid();
|
self.render_history_grid();
|
||||||
|
queryToolNotifications.renderNotificationsGrid(self.notifications_panel);
|
||||||
|
|
||||||
if (!self.handler.is_new_browser_tab) {
|
if (!self.handler.is_new_browser_tab) {
|
||||||
// Listen on the panel closed event and notify user to save modifications.
|
// Listen on the panel closed event and notify user to save modifications.
|
||||||
|
@ -3832,6 +3846,12 @@ define('tools.querytool', [
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/* This function is used to raise notify messages and update
|
||||||
|
* the notification grid.
|
||||||
|
*/
|
||||||
|
update_notifications: function (notifications) {
|
||||||
|
queryToolNotifications.updateNotifications(notifications);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
pgAdmin.SqlEditor = {
|
pgAdmin.SqlEditor = {
|
||||||
|
|
|
@ -47,6 +47,7 @@ class StartRunningQuery:
|
||||||
transaction_object = pickle.loads(session_obj['command_obj'])
|
transaction_object = pickle.loads(session_obj['command_obj'])
|
||||||
can_edit = False
|
can_edit = False
|
||||||
can_filter = False
|
can_filter = False
|
||||||
|
notifies = None
|
||||||
if transaction_object is not None and session_obj is not None:
|
if transaction_object is not None and session_obj is not None:
|
||||||
# set fetched row count to 0 as we are executing query again.
|
# set fetched row count to 0 as we are executing query again.
|
||||||
transaction_object.update_fetched_row_cnt(0)
|
transaction_object.update_fetched_row_cnt(0)
|
||||||
|
@ -88,6 +89,8 @@ class StartRunningQuery:
|
||||||
can_edit = transaction_object.can_edit()
|
can_edit = transaction_object.can_edit()
|
||||||
can_filter = transaction_object.can_filter()
|
can_filter = transaction_object.can_filter()
|
||||||
|
|
||||||
|
# Get the notifies
|
||||||
|
notifies = conn.get_notifies()
|
||||||
else:
|
else:
|
||||||
status = False
|
status = False
|
||||||
result = gettext(
|
result = gettext(
|
||||||
|
@ -97,7 +100,8 @@ class StartRunningQuery:
|
||||||
'status': status, 'result': result,
|
'status': status, 'result': result,
|
||||||
'can_edit': can_edit, 'can_filter': can_filter,
|
'can_edit': can_edit, 'can_filter': can_filter,
|
||||||
'info_notifier_timeout':
|
'info_notifier_timeout':
|
||||||
self.blueprint_object.info_notifier_timeout.get()
|
self.blueprint_object.info_notifier_timeout.get(),
|
||||||
|
'notifies': notifies
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -117,7 +117,8 @@ class StartRunningQueryTest(BaseTestGenerator):
|
||||||
'not found.',
|
'not found.',
|
||||||
can_edit=False,
|
can_edit=False,
|
||||||
can_filter=False,
|
can_filter=False,
|
||||||
info_notifier_timeout=5
|
info_notifier_timeout=5,
|
||||||
|
notifies=None
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
expect_internal_server_error_called_with=None,
|
expect_internal_server_error_called_with=None,
|
||||||
|
@ -276,7 +277,8 @@ class StartRunningQueryTest(BaseTestGenerator):
|
||||||
result='async function result output',
|
result='async function result output',
|
||||||
can_edit=True,
|
can_edit=True,
|
||||||
can_filter=True,
|
can_filter=True,
|
||||||
info_notifier_timeout=5
|
info_notifier_timeout=5,
|
||||||
|
notifies=None
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
expect_internal_server_error_called_with=None,
|
expect_internal_server_error_called_with=None,
|
||||||
|
@ -319,7 +321,8 @@ class StartRunningQueryTest(BaseTestGenerator):
|
||||||
result='async function result output',
|
result='async function result output',
|
||||||
can_edit=True,
|
can_edit=True,
|
||||||
can_filter=True,
|
can_filter=True,
|
||||||
info_notifier_timeout=5
|
info_notifier_timeout=5,
|
||||||
|
notifies=None
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
expect_internal_server_error_called_with=None,
|
expect_internal_server_error_called_with=None,
|
||||||
|
@ -362,7 +365,8 @@ class StartRunningQueryTest(BaseTestGenerator):
|
||||||
result='async function result output',
|
result='async function result output',
|
||||||
can_edit=True,
|
can_edit=True,
|
||||||
can_filter=True,
|
can_filter=True,
|
||||||
info_notifier_timeout=5
|
info_notifier_timeout=5,
|
||||||
|
notifies=None
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
expect_internal_server_error_called_with=None,
|
expect_internal_server_error_called_with=None,
|
||||||
|
@ -406,7 +410,8 @@ class StartRunningQueryTest(BaseTestGenerator):
|
||||||
result='async function result output',
|
result='async function result output',
|
||||||
can_edit=True,
|
can_edit=True,
|
||||||
can_filter=True,
|
can_filter=True,
|
||||||
info_notifier_timeout=5
|
info_notifier_timeout=5,
|
||||||
|
notifies=None
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
expect_internal_server_error_called_with=None,
|
expect_internal_server_error_called_with=None,
|
||||||
|
@ -511,8 +516,10 @@ class StartRunningQueryTest(BaseTestGenerator):
|
||||||
connect=MagicMock(),
|
connect=MagicMock(),
|
||||||
execute_async=MagicMock(),
|
execute_async=MagicMock(),
|
||||||
execute_void=MagicMock(),
|
execute_void=MagicMock(),
|
||||||
|
get_notifies=MagicMock(),
|
||||||
)
|
)
|
||||||
self.connection.connect.return_value = self.connection_connect_return
|
self.connection.connect.return_value = self.connection_connect_return
|
||||||
|
self.connection.get_notifies.return_value = None
|
||||||
self.connection.execute_async.return_value = \
|
self.connection.execute_async.return_value = \
|
||||||
self.execute_async_return_value
|
self.execute_async_return_value
|
||||||
if self.manager_connection_exception is None:
|
if self.manager_connection_exception is None:
|
||||||
|
|
|
@ -16,6 +16,7 @@ object.
|
||||||
import random
|
import random
|
||||||
import select
|
import select
|
||||||
import sys
|
import sys
|
||||||
|
import datetime
|
||||||
from collections import deque
|
from collections import deque
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
@ -136,6 +137,13 @@ class Connection(BaseConnection):
|
||||||
formatted error message if flag is set to true else return
|
formatted error message if flag is set to true else return
|
||||||
normal error message.
|
normal error message.
|
||||||
|
|
||||||
|
* check_notifies(required_polling)
|
||||||
|
- Check for the notify messages by polling the connection or after
|
||||||
|
execute is there in notifies.
|
||||||
|
|
||||||
|
* get_notifies()
|
||||||
|
- This function will returns list of notifies received from database
|
||||||
|
server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
|
def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
|
||||||
|
@ -155,6 +163,7 @@ class Connection(BaseConnection):
|
||||||
self.execution_aborted = False
|
self.execution_aborted = False
|
||||||
self.row_count = 0
|
self.row_count = 0
|
||||||
self.__notices = None
|
self.__notices = None
|
||||||
|
self.__notifies = None
|
||||||
self.password = None
|
self.password = None
|
||||||
# This flag indicates the connection status (connected/disconnected).
|
# This flag indicates the connection status (connected/disconnected).
|
||||||
self.wasConnected = False
|
self.wasConnected = False
|
||||||
|
@ -891,6 +900,7 @@ WHERE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.__notices = []
|
self.__notices = []
|
||||||
|
self.__notifies = []
|
||||||
self.execution_aborted = False
|
self.execution_aborted = False
|
||||||
cur.execute(query, params)
|
cur.execute(query, params)
|
||||||
res = self._wait_timeout(cur.connection)
|
res = self._wait_timeout(cur.connection)
|
||||||
|
@ -908,6 +918,9 @@ WHERE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for the asynchronous notifies.
|
||||||
|
self.check_notifies()
|
||||||
|
|
||||||
if self.is_disconnected(pe):
|
if self.is_disconnected(pe):
|
||||||
raise ConnectionLost(
|
raise ConnectionLost(
|
||||||
self.manager.sid,
|
self.manager.sid,
|
||||||
|
@ -1366,6 +1379,9 @@ Failed to reset the connection to the server due to following error:
|
||||||
self.__notices.extend(self.conn.notices)
|
self.__notices.extend(self.conn.notices)
|
||||||
self.conn.notices.clear()
|
self.conn.notices.clear()
|
||||||
|
|
||||||
|
# Check for the asynchronous notifies.
|
||||||
|
self.check_notifies()
|
||||||
|
|
||||||
# We also need to fetch notices before we return from function in case
|
# We also need to fetch notices before we return from function in case
|
||||||
# of any Exception, To avoid code duplication we will return after
|
# of any Exception, To avoid code duplication we will return after
|
||||||
# fetching the notices in case of any Exception
|
# fetching the notices in case of any Exception
|
||||||
|
@ -1542,6 +1558,21 @@ Failed to reset the connection to the server due to following error:
|
||||||
resp = []
|
resp = []
|
||||||
while self.__notices:
|
while self.__notices:
|
||||||
resp.append(self.__notices.pop(0))
|
resp.append(self.__notices.pop(0))
|
||||||
|
|
||||||
|
for notify in self.__notifies:
|
||||||
|
if notify.payload is not None and notify.payload is not '':
|
||||||
|
notify_msg = gettext(
|
||||||
|
"Asynchronous notification \"{0}\" with payload \"{1}\" "
|
||||||
|
"received from server process with PID {2}\n"
|
||||||
|
).format(notify.channel, notify.payload, notify.pid)
|
||||||
|
|
||||||
|
else:
|
||||||
|
notify_msg = gettext(
|
||||||
|
"Asynchronous notification \"{0}\" received from "
|
||||||
|
"server process with PID {1}\n"
|
||||||
|
).format(notify.channel, notify.pid)
|
||||||
|
resp.append(notify_msg)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def decode_to_utf8(self, value):
|
def decode_to_utf8(self, value):
|
||||||
|
@ -1711,3 +1742,34 @@ Failed to reset the connection to the server due to following error:
|
||||||
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def check_notifies(self, required_polling=False):
|
||||||
|
"""
|
||||||
|
Check for the notify messages by polling the connection or after
|
||||||
|
execute is there in notifies.
|
||||||
|
"""
|
||||||
|
if self.conn and required_polling:
|
||||||
|
self.conn.poll()
|
||||||
|
|
||||||
|
if self.conn and hasattr(self.conn, 'notifies') and \
|
||||||
|
len(self.conn.notifies) > 0:
|
||||||
|
self.__notifies.extend(self.conn.notifies)
|
||||||
|
self.conn.notifies = []
|
||||||
|
else:
|
||||||
|
self.__notifies = []
|
||||||
|
|
||||||
|
def get_notifies(self):
|
||||||
|
"""
|
||||||
|
This function will returns list of notifies received from database
|
||||||
|
server.
|
||||||
|
"""
|
||||||
|
notifies = None
|
||||||
|
# Convert list of Notify objects into list of Dict.
|
||||||
|
if self.__notifies is not None and len(self.__notifies) > 0:
|
||||||
|
notifies = [{'recorded_time': str(datetime.datetime.now()),
|
||||||
|
'channel': notify.channel,
|
||||||
|
'payload': notify.payload,
|
||||||
|
'pid': notify.pid
|
||||||
|
} for notify in self.__notifies
|
||||||
|
]
|
||||||
|
return notifies
|
||||||
|
|
|
@ -43,6 +43,7 @@ describe('ExecuteQuery', () => {
|
||||||
'saveState',
|
'saveState',
|
||||||
'initTransaction',
|
'initTransaction',
|
||||||
'handle_connection_lost',
|
'handle_connection_lost',
|
||||||
|
'update_notifications',
|
||||||
]);
|
]);
|
||||||
sqlEditorMock.transId = 123;
|
sqlEditorMock.transId = 123;
|
||||||
sqlEditorMock.rows_affected = 1000;
|
sqlEditorMock.rows_affected = 1000;
|
||||||
|
@ -76,7 +77,7 @@ describe('ExecuteQuery', () => {
|
||||||
describe('when query was successful', () => {
|
describe('when query was successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
response = {
|
response = {
|
||||||
data: {status: 'Success'},
|
data: {status: 'Success', notifies: [{'pid': 100}]},
|
||||||
};
|
};
|
||||||
networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response);
|
networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response);
|
||||||
|
|
||||||
|
@ -97,7 +98,15 @@ describe('ExecuteQuery', () => {
|
||||||
it('should render the results', (done) => {
|
it('should render the results', (done) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(sqlEditorMock.call_render_after_poll)
|
expect(sqlEditorMock.call_render_after_poll)
|
||||||
.toHaveBeenCalledWith({status: 'Success'});
|
.toHaveBeenCalledWith({status: 'Success', notifies: [{'pid': 100}]});
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the notification panel', (done) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(sqlEditorMock.update_notifications)
|
||||||
|
.toHaveBeenCalledWith([{'pid': 100}]);
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue