diff --git a/web/pgadmin/static/js/sqleditor/keyboard_shortcuts.js b/web/pgadmin/static/js/sqleditor/keyboard_shortcuts.js index 5d947d19..d31c6bed 100644 --- a/web/pgadmin/static/js/sqleditor/keyboard_shortcuts.js +++ b/web/pgadmin/static/js/sqleditor/keyboard_shortcuts.js @@ -2,10 +2,9 @@ const F5_KEY = 116, F7_KEY = 118, F8_KEY = 119, PERIOD_KEY = 190, - FWD_SLASH_KEY = 191, - IS_CMD_KEY = window.navigator.platform.search('Mac') != -1; + FWD_SLASH_KEY = 191; -function keyboardShortcuts(sqlEditorController, event) { +function keyboardShortcuts(sqlEditorController, queryToolActions, event) { if (sqlEditorController.isQueryRunning()) { return; } @@ -14,28 +13,28 @@ function keyboardShortcuts(sqlEditorController, event) { if (keyCode === F5_KEY) { event.preventDefault(); - sqlEditorController.executeQuery(); + queryToolActions.executeQuery(sqlEditorController); } else if (event.shiftKey && keyCode === F7_KEY) { _stopEventPropagation(); - sqlEditorController.explainAnalyze(event); + queryToolActions.explainAnalyze(sqlEditorController); } else if (keyCode === F7_KEY) { _stopEventPropagation(); - sqlEditorController.explain(event); + queryToolActions.explain(sqlEditorController); } else if (keyCode === F8_KEY) { event.preventDefault(); - sqlEditorController.download(); - } else if (((IS_CMD_KEY && event.metaKey) || (!IS_CMD_KEY && event.ctrlKey)) && - event.shiftKey && keyCode === FWD_SLASH_KEY) { + queryToolActions.download(sqlEditorController); + } else if (((this.isMac() && event.metaKey) || (!this.isMac() && event.ctrlKey)) && + event.shiftKey && keyCode === FWD_SLASH_KEY) { _stopEventPropagation(); - sqlEditorController.commentLineCode(); - } else if (((IS_CMD_KEY && event.metaKey) || (!IS_CMD_KEY && event.ctrlKey)) && - event.shiftKey && keyCode === PERIOD_KEY) { + queryToolActions.commentLineCode(sqlEditorController); + } else if (((this.isMac() && event.metaKey) || (!this.isMac() && event.ctrlKey)) && + event.shiftKey && keyCode === PERIOD_KEY) { _stopEventPropagation(); - sqlEditorController.uncommentLineCode(); - } else if (((IS_CMD_KEY && event.metaKey) || (!IS_CMD_KEY && event.ctrlKey)) && - keyCode === FWD_SLASH_KEY) { + queryToolActions.uncommentLineCode(sqlEditorController); + } else if (((this.isMac() && event.metaKey) || (!this.isMac() && event.ctrlKey)) && + keyCode === FWD_SLASH_KEY) { _stopEventPropagation(); - sqlEditorController.commentBlockCode(); + queryToolActions.commentBlockCode(sqlEditorController); } function _stopEventPropagation() { @@ -46,6 +45,11 @@ function keyboardShortcuts(sqlEditorController, event) { } } +function isMac() { + return window.navigator.platform.search('Mac') != -1; +} + module.exports = { processEvent: keyboardShortcuts, + isMac: isMac, }; diff --git a/web/pgadmin/static/js/sqleditor/query_tool_actions.js b/web/pgadmin/static/js/sqleditor/query_tool_actions.js new file mode 100644 index 00000000..a376f017 --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/query_tool_actions.js @@ -0,0 +1,99 @@ +import $ from 'jquery'; + +let queryToolActions = { + _verbose: function () { + return $('.explain-verbose').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + }, + + _costsEnabled: function () { + return $('.explain-costs').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + }, + + _buffers: function () { + return $('.explain-buffers').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + }, + + _timing: function () { + return $('.explain-timing').hasClass('visibility-hidden') ? 'OFF' : 'ON'; + }, + + _clearMessageTab: function () { + $('.sql-editor-message').html(''); + }, + + executeQuery: function (sqlEditorController) { + if(sqlEditorController.is_query_tool) { + this._clearMessageTab(); + sqlEditorController.execute(); + } else { + sqlEditorController.execute_data_query(); + } + }, + + explainAnalyze: function (sqlEditorController) { + let costEnabled = this._costsEnabled(); + let verbose = this._verbose(); + let buffers = this._buffers(); + let timing = this._timing(); + let explainAnalyzeQuery = `EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE ${verbose}, COSTS ${costEnabled}, BUFFERS ${buffers}, TIMING ${timing}) `; + sqlEditorController.execute(explainAnalyzeQuery); + }, + + explain: function (sqlEditorController) { + let costEnabled = this._costsEnabled(); + let verbose = this._verbose(); + + let explainQuery = `EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE ${verbose}, COSTS ${costEnabled}, BUFFERS OFF, TIMING OFF) `; + sqlEditorController.execute(explainQuery); + }, + + download: function (sqlEditorController) { + let sqlQuery = sqlEditorController.gridView.query_tool_obj.getSelection(); + + if (!sqlQuery) { + sqlQuery = sqlEditorController.gridView.query_tool_obj.getValue(); + } + + if (!sqlQuery) return; + + let filename = 'data-' + new Date().getTime() + '.csv'; + + if (!sqlEditorController.is_query_tool) { + filename = sqlEditorController.table_name + '.csv'; + } + + sqlEditorController.trigger_csv_download(sqlQuery, filename); + }, + + commentBlockCode: function (sqlEditorController) { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + if (!codeMirrorObj.getValue()) return; + + codeMirrorObj.toggleComment(codeMirrorObj.getCursor(true), codeMirrorObj.getCursor(false)); + }, + + commentLineCode: function (sqlEditorController) { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + if (!codeMirrorObj.getValue()) return; + + codeMirrorObj.lineComment(codeMirrorObj.getCursor(true), + codeMirrorObj.getCursor(false), + {lineComment: '--'} + ); + }, + + uncommentLineCode: function (sqlEditorController) { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + if (!codeMirrorObj.getValue()) return; + + codeMirrorObj.uncomment(codeMirrorObj.getCursor(true), + codeMirrorObj.getCursor(false), + {lineComment: '--'} + ); + }, +}; + +module.exports = queryToolActions; diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 3e61f104..d2c32e26 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -16,18 +16,19 @@ define('tools.querytool', [ 'sources/../jsx/history/query_history', 'react', 'react-dom', 'sources/sqleditor/keyboard_shortcuts', - 'sources/../bundle/slickgrid', + 'sources/sqleditor/query_tool_actions', + 'sources/../bundle/slickgrid', 'pgadmin.file_manager', 'backgrid.sizeable.columns', 'slick.pgadmin.formatters', 'slick.pgadmin.editors', 'pgadmin.browser' ], function( - babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror, + babelPollyfill,gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror, pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, XCellSelectionModel, setStagedRows, SqlEditorUtils, HistoryBundle, queryHistory, React, ReactDOM, keyboardShortcuts -) { +, queryToolActions) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) return pgAdmin.SqlEditor; @@ -36,512 +37,513 @@ define('tools.querytool', [ // Generally the one, which do no have AMD support. var wcDocker = window.wcDocker, pgBrowser = pgAdmin.Browser, - CodeMirror = codemirror.default, - Slick = window.Slick; - - var is_query_running = false; - - // Defining Backbone view for the sql grid. - var SQLEditorView = Backbone.View.extend({ - initialize: function(opts) { - this.$el = opts.el; - this.handler = opts.handler; - this.handler['col_size'] = {}; - }, - - // Bind all the events - events: { - "click .btn-load-file": "on_file_load", - "click #btn-save": "on_save", - "click #btn-file-menu-save": "on_save", - "click #btn-file-menu-save-as": "on_save_as", - "click #btn-find": "on_find", - "click #btn-find-menu-find": "on_find", - "click #btn-find-menu-find-next": "on_find_next", - "click #btn-find-menu-find-previous": "on_find_previous", - "click #btn-find-menu-replace": "on_replace", - "click #btn-find-menu-replace-all": "on_replace_all", - "click #btn-find-menu-find-persistent": "on_find_persistent", - "click #btn-find-menu-jump": "on_jump", - "click #btn-delete-row": "on_delete", - "click #btn-filter": "on_show_filter", - "click #btn-filter-menu": "on_show_filter", - "click #btn-include-filter": "on_include_filter", - "click #btn-exclude-filter": "on_exclude_filter", - "click #btn-remove-filter": "on_remove_filter", - "click #btn-apply": "on_apply", - "click #btn-cancel": "on_cancel", - "click #btn-copy-row": "on_copy_row", - "click #btn-paste-row": "on_paste_row", - "click #btn-flash": "on_flash", - "click #btn-flash-menu": "on_flash", - "click #btn-cancel-query": "on_cancel_query", - "click #btn-download": "on_download", - "click #btn-edit": "on_clear", - "click #btn-clear": "on_clear", - "click #btn-auto-commit": "on_auto_commit", - "click #btn-auto-rollback": "on_auto_rollback", - "click #btn-clear-history": "on_clear_history", - "click .noclose": 'do_not_close_menu', - "click #btn-explain": "on_explain", - "click #btn-explain-analyze": "on_explain_analyze", - "click #btn-explain-verbose": "on_explain_verbose", - "click #btn-explain-costs": "on_explain_costs", - "click #btn-explain-buffers": "on_explain_buffers", - "click #btn-explain-timing": "on_explain_timing", - "change .limit": "on_limit_change", - "keydown": "keyAction", - // Comment options - "click #btn-comment-code": "on_toggle_comment_block_code", - "click #btn-toggle-comment-block": "on_toggle_comment_block_code", - "click #btn-comment-line": "on_comment_line_code", - "click #btn-uncomment-line": "on_uncomment_line_code", - // Indentation options - "click #btn-indent-code": "on_indent_code", - "click #btn-unindent-code": "on_unindent_code" - }, - - // This function is used to render the template. - render: function() { - var self = this, - filter = self.$el.find('#sql_filter'); - - $('.editor-title').text(_.unescape(self.editor_title)); - self.filter_obj = CodeMirror.fromTextArea(filter.get(0), { - lineNumbers: true, - indentUnit: 4, - mode: "text/x-pgsql", - foldOptions: { - widget: "\u2026" - }, - foldGutter: { - rangeFinder: CodeMirror.fold.combine(CodeMirror.pgadminBeginRangeFinder, CodeMirror.pgadminIfRangeFinder, - CodeMirror.pgadminLoopRangeFinder, CodeMirror.pgadminCaseRangeFinder) - }, - gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], - extraKeys: pgBrowser.editor_shortcut_keys, - tabSize: pgAdmin.Browser.editor_options.tabSize, - lineWrapping: pgAdmin.Browser.editor_options.wrapCode, - autoCloseBrackets: pgAdmin.Browser.editor_options.insert_pair_brackets, - matchBrackets: pgAdmin.Browser.editor_options.brace_matching - }); - - // Create main wcDocker instance - var main_docker = new wcDocker( - '#editor-panel', { + CodeMirror = codemirror.default,Slick = window.Slick; + + var is_query_running = false; + + // Defining Backbone view for the sql grid. + var SQLEditorView = Backbone.View.extend({ + initialize: function (opts) { + this.$el = opts.el; + this.handler = opts.handler; + this.handler['col_size'] = {}; + }, + + // Bind all the events + events: { + "click .btn-load-file": "on_file_load", + "click #btn-save": "on_save", + "click #btn-file-menu-save": "on_save", + "click #btn-file-menu-save-as": "on_save_as", + "click #btn-find": "on_find", + "click #btn-find-menu-find": "on_find", + "click #btn-find-menu-find-next": "on_find_next", + "click #btn-find-menu-find-previous": "on_find_previous", + "click #btn-find-menu-replace": "on_replace", + "click #btn-find-menu-replace-all": "on_replace_all", + "click #btn-find-menu-find-persistent": "on_find_persistent", + "click #btn-find-menu-jump": "on_jump", + "click #btn-delete-row": "on_delete", + "click #btn-filter": "on_show_filter", + "click #btn-filter-menu": "on_show_filter", + "click #btn-include-filter": "on_include_filter", + "click #btn-exclude-filter": "on_exclude_filter", + "click #btn-remove-filter": "on_remove_filter", + "click #btn-apply": "on_apply", + "click #btn-cancel": "on_cancel", + "click #btn-copy-row": "on_copy_row", + "click #btn-paste-row": "on_paste_row", + "click #btn-flash": "on_flash", + "click #btn-flash-menu": "on_flash", + "click #btn-cancel-query": "on_cancel_query", + "click #btn-download": "on_download", + "click #btn-edit": "on_clear", + "click #btn-clear": "on_clear", + "click #btn-auto-commit": "on_auto_commit", + "click #btn-auto-rollback": "on_auto_rollback", + "click #btn-clear-history": "on_clear_history", + "click .noclose": 'do_not_close_menu', + "click #btn-explain": "on_explain", + "click #btn-explain-analyze": "on_explain_analyze", + "click #btn-explain-verbose": "on_explain_verbose", + "click #btn-explain-costs": "on_explain_costs", + "click #btn-explain-buffers": "on_explain_buffers", + "click #btn-explain-timing": "on_explain_timing", + "change .limit": "on_limit_change", + "keydown": "keyAction", + // Comment options + "click #btn-comment-code": "on_toggle_comment_block_code", + "click #btn-toggle-comment-block": "on_toggle_comment_block_code", + "click #btn-comment-line": "on_comment_line_code", + "click #btn-uncomment-line": "on_uncomment_line_code", + // Indentation options + "click #btn-indent-code": "on_indent_code", + "click #btn-unindent-code": "on_unindent_code" + }, + + // This function is used to render the template. + render: function () { + var self = this, + filter = self.$el.find('#sql_filter'); + + $('.editor-title').text(_.unescape(self.editor_title)); + self.filter_obj = CodeMirror.fromTextArea(filter.get(0), { + lineNumbers: true, + indentUnit: 4, + mode: "text/x-pgsql", + foldOptions: { + widget: "\u2026" + }, + foldGutter: { + rangeFinder: CodeMirror.fold.combine(CodeMirror.pgadminBeginRangeFinder, CodeMirror.pgadminIfRangeFinder, + CodeMirror.pgadminLoopRangeFinder, CodeMirror.pgadminCaseRangeFinder) + }, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + extraKeys: pgBrowser.editor_shortcut_keys, + tabSize: pgAdmin.Browser.editor_options.tabSize, + lineWrapping: pgAdmin.Browser.editor_options.wrapCode, + autoCloseBrackets: pgAdmin.Browser.editor_options.insert_pair_brackets, + matchBrackets: pgAdmin.Browser.editor_options.brace_matching + }); + + // Create main wcDocker instance + var main_docker = new wcDocker( + '#editor-panel', { allowContextMenu: false, allowCollapse: false, themePath: url_for('static', {'filename': 'css'}), theme: 'webcabin.overrides.css' }); - var sql_panel = new pgAdmin.Browser.Panel({ - name: 'sql_panel', - title: false, - width: '100%', - height:'20%', - isCloseable: false, - isPrivate: true + var sql_panel = new pgAdmin.Browser.Panel({ + name: 'sql_panel', + title: false, + width: '100%', + height: '20%', + isCloseable: false, + isPrivate: true + }); + + sql_panel.load(main_docker); + var sql_panel_obj = main_docker.addPanel('sql_panel', wcDocker.DOCK.TOP); + + var text_container = $(''); + var output_container = $('
').append(text_container); + sql_panel_obj.$container.find('.pg-panel-content').append(output_container); + + self.query_tool_obj = CodeMirror.fromTextArea(text_container.get(0), { + lineNumbers: true, + indentUnit: 4, + styleSelectedText: true, + mode: "text/x-pgsql", + foldOptions: { + widget: "\u2026" + }, + foldGutter: { + rangeFinder: CodeMirror.fold.combine(CodeMirror.pgadminBeginRangeFinder, CodeMirror.pgadminIfRangeFinder, + CodeMirror.pgadminLoopRangeFinder, CodeMirror.pgadminCaseRangeFinder) + }, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + extraKeys: pgBrowser.editor_shortcut_keys, + tabSize: pgAdmin.Browser.editor_options.tabSize, + lineWrapping: pgAdmin.Browser.editor_options.wrapCode, + scrollbarStyle: 'simple', + autoCloseBrackets: pgAdmin.Browser.editor_options.insert_pair_brackets, + matchBrackets: pgAdmin.Browser.editor_options.brace_matching + }); + + // Refresh Code mirror on SQL panel resize to + // display its value properly + sql_panel_obj.on(wcDocker.EVENT.RESIZE_ENDED, function () { + setTimeout(function () { + if (self && self.query_tool_obj) { + self.query_tool_obj.refresh(); + } + }, 200); + }); + + // Create panels for 'Data Output', 'Explain', 'Messages' and 'History' + var data_output = new pgAdmin.Browser.Panel({ + name: 'data_output', + title: gettext("Data Output"), + width: '100%', + height: '100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + var explain = new pgAdmin.Browser.Panel({ + name: 'explain', + title: gettext("Explain"), + width: '100%', + height: '100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + var messages = new pgAdmin.Browser.Panel({ + name: 'messages', + title: gettext("Messages"), + width: '100%', + height: '100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + var history = new pgAdmin.Browser.Panel({ + name: 'history', + title: gettext("History"), + width: '100%', + height: '100%', + isCloseable: false, + isPrivate: true, + content: '
' + }) + + // Load all the created panels + data_output.load(main_docker); + explain.load(main_docker); + messages.load(main_docker); + history.load(main_docker); + + // Add all the panels to the docker + 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.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.render_history_grid(); + + if (!self.handler.is_new_browser_tab) { + // Listen on the panel closed event and notify user to save modifications. + _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function (p) { + if (p.isVisible()) { + p.on(wcDocker.EVENT.CLOSING, function () { + // Only if we can edit data then perform this check + var notify = false, msg; + if (self.handler.can_edit) { + var data_store = self.handler.data_store; + if (data_store && (_.size(data_store.added) || + _.size(data_store.updated))) { + msg = gettext("The data has changed. Do you want to save changes?"); + notify = true; + } + } else if (self.handler.is_query_tool && self.handler.is_query_changed) { + msg = gettext("The text has changed. Do you want to save changes?"); + notify = true; + } + if (notify) { + return self.user_confirmation(p, msg); + } + return true; + }); + // Set focus on query tool of active panel + p.on(wcDocker.EVENT.GAIN_FOCUS, function () { + if (!$(p.$container).hasClass('wcPanelTabContentHidden')) { + setTimeout(function () { + self.handler.gridView.query_tool_obj.focus(); + }, 200); + } + }); + } }); + } - sql_panel.load(main_docker); - var sql_panel_obj = main_docker.addPanel('sql_panel', wcDocker.DOCK.TOP); + // set focus on query tool once loaded + setTimeout(function () { + self.query_tool_obj.focus(); + }, 500); - var text_container = $(''); - var output_container = $('
').append(text_container); - sql_panel_obj.$container.find('.pg-panel-content').append(output_container); + /* We have override/register the hint function of CodeMirror + * to provide our own hint logic. + */ + CodeMirror.registerHelper("hint", "sql", function (editor, options) { + var data = [], + doc = editor.getDoc(), + cur = doc.getCursor(), + // Get the current cursor position + current_cur = cur.ch, + // function context + ctx = { + editor: editor, + // URL for auto-complete + url: url_for('sqleditor.autocomplete', {'trans_id': self.transId}), + data: data, + // Get the line number in the cursor position + current_line: cur.line, + /* + * Render function for hint to add our own class + * and icon as per the object type. + */ + hint_render: function (elt, data, cur) { + var el = document.createElement('span'); - self.query_tool_obj = CodeMirror.fromTextArea(text_container.get(0), { - lineNumbers: true, - indentUnit: 4, - styleSelectedText: true, - mode: "text/x-pgsql", - foldOptions: { - widget: "\u2026" - }, - foldGutter: { - rangeFinder: CodeMirror.fold.combine(CodeMirror.pgadminBeginRangeFinder, CodeMirror.pgadminIfRangeFinder, - CodeMirror.pgadminLoopRangeFinder, CodeMirror.pgadminCaseRangeFinder) - }, - gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], - extraKeys: pgBrowser.editor_shortcut_keys, - tabSize: pgAdmin.Browser.editor_options.tabSize, - lineWrapping: pgAdmin.Browser.editor_options.wrapCode, - scrollbarStyle: 'simple', - autoCloseBrackets: pgAdmin.Browser.editor_options.insert_pair_brackets, - matchBrackets: pgAdmin.Browser.editor_options.brace_matching - }); + switch (cur.type) { + case 'database': + el.className = 'sqleditor-hint pg-icon-' + cur.type; + break; + case 'datatype': + el.className = 'sqleditor-hint icon-type'; + break; + case 'keyword': + el.className = 'fa fa-key'; + break; + case 'table alias': + el.className = 'fa fa-at'; + break; + default: + el.className = 'sqleditor-hint icon-' + cur.type; + } - // Refresh Code mirror on SQL panel resize to - // display its value properly - sql_panel_obj.on(wcDocker.EVENT.RESIZE_ENDED, function() { - setTimeout(function() { - if(self && self.query_tool_obj) { - self.query_tool_obj.refresh(); + el.appendChild(document.createTextNode(cur.text)); + elt.appendChild(el); } - }, 200); - }); + }; - // Create panels for 'Data Output', 'Explain', 'Messages' and 'History' - var data_output = new pgAdmin.Browser.Panel({ - name: 'data_output', - title: gettext("Data Output"), - width: '100%', - height:'100%', - isCloseable: false, - isPrivate: true, - content: '
' - }) - - var explain = new pgAdmin.Browser.Panel({ - name: 'explain', - title: gettext("Explain"), - width: '100%', - height:'100%', - isCloseable: false, - isPrivate: true, - content: '
' - }) - - var messages = new pgAdmin.Browser.Panel({ - name: 'messages', - title: gettext("Messages"), - width: '100%', - height:'100%', - isCloseable: false, - isPrivate: true, - content: '
' - }) - - var history = new pgAdmin.Browser.Panel({ - name: 'history', - title: gettext("History"), - width: '100%', - height:'100%', - isCloseable: false, - isPrivate: true, - content: '
' - }) - - // Load all the created panels - data_output.load(main_docker); - explain.load(main_docker); - messages.load(main_docker); - history.load(main_docker); - - // Add all the panels to the docker - 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.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.render_history_grid(); - - if (!self.handler.is_new_browser_tab) { - // Listen on the panel closed event and notify user to save modifications. - _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function(p) { - if(p.isVisible()) { - p.on(wcDocker.EVENT.CLOSING, function() { - // Only if we can edit data then perform this check - var notify = false, msg; - if(self.handler.can_edit) { - var data_store = self.handler.data_store; - if(data_store && (_.size(data_store.added) || - _.size(data_store.updated))) { - msg = gettext("The data has changed. Do you want to save changes?"); - notify = true; - } - } else if(self.handler.is_query_tool && self.handler.is_query_changed) { - msg = gettext("The text has changed. Do you want to save changes?"); - notify = true; - } - if(notify) {return self.user_confirmation(p, msg);} - return true; - }); - // Set focus on query tool of active panel - p.on(wcDocker.EVENT.GAIN_FOCUS, function() { - if (!$(p.$container).hasClass('wcPanelTabContentHidden')) { - setTimeout(function() { - self.handler.gridView.query_tool_obj.focus(); - }, 200); - } - }); - } - }); - } + data.push(doc.getValue()); + // Get the text from start to the current cursor position. + data.push( + doc.getRange( + {line: 0, ch: 0}, + {line: ctx.current_line, ch: current_cur} + ) + ); - // set focus on query tool once loaded - setTimeout(function() { - self.query_tool_obj.focus(); - }, 500); + return { + then: function (cb) { + var self = this; + // Make ajax call to find the autocomplete data + $.ajax({ + url: self.url, + method: 'POST', + contentType: "application/json", + data: JSON.stringify(self.data), + success: function (res) { + var result = []; + + _.each(res.data.result, function (obj, key) { + result.push({ + text: key, type: obj.object_type, + render: self.hint_render + }); + }); + + // Sort function to sort the suggestion's alphabetically. + result.sort(function (a, b) { + var textA = a.text.toLowerCase(), textB = b.text.toLowerCase(); + if (textA < textB) //sort string ascending + return -1; + if (textA > textB) + return 1; + return 0; //default return value (no sorting) + }); - /* We have override/register the hint function of CodeMirror - * to provide our own hint logic. - */ - CodeMirror.registerHelper("hint", "sql", function(editor, options) { - var data = [], - doc = editor.getDoc(), - cur = doc.getCursor(), - // Get the current cursor position - current_cur = cur.ch, - // function context - ctx = { - editor: editor, - // URL for auto-complete - url: url_for('sqleditor.autocomplete', {'trans_id': self.transId}), - data: data, - // Get the line number in the cursor position - current_line: cur.line, /* - * Render function for hint to add our own class - * and icon as per the object type. + * Below logic find the start and end point + * to replace the selected auto complete suggestion. */ - hint_render: function(elt, data, cur) { - var el = document.createElement('span'); - - switch(cur.type) { - case 'database': - el.className = 'sqleditor-hint pg-icon-' + cur.type; - break; - case 'datatype': - el.className = 'sqleditor-hint icon-type'; - break; - case 'keyword': - el.className = 'fa fa-key'; - break; - case 'table alias': - el.className = 'fa fa-at'; - break; - default: - el.className = 'sqleditor-hint icon-' + cur.type; - } - - el.appendChild(document.createTextNode(cur.text)); - elt.appendChild(el); + var token = self.editor.getTokenAt(cur), start, end, search; + if (token.end > cur.ch) { + token.end = cur.ch; + token.string = token.string.slice(0, cur.ch - token.start); } - }; - - data.push(doc.getValue()); - // Get the text from start to the current cursor position. - data.push( - doc.getRange( - { line: 0, ch: 0 }, - { line: ctx.current_line, ch: current_cur } - ) - ); - - return { - then: function(cb) { - var self = this; - // Make ajax call to find the autocomplete data - $.ajax({ - url: self.url, - method: 'POST', - contentType: "application/json", - data: JSON.stringify(self.data), - success: function(res) { - var result = []; - - _.each(res.data.result, function(obj, key) { - result.push({ - text: key, type: obj.object_type, - render: self.hint_render - }); - }); - - // Sort function to sort the suggestion's alphabetically. - result.sort(function(a, b){ - var textA = a.text.toLowerCase(), textB = b.text.toLowerCase(); - if (textA < textB) //sort string ascending - return -1; - if (textA > textB) - return 1; - return 0; //default return value (no sorting) - }); - - /* - * Below logic find the start and end point - * to replace the selected auto complete suggestion. - */ - var token = self.editor.getTokenAt(cur), start, end, search; - if (token.end > cur.ch) { - token.end = cur.ch; - token.string = token.string.slice(0, cur.ch - token.start); - } - - if (token.string.match(/^[.`\w@]\w*$/)) { - search = token.string; - start = token.start; - end = token.end; - } else { - start = end = cur.ch; - search = ""; - } - /* - * Added 1 in the start position if search string - * started with "." or "`" else auto complete of code mirror - * will remove the "." when user select any suggestion. - */ - if (search.charAt(0) == "." || search.charAt(0) == "``") - start += 1; - - cb({ - list: result, - from: {line: self.current_line, ch: start }, - to: { line: self.current_line, ch: end } - }); + if (token.string.match(/^[.`\w@]\w*$/)) { + search = token.string; + start = token.start; + end = token.end; + } else { + start = end = cur.ch; + search = ""; } - }); - }.bind(ctx) - }; - }); - }, - /* To prompt user for unsaved changes */ - user_confirmation: function(panel, msg) { - // If there is anything to save then prompt user - var that = this; - - alertify.confirmSave || alertify.dialog('confirmSave', function() { - return { - main: function(title, message) { - var content = '
' - + gettext('The text has changed. Do you want to save changes?') - + '
'; - this.setHeader(title); - this.setContent(message); - }, - setup: function () { - return { - buttons: [ - { - text: gettext('Save'), - className: 'btn btn-primary', - },{ - text: gettext('Don\'t save'), - className: 'btn btn-danger', - },{ - text: gettext('Cancel'), - key: 27, // ESC - invokeOnClose: true, - className: 'btn btn-warning', - } - ], - focus: { - element: 0, - select: false - }, - options: { - maximizable: false, - resizable: false + /* + * Added 1 in the start position if search string + * started with "." or "`" else auto complete of code mirror + * will remove the "." when user select any suggestion. + */ + if (search.charAt(0) == "." || search.charAt(0) == "``") + start += 1; + + cb({ + list: result, + from: {line: self.current_line, ch: start}, + to: {line: self.current_line, ch: end} + }); + } + }); + }.bind(ctx) + }; + }); + }, + + /* To prompt user for unsaved changes */ + user_confirmation: function (panel, msg) { + // If there is anything to save then prompt user + var that = this; + + alertify.confirmSave || alertify.dialog('confirmSave', function () { + return { + main: function (title, message) { + var content = '
' + + gettext('The text has changed. Do you want to save changes?') + + '
'; + this.setHeader(title); + this.setContent(message); + }, + setup: function () { + return { + buttons: [ + { + text: gettext('Save'), + className: 'btn btn-primary', + }, { + text: gettext('Don\'t save'), + className: 'btn btn-danger', + }, { + text: gettext('Cancel'), + key: 27, // ESC + invokeOnClose: true, + className: 'btn btn-warning', } - }; - }, - callback: function (closeEvent) { - switch (closeEvent.index) { - case 0: // Save - that.handler.close_on_save = true; - that.handler._save(that, that.handler); - break; - case 1: // Don't Save - that.handler.close_on_save = false; - that.handler.close(); - break; - case 2: //Cancel - //Do nothing. - break; + ], + focus: { + element: 0, + select: false + }, + options: { + maximizable: false, + resizable: false } + }; + }, + callback: function (closeEvent) { + switch (closeEvent.index) { + case 0: // Save + that.handler.close_on_save = true; + that.handler._save(that, that.handler); + break; + case 1: // Don't Save + that.handler.close_on_save = false; + that.handler.close(); + break; + case 2: //Cancel + //Do nothing. + break; } - }; - }); - alertify.confirmSave(gettext("Save changes?"), msg); - return false; - }, - - /* Regarding SlickGrid usage in render_grid function. - - SlickGrid Plugins: - ------------------ - 1) Slick.AutoTooltips - - This plugin is useful for displaying cell data as tooltip when - user hover mouse on cell if data is large - 2) Slick.CheckboxSelectColumn - - This plugin is useful for selecting rows using checkbox - 3) RowSelectionModel - - This plugin is needed by CheckboxSelectColumn plugin to select rows - - Grid Options: - ------------- - 1) editable - - This option allow us to make grid editable - 2) enableAddRow - - This option allow us to add new rows at the end of grid - 3) enableCellNavigation - - This option allow us to navigate cells using keyboard - 4) enableColumnReorder - - This option allow us to record column - 5) asyncEditorLoading - - This option allow us to open editor async - 6) autoEdit - - This option allow us to enter in edit mode directly when user clicks on it - otherwise user have to double click or manually press enter on cell to go - in cell edit mode - - Handling of data: - ----------------- - We are doing data handling manually,what user adds/updates/deletes etc - we will use `data_store` object to store everything user does within grid data - - - updated: - This will hold all the data which user updates in grid - - added: - This will hold all the new row(s) data which user adds in grid - - staged_rows: - This will hold all the data which user copies/pastes/deletes in grid - - deleted: - This will hold all the data which user deletes in grid - - Events handling: - ---------------- - 1) onCellChange - - We are using this event to listen to changes on individual cell. - 2) onAddNewRow - - We are using this event to listen to new row adding functionality. - 3) onSelectedRangesChanged - - We are using this event to listen when user selects rows for copy/delete operation. - 4) onBeforeEditCell - - We are using this event to save the data before users modified them - 5) onKeyDown - - We are using this event for Copy operation on grid - */ + } + }; + }); + alertify.confirmSave(gettext("Save changes?"), msg); + return false; + }, + + /* Regarding SlickGrid usage in render_grid function. + + SlickGrid Plugins: + ------------------ + 1) Slick.AutoTooltips + - This plugin is useful for displaying cell data as tooltip when + user hover mouse on cell if data is large + 2) Slick.CheckboxSelectColumn + - This plugin is useful for selecting rows using checkbox + 3) RowSelectionModel + - This plugin is needed by CheckboxSelectColumn plugin to select rows + + Grid Options: + ------------- + 1) editable + - This option allow us to make grid editable + 2) enableAddRow + - This option allow us to add new rows at the end of grid + 3) enableCellNavigation + - This option allow us to navigate cells using keyboard + 4) enableColumnReorder + - This option allow us to record column + 5) asyncEditorLoading + - This option allow us to open editor async + 6) autoEdit + - This option allow us to enter in edit mode directly when user clicks on it + otherwise user have to double click or manually press enter on cell to go + in cell edit mode + + Handling of data: + ----------------- + We are doing data handling manually,what user adds/updates/deletes etc + we will use `data_store` object to store everything user does within grid data + + - updated: + This will hold all the data which user updates in grid + - added: + This will hold all the new row(s) data which user adds in grid + - staged_rows: + This will hold all the data which user copies/pastes/deletes in grid + - deleted: + This will hold all the data which user deletes in grid + + Events handling: + ---------------- + 1) onCellChange + - We are using this event to listen to changes on individual cell. + 2) onAddNewRow + - We are using this event to listen to new row adding functionality. + 3) onSelectedRangesChanged + - We are using this event to listen when user selects rows for copy/delete operation. + 4) onBeforeEditCell + - We are using this event to save the data before users modified them + 5) onKeyDown + - We are using this event for Copy operation on grid + */ - // This function is responsible to create and render the SlickGrid. - render_grid: function(collection, columns, is_editable, client_primary_key, rows_affected) { - var self = this; + // This function is responsible to create and render the SlickGrid. + render_grid: function (collection, columns, is_editable, client_primary_key, rows_affected) { + var self = this; - // This will work as data store and holds all the - // inserted/updated/deleted data from grid - self.handler.data_store = { - updated: {}, - added: {}, - staged_rows: {}, - deleted: {}, - updated_index: {}, - added_index: {} - }; + // This will work as data store and holds all the + // inserted/updated/deleted data from grid + self.handler.data_store = { + updated: {}, + added: {}, + staged_rows: {}, + deleted: {}, + updated_index: {}, + added_index: {} + }; - // To store primary keys before they gets changed - self.handler.primary_keys_data = {}; + // To store primary keys before they gets changed + self.handler.primary_keys_data = {}; - self.client_primary_key = client_primary_key; + self.client_primary_key = client_primary_key; - self.client_primary_key_counter = 0; + self.client_primary_key_counter = 0; - // Remove any existing grid first - if (self.handler.slickgrid) { - self.handler.slickgrid.destroy(); - } + // Remove any existing grid first + if (self.handler.slickgrid) { + self.handler.slickgrid.destroy(); + } if(!_.isArray(collection) || !_.size(collection)) { collection = []; @@ -552,3238 +554,3107 @@ define('tools.querytool', [ var column_size = self.handler['col_size'], query = self.handler.query, // Extract table name from query - table_list = query.match(/select.*from\s+(\w+)/i); + table_list = query.match(/select.*from\s+\w+\.*(\w+)/i); + + if (!table_list) { + table_name = SqlEditorUtils.getHash(query); + } + else { + table_name = table_list[1]; + } + + self.handler['table_name'] = table_name; + column_size[table_name] = column_size[table_name] || {}; + + var grid_width = $($('#editor-panel').find('.wcFrame')[1]).width(); + _.each(columns, function (c) { + var options = { + id: c.name, + pos: c.pos, + field: c.name, + name: c.label, + display_name: c.display_name, + column_type: c.column_type, + column_type_internal: c.column_type_internal, + not_null: c.not_null, + has_default_val: c.has_default_val + }; + + // Get the columns width based on longer string among data type or + // column name. + var column_type = c.column_type.trim(); + var label = c.name.length > column_type.length ? c.name : column_type; - if (!table_list) { - table_name = SqlEditorUtils.getHash(query); + if (_.isUndefined(column_size[table_name][c.name])) { + options['width'] = SqlEditorUtils.calculateColumnWidth(label); + column_size[table_name][c.name] = options['width']; } else { - table_name = table_list[1]; + options['width'] = column_size[table_name][c.name]; } - self.handler['table_name'] = table_name; - column_size[table_name] = column_size[table_name] || {}; - - var grid_width = $($('#editor-panel').find('.wcFrame')[1]).width(); - _.each(columns, function(c) { - var options = { - id: c.name, - pos: c.pos, - field: c.name, - name: c.label, - display_name: c.display_name, - column_type: c.column_type, - column_type_internal: c.column_type_internal, - not_null: c.not_null, - has_default_val: c.has_default_val - }; + // If grid is editable then add editor else make it readonly + if (c.cell == 'Json') { + options['editor'] = is_editable ? Slick.Editors.JsonText + : Slick.Editors.ReadOnlyJsonText; + options['formatter'] = Slick.Formatters.JsonString; + } else if (c.cell == 'number' || + $.inArray(c.type, ['oid', 'xid', 'real']) !== -1 + ) { + options['editor'] = is_editable ? Slick.Editors.CustomNumber + : Slick.Editors.ReadOnlyText; + options['formatter'] = Slick.Formatters.Numbers; + } else if (c.cell == 'boolean') { + options['editor'] = is_editable ? Slick.Editors.Checkbox + : Slick.Editors.ReadOnlyCheckbox; + options['formatter'] = Slick.Formatters.Checkmark; + } else { + options['editor'] = is_editable ? Slick.Editors.pgText + : Slick.Editors.ReadOnlypgText; + options['formatter'] = Slick.Formatters.Text; + } - // Get the columns width based on longer string among data type or - // column name. - var column_type = c.column_type.trim(); - var label = c.name.length > column_type.length ? c.name : column_type; + grid_columns.push(options) + }); - if (_.isUndefined(column_size[table_name][c.name])) { - options['width'] = SqlEditorUtils.calculateColumnWidth(label); - column_size[table_name][c.name] = options['width']; - } - else { - options['width'] = column_size[table_name][c.name]; - } + var gridSelector = new GridSelector(); + grid_columns = self.grid_columns = gridSelector.getColumnDefinitions(grid_columns); - // If grid is editable then add editor else make it readonly - if(c.cell == 'Json') { - options['editor'] = is_editable ? Slick.Editors.JsonText - : Slick.Editors.ReadOnlyJsonText; - options['formatter'] = Slick.Formatters.JsonString; - } else if(c.cell == 'number' || - $.inArray(c.type, ['oid', 'xid', 'real']) !== -1 - ) { - options['editor'] = is_editable ? Slick.Editors.CustomNumber - : Slick.Editors.ReadOnlyText; - options['formatter'] = Slick.Formatters.Numbers; - } else if(c.cell == 'boolean') { - options['editor'] = is_editable ? Slick.Editors.Checkbox - : Slick.Editors.ReadOnlyCheckbox; - options['formatter'] = Slick.Formatters.Checkmark; - } else { - options['editor'] = is_editable ? Slick.Editors.pgText - : Slick.Editors.ReadOnlypgText; - options['formatter'] = Slick.Formatters.Text; + if (rows_affected) { + // calculate with for header row column. + grid_columns[0]['width'] = SqlEditorUtils.calculateColumnWidth(rows_affected); + } + + var grid_options = { + editable: true, + enableAddRow: is_editable, + enableCellNavigation: true, + enableColumnReorder: false, + asyncEditorLoading: false, + autoEdit: false + }; + + var $data_grid = self.$el.find('#datagrid'); + // Calculate height based on panel size at runtime & set it + var grid_height = $($('#editor-panel').find('.wcFrame')[1]).height() - 35; + $data_grid.height(grid_height); + + var dataView = self.dataView = new Slick.Data.DataView(), + grid = self.grid = new Slick.Grid($data_grid, dataView, grid_columns, grid_options); + + // Add-on function which allow us to identify the faulty row after insert/update + // and apply css accordingly + + dataView.getItemMetadata = function (i) { + var res = {}, cssClass = '', + data_store = self.handler.data_store; + + if (_.has(self.handler, 'data_store')) { + if (i in data_store.added_index && + data_store.added_index[i] in data_store.added) { + cssClass = 'new_row'; + if (data_store.added[data_store.added_index[i]].err) { + cssClass += ' error'; + } + } else if (i in data_store.updated_index && i in data_store.updated) { + cssClass = 'updated_row'; + if (data_store.updated[data_store.updated_index[i]].err) { + cssClass += ' error'; } + } + } + // Disable rows having default values + if (!_.isUndefined(self.handler.rows_to_disable) && + _.indexOf(self.handler.rows_to_disable, i) !== -1 + ) { + cssClass += ' disabled_row'; + } + return {'cssClasses': cssClass}; + }; + + grid.registerPlugin(new Slick.AutoTooltips({enableForHeaderCells: false})); + grid.registerPlugin(new ActiveCellCapture()); + grid.setSelectionModel(new XCellSelectionModel()); + grid.registerPlugin(gridSelector); + + var editor_data = { + keys: self.handler.primary_keys, + vals: collection, + columns: columns, + grid: grid, + selection: grid.getSelectionModel(), + editor: self, + client_primary_key: self.client_primary_key + }; + + self.handler.slickgrid = grid; + + // Listener function to watch selected rows from grid + if (editor_data.selection) { + editor_data.selection.onSelectedRangesChanged.subscribe( + setStagedRows.bind(editor_data)); + } - grid_columns.push(options) + grid.onColumnsResized.subscribe(function (e, args) { + var columns = this.getColumns(); + _.each(columns, function (col, key) { + var column_size = self.handler['col_size']; + column_size[self.handler['table_name']][col['id']] = col['width']; }); + }); + + gridSelector.onBeforeGridSelectAll.subscribe(function (e, args) { + if (self.handler.has_more_rows) { + // this will prevent selection un-till we load all data + e.stopImmediatePropagation(); + self.fetch_next_all(function () { + // since we've stopped event propagation we need to + // trigger onGridSelectAll manually with new event data. + gridSelector.onGridSelectAll.notify(args, new Slick.EventData()); + }); + } + }); + + gridSelector.onBeforeGridColumnSelectAll.subscribe(function (e, args) { + if (self.handler.has_more_rows) { + // this will prevent selection un-till we load all data + e.stopImmediatePropagation(); + self.fetch_next_all(function () { + // since we've stopped event propagation we need to + // trigger onGridColumnSelectAll manually with new event data. + gridSelector.onGridColumnSelectAll.notify(args, new Slick.EventData()); + }); + } + }); + + // listen for row count change. + dataView.onRowCountChanged.subscribe(function (e, args) { + grid.updateRowCount(); + grid.render(); + }); + + // listen for rows change. + dataView.onRowsChanged.subscribe(function (e, args) { + grid.invalidateRows(args.rows); + grid.render(); + }); + + // Listener function which will be called before user updates existing cell + // This will be used to collect primary key for that row + grid.onBeforeEditCell.subscribe(function (e, args) { + if (args.column.column_type_internal == 'bytea' || + args.column.column_type_internal == 'bytea[]') { + return false; + } - var gridSelector = new GridSelector(); - grid_columns = self.grid_columns = gridSelector.getColumnDefinitions(grid_columns); + var before_data = args.item; - if (rows_affected) { - // calculate with for header row column. - grid_columns[0]['width'] = SqlEditorUtils.calculateColumnWidth(rows_affected); + // If newly added row is saved but grid is not refreshed, + // then disable cell editing for that row + if (self.handler.rows_to_disable && + _.contains(self.handler.rows_to_disable, args.row)) { + return false; } - var grid_options = { - editable: true, - enableAddRow: is_editable, - enableCellNavigation: true, - enableColumnReorder: false, - asyncEditorLoading: false, - autoEdit: false - }; + if (self.handler.can_edit && before_data && self.client_primary_key in before_data) { + var _pk = before_data[self.client_primary_key], + _keys = self.handler.primary_keys, + current_pk = {}, each_pk_key = {}; - var $data_grid = self.$el.find('#datagrid'); - // Calculate height based on panel size at runtime & set it - var grid_height = $($('#editor-panel').find('.wcFrame')[1]).height() - 35; - $data_grid.height(grid_height); + // If already have primary key data then no need to go ahead + if (_pk in self.handler.primary_keys_data) { + return; + } - var dataView = self.dataView = new Slick.Data.DataView(), - grid = self.grid = new Slick.Grid($data_grid, dataView, grid_columns, grid_options); + // Fetch primary keys for the row before they gets modified + var _columns = self.handler.columns; + _.each(_keys, function (value, key) { + current_pk[key] = before_data[key]; + }); + // Place it in main variable for later use + self.handler.primary_keys_data[_pk] = current_pk + } + }); + + grid.onKeyDown.subscribe(function (event, args) { + var KEY_A = 65; + var modifiedKey = event.keyCode; + var isModifierDown = event.ctrlKey || event.metaKey; + // Intercept Ctrl/Cmd + A key board event. + // As we might want to load all rows before selecting all. + if (isModifierDown && modifiedKey == KEY_A && self.handler.has_more_rows) { + self.fetch_next_all(function () { + handleQueryOutputKeyboardEvent(event, args); + }); + } else { + handleQueryOutputKeyboardEvent(event, args); + } + }); + + // Listener function which will be called when user updates existing rows + grid.onCellChange.subscribe(function (e, args) { + // self.handler.data_store.updated will holds all the updated data + var changed_column = args.grid.getColumns()[args.cell].field, + updated_data = args.item[changed_column], // New value for current field + _pk = args.item[self.client_primary_key] || null, // Unique key to identify row + column_data = {}, + _type; + + // Access to row/cell value after a cell is changed. + // The purpose is to remove row_id from temp_new_row + // if new row has primary key instead of [default_value] + // so that cell edit is enabled for that row. + var grid = args.grid, + row_data = grid.getDataItem(args.row), + is_primary_key = _.all( + _.values( + _.pick( + row_data, self.primary_keys + ) + ), + function (val) { + return val != undefined + } + ); - // Add-on function which allow us to identify the faulty row after insert/update - // and apply css accordingly + // temp_new_rows is available only for view data. + if (is_primary_key && self.handler.temp_new_rows) { + var index = self.handler.temp_new_rows.indexOf(args.row); + if (index > -1) { + self.handler.temp_new_rows.splice(index, 1); + } + } - dataView.getItemMetadata = function(i) { - var res = {}, cssClass = '', - data_store = self.handler.data_store; + column_data[changed_column] = updated_data; - if (_.has(self.handler, 'data_store')) { - if (i in data_store.added_index && - data_store.added_index[i] in data_store.added) { - cssClass = 'new_row'; - if (data_store.added[data_store.added_index[i]].err) { - cssClass += ' error'; - } - } else if (i in data_store.updated_index && i in data_store.updated) { - cssClass = 'updated_row'; - if (data_store.updated[data_store.updated_index[i]].err) { - cssClass += ' error'; - } - } - } - // Disable rows having default values - if (!_.isUndefined(self.handler.rows_to_disable) && - _.indexOf(self.handler.rows_to_disable, i) !== -1 - ) { - cssClass += ' disabled_row'; + if (_pk) { + // Check if it is in newly added row by user? + if (_pk in self.handler.data_store.added) { + _.extend( + self.handler.data_store.added[_pk]['data'], + column_data); + //Find type for current column + self.handler.data_store.added[_pk]['err'] = false + // Check if it is updated data from existing rows? + } else if (_pk in self.handler.data_store.updated) { + _.extend( + self.handler.data_store.updated[_pk]['data'], + column_data + ); + self.handler.data_store.updated[_pk]['err'] = false + } else { + // First updated data for this primary key + self.handler.data_store.updated[_pk] = { + 'err': false, 'data': column_data, + 'primary_keys': self.handler.primary_keys_data[_pk] + }; + self.handler.data_store.updated_index[args.row] = _pk; } - return {'cssClasses': cssClass}; - }; + } + // Enable save button + $("#btn-save").prop('disabled', false); + }.bind(editor_data)); + + // Listener function which will be called when user adds new rows + grid.onAddNewRow.subscribe(function (e, args) { + // self.handler.data_store.added will holds all the newly added rows/data + var column = args.column, + item = args.item, data_length = this.grid.getDataLength(), + _key = (self.client_primary_key_counter++).toString(), + dataView = this.grid.getData(); + + // Add new row in list to keep track of it + if (_.isUndefined(item[0])) { + self.handler.temp_new_rows.push(data_length); + } - grid.registerPlugin( new Slick.AutoTooltips({ enableForHeaderCells: false }) ); - grid.registerPlugin(new ActiveCellCapture()); - grid.setSelectionModel(new XCellSelectionModel()); - grid.registerPlugin(gridSelector); - - var editor_data = { - keys: self.handler.primary_keys, - vals: collection, - columns: columns, - grid: grid, - selection: grid.getSelectionModel(), - editor: self, - client_primary_key: self.client_primary_key - }; + // If copied item has already primary key, use it. + if (item) { + item[self.client_primary_key] = _key; + } - self.handler.slickgrid = grid; + dataView.addItem(item); + self.handler.data_store.added[_key] = {'err': false, 'data': item}; + self.handler.data_store.added_index[data_length] = _key; + // Fetch data type & add it for the column + var temp = {}; + temp[column.name] = _.where(this.columns, {pos: column.pos})[0]['type']; + grid.updateRowCount(); + grid.render(); + + // Enable save button + $("#btn-save").prop('disabled', false); + }.bind(editor_data)); + + // Listen grid viewportChanged event to load next chunk of data. + grid.onViewportChanged.subscribe(function (e, args) { + var rendered_range = args.grid.getRenderedRange(), + data_len = args.grid.getDataLength(); + // start fetching next batch of records before reaching to bottom. + if (self.handler.has_more_rows && !self.handler.fetching_rows && rendered_range.bottom > data_len - 100) { + // fetch asynchronous + setTimeout(self.fetch_next.bind(self)); + } + }) + // Resize SlickGrid when window resize + $(window).resize(function () { + // Resize grid only when 'Data Output' panel is visible. + if (self.data_output_panel.isVisible()) { + self.grid_resize(grid); + } + }); - // Listener function to watch selected rows from grid - if (editor_data.selection) { - editor_data.selection.onSelectedRangesChanged.subscribe( - setStagedRows.bind(editor_data)); + // Resize SlickGrid when output Panel resize + self.data_output_panel.on(wcDocker.EVENT.RESIZE_ENDED, function () { + // Resize grid only when 'Data Output' panel is visible. + if (self.data_output_panel.isVisible()) { + self.grid_resize(grid); + } + }); + + // Resize SlickGrid when output Panel gets focus + self.data_output_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function () { + // Resize grid only if output panel is visible + if (self.data_output_panel.isVisible()) + self.grid_resize(grid); + }); + + for (var i = 0; i < collection.length; i++) { + // Convert to dict from 2darray + var item = {}; + for (var j = 1; j < grid_columns.length; j++) { + item[grid_columns[j]['field']] = collection[i][grid_columns[j]['pos']] } - grid.onColumnsResized.subscribe(function (e, args) { - var columns = this.getColumns(); - _.each(columns, function(col, key) { - var column_size = self.handler['col_size']; - column_size[self.handler['table_name']][col['id']] = col['width']; - }); - }); + item[self.client_primary_key] = (self.client_primary_key_counter++).toString(); + collection[i] = item; + } + dataView.setItems(collection, self.client_primary_key); + }, + fetch_next_all: function (cb) { + this.fetch_next(true, cb); + }, + fetch_next: function (fetch_all, cb) { + var self = this, url = ''; - gridSelector.onBeforeGridSelectAll.subscribe(function(e, args) { - if (self.handler.has_more_rows) { - // this will prevent selection un-till we load all data - e.stopImmediatePropagation(); - self.fetch_next_all(function() { - // since we've stopped event propagation we need to - // trigger onGridSelectAll manually with new event data. - gridSelector.onGridSelectAll.notify(args, new Slick.EventData()); - }); - } - }); + // This will prevent fetch operation if previous fetch operation is + // already in progress. + self.handler.fetching_rows = true; - gridSelector.onBeforeGridColumnSelectAll.subscribe(function(e, args) { - if (self.handler.has_more_rows) { - // this will prevent selection un-till we load all data - e.stopImmediatePropagation(); - self.fetch_next_all(function() { - // since we've stopped event propagation we need to - // trigger onGridColumnSelectAll manually with new event data. - gridSelector.onGridColumnSelectAll.notify(args, new Slick.EventData()); - }); + $("#btn-flash").prop('disabled', true); + + if (fetch_all) { + self.handler.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext('Fetching all records...') + ); + url = url_for('sqleditor.fetch_all', {'trans_id': self.transId, 'fetch_all': 1}); + } else { + url = url = url_for('sqleditor.fetch', {'trans_id': self.transId}); + } + + $.ajax({ + url: url, + method: 'GET', + success: function (res) { + self.handler.has_more_rows = res.data.has_more_rows; + $("#btn-flash").prop('disabled', false); + self.handler.trigger('pgadmin-sqleditor:loading-icon:hide'); + self.update_grid_data(res.data.result); + self.handler.fetching_rows = false; + if (typeof cb == "function") { + cb(); } - }); + }, + error: function (e) { + $("#btn-flash").prop('disabled', false); + self.handler.trigger('pgadmin-sqleditor:loading-icon:hide'); + self.handler.has_more_rows = false; + self.handler.fetching_rows = false; + if (typeof cb == "function") { + cb(); + } + if (e.readyState == 0) { + self.update_msg_history(false, + gettext('Not connected to the server or the connection to the server has been closed.') + ); + return; + } + } + }); + }, - // listen for row count change. - dataView.onRowCountChanged.subscribe(function (e, args) { - grid.updateRowCount(); - grid.render(); - }); + update_grid_data: function (data) { + this.dataView.beginUpdate(); - // listen for rows change. - dataView.onRowsChanged.subscribe(function (e, args) { - grid.invalidateRows(args.rows); - grid.render(); - }); + for (var i = 0; i < data.length; i++) { + // Convert 2darray to dict. + var item = {}; + for (var j = 1; j < this.grid_columns.length; j++) { + item[this.grid_columns[j]['field']] = data[i][this.grid_columns[j]['pos']] + } - // Listener function which will be called before user updates existing cell - // This will be used to collect primary key for that row - grid.onBeforeEditCell.subscribe(function (e, args) { - if (args.column.column_type_internal == 'bytea' || - args.column.column_type_internal == 'bytea[]') { - return false; - } + item[this.client_primary_key] = (this.client_primary_key_counter++).toString(); + this.dataView.addItem(item); + } - var before_data = args.item; + this.dataView.endUpdate(); + }, - // If newly added row is saved but grid is not refreshed, - // then disable cell editing for that row - if(self.handler.rows_to_disable && - _.contains(self.handler.rows_to_disable, args.row)) { - return false; - } + /* This function is responsible to render output grid */ + grid_resize: function (grid) { + var h = $($('#editor-panel').find('.wcFrame')[1]).height() - 35; + $('#datagrid').css({'height': h + 'px'}); + grid.resizeCanvas(); + }, - if(self.handler.can_edit && before_data && self.client_primary_key in before_data) { - var _pk = before_data[self.client_primary_key], - _keys = self.handler.primary_keys, - current_pk = {}, each_pk_key = {}; + /* This function is responsible to create and render the + * new backgrid for the history tab. + */ + render_history_grid: function () { + var self = this; - // If already have primary key data then no need to go ahead - if(_pk in self.handler.primary_keys_data) { - return; - } + self.history_collection = new HistoryBundle.HistoryCollection([]); - // Fetch primary keys for the row before they gets modified - var _columns = self.handler.columns; - _.each(_keys, function(value, key) { - current_pk[key] = before_data[key]; - }); - // Place it in main variable for later use - self.handler.primary_keys_data[_pk] = current_pk - } + var historyComponent; + var historyCollectionReactElement = React.createElement( + queryHistory.QueryHistory, { + historyCollection: self.history_collection, + ref: function (component) { + historyComponent = component; + }, }); - - grid.onKeyDown.subscribe(function(event, args) { - var KEY_A = 65; - var modifiedKey = event.keyCode; - var isModifierDown = event.ctrlKey || event.metaKey; - // Intercept Ctrl/Cmd + A key board event. - // As we might want to load all rows before selecting all. - if (isModifierDown && modifiedKey == KEY_A && self.handler.has_more_rows) { - self.fetch_next_all(function() { - handleQueryOutputKeyboardEvent(event, args); - }); - } else { - handleQueryOutputKeyboardEvent(event, args); + ReactDOM.render(historyCollectionReactElement, $('#history_grid')[0]); + + self.history_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function () { + historyComponent.refocus(); + }); + }, + + // Callback function for Add New Row button click. + on_delete: function () { + var self = this; + + // Trigger the addrow signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:deleterow', + self, + self.handler + ); + }, + + _stopEventPropogation: function (ev) { + ev = ev || window.event; + ev.cancelBubble = true; + ev.stopPropagation(); + ev.stopImmediatePropagation(); + ev.preventDefault(); + }, + + _closeDropDown: function (ev) { + var target = ev && (ev.currentTarget || ev.target); + if (target) { + $(target).closest('.open').removeClass('open').find('.dropdown-backdrop').remove(); + } + }, + + // Callback function for Save button click. + on_save: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.handler.close_on_save = false; + // Trigger the save signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:save', + self, + self.handler + ); + }, + + // Callback function for Save button click. + on_save_as: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.handler.close_on_save = false; + // Trigger the save signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:save', + self, + self.handler, + true + ); + }, + + // Callback function for the find button click. + on_find: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.query_tool_obj.execCommand("find"); + }, + + // Callback function for the find next button click. + on_find_next: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.query_tool_obj.execCommand("findNext"); + }, + + // Callback function for the find previous button click. + on_find_previous: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.query_tool_obj.execCommand("findPrev"); + }, + + // Callback function for the replace button click. + on_replace: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.query_tool_obj.execCommand("replace"); + }, + + // Callback function for the replace all button click. + on_replace_all: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.query_tool_obj.execCommand("replaceAll"); + }, + + // Callback function for the find persistent button click. + on_find_persistent: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.query_tool_obj.execCommand("findPersistent"); + }, + + // Callback function for the jump button click. + on_jump: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + self.query_tool_obj.execCommand("jumpToLine"); + }, + + // Callback function for filter button click. + on_show_filter: function () { + var self = this; + + // Trigger the show_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:show_filter', + self, + self.handler + ); + }, + + // Callback function for include filter button click. + on_include_filter: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + // Trigger the include_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:include_filter', + self, + self.handler + ); + }, + + // Callback function for exclude filter button click. + on_exclude_filter: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + // Trigger the exclude_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:exclude_filter', + self, + self.handler + ); + }, + + // Callback function for remove filter button click. + on_remove_filter: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + // Trigger the remove_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:remove_filter', + self, + self.handler + ); + }, + + // Callback function for ok button click. + on_apply: function () { + var self = this; + + // Trigger the apply_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:apply_filter', + self, + self.handler + ); + }, + + // Callback function for cancel button click. + on_cancel: function () { + $('#filter').addClass('hidden'); + $('#editor-panel').removeClass('sql-editor-busy-fetching'); + }, + + // Callback function for copy button click. + on_copy_row: function () { + var self = this; + + // Trigger the copy signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:copy_row', + self, + self.handler + ); + }, + + // Callback function for paste button click. + on_paste_row: function () { + var self = this; + + // Trigger the paste signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:paste_row', + self, + self.handler + ); + }, + + // Callback function for the change event of combo box + on_limit_change: function () { + var self = this; + + // Trigger the limit signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:limit', + self, + self.handler + ); + }, + + // Callback function for the flash button click. + on_flash: function () { + queryToolActions.executeQuery(this.handler); + }, + + // Callback function for the cancel query button click. + on_cancel_query: function () { + var self = this; + + // Trigger the cancel-query signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:cancel-query', + self, + self.handler + ); + }, + + // Callback function for the line comment code + on_comment_line_code: function () { + queryToolActions.commentLineCode(this.handler); + }, + + // Callback function for the line uncomment code + on_uncomment_line_code: function () { + queryToolActions.uncommentLineCode(this.handler); + }, + + // Callback function for the block comment/uncomment code + on_toggle_comment_block_code: function () { + queryToolActions.commentBlockCode(this.handler); + }, + + on_indent_code: function () { + var self = this; + // Trigger the comment signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:indent_selected_code', + self, + self.handler + ); + }, + + on_unindent_code: function () { + var self = this; + // Trigger the comment signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:unindent_selected_code', + self, + self.handler + ); + }, + + // Callback function for the clear button click. + on_clear: function (ev) { + var self = this, sql; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + /* If is_query_changed flag is set to false then no need to + * confirm with the user for unsaved changes. + */ + if (self.handler.is_query_changed) { + alertify.confirm( + gettext("Unsaved changes"), + gettext("Are you sure you wish to discard the current changes?"), + function () { + // Do nothing as user do not want to save, just continue + self.query_tool_obj.setValue(''); + }, + function () { + return true; } - }); - - // Listener function which will be called when user updates existing rows - grid.onCellChange.subscribe(function (e, args) { - // self.handler.data_store.updated will holds all the updated data - var changed_column = args.grid.getColumns()[args.cell].field, - updated_data = args.item[changed_column], // New value for current field - _pk = args.item[self.client_primary_key] || null, // Unique key to identify row - column_data = {}, - _type; - - // Access to row/cell value after a cell is changed. - // The purpose is to remove row_id from temp_new_row - // if new row has primary key instead of [default_value] - // so that cell edit is enabled for that row. - var grid = args.grid, - row_data = grid.getDataItem(args.row), - is_primary_key = _.all( - _.values( - _.pick( - row_data, self.primary_keys - ) - ), - function(val) { - return val != undefined - } - ); + ).set('labels', {ok: 'Yes', cancel: 'No'}); + } else { + self.query_tool_obj.setValue(''); + } + }, + + // Callback function for the clear history button click. + on_clear_history: function (ev) { + var self = this; + this._stopEventPropogation(ev); + this._closeDropDown(ev); + // ask for confirmation only if anything to clear + if (!self.history_collection.length()) { + return; + } - // temp_new_rows is available only for view data. - if (is_primary_key && self.handler.temp_new_rows) { - var index = self.handler.temp_new_rows.indexOf(args.row); - if (index > -1) { - self.handler.temp_new_rows.splice(index, 1); - } + alertify.confirm(gettext("Clear history"), + gettext("Are you sure you wish to clear the history?"), + function () { + if (self.history_collection) { + self.history_collection.reset(); } + }, + function () { + return true; + } + ).set('labels', {ok: 'Yes', cancel: 'No'}); + }, + + // Callback function for the auto commit button click. + on_auto_commit: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + + // Trigger the auto-commit signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:auto_commit', + self, + self.handler + ); + }, + + // Callback function for the auto rollback button click. + on_auto_rollback: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + + // Trigger the download signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:auto_rollback', + self, + self.handler + ); + }, + + // Callback function for explain button click. + on_explain: function (event) { + this._stopEventPropogation(event); + this._closeDropDown(event); + + queryToolActions.explain(this.handler); + }, + + // Callback function for explain analyze button click. + on_explain_analyze: function (event) { + this._stopEventPropogation(event); + this._closeDropDown(event); + + queryToolActions.explainAnalyze(this.handler); + }, + + // Callback function for explain option "verbose" button click + on_explain_verbose: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + + // Trigger the explain "verbose" signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:explain-verbose', + self, + self.handler + ); + }, + + // Callback function for explain option "costs" button click + on_explain_costs: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + + // Trigger the explain "costs" signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:explain-costs', + self, + self.handler + ); + }, + + // Callback function for explain option "buffers" button click + on_explain_buffers: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + + // Trigger the explain "buffers" signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:explain-buffers', + self, + self.handler + ); + }, + + // Callback function for explain option "timing" button click + on_explain_timing: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + + // Trigger the explain "timing" signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:explain-timing', + self, + self.handler + ); + }, + + do_not_close_menu: function (ev) { + ev.stopPropagation(); + }, + + // callback function for load file button click. + on_file_load: function (ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + // Trigger the save signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:load_file', + self, + self.handler + ); + }, + + on_download: function () { + this.handler.download(); + }, + + keyAction: function (event) { + keyboardShortcuts.processEvent(this.handler, queryToolActions, event); + }, + }); - column_data[changed_column] = updated_data; + /* Defining controller class for data grid, which actually + * perform the operations like executing the sql query, poll the result, + * render the data in the grid, Save/Refresh the data etc... + */ + var SqlEditorController = function (container, options) { + this.initialize.apply(this, arguments); + }; + + _.extend( + SqlEditorController.prototype, + Backbone.Events, + { + initialize: function (container, opts) { + this.container = container; + }, - if(_pk) { - // Check if it is in newly added row by user? - if(_pk in self.handler.data_store.added) { - _.extend( - self.handler.data_store.added[_pk]['data'], - column_data); - //Find type for current column - self.handler.data_store.added[_pk]['err'] = false - // Check if it is updated data from existing rows? - } else if(_pk in self.handler.data_store.updated) { - _.extend( - self.handler.data_store.updated[_pk]['data'], - column_data - ); - self.handler.data_store.updated[_pk]['err'] = false - } else { - // First updated data for this primary key - self.handler.data_store.updated[_pk] = { - 'err': false, 'data': column_data, - 'primary_keys': self.handler.primary_keys_data[_pk] - }; - self.handler.data_store.updated_index[args.row] = _pk; - } - } - // Enable save button - $("#btn-save").prop('disabled', false); - }.bind(editor_data)); - - // Listener function which will be called when user adds new rows - grid.onAddNewRow.subscribe(function (e, args) { - // self.handler.data_store.added will holds all the newly added rows/data - var column = args.column, - item = args.item, data_length = this.grid.getDataLength(), - _key = (self.client_primary_key_counter++).toString(), - dataView = this.grid.getData(); - - // Add new row in list to keep track of it - if (_.isUndefined(item[0])) { - self.handler.temp_new_rows.push(data_length); - } + /* This function is used to create instance of SQLEditorView, + * call the render method of the grid view to render the backgrid + * header and loading icon and start execution of the sql query. + */ + start: function (is_query_tool, editor_title, script_sql, is_new_browser_tab) { + var self = this; - // If copied item has already primary key, use it. - if(item) { - item[self.client_primary_key] = _key; - } + self.is_query_tool = is_query_tool; + self.rows_affected = 0; + self.marked_line_no = 0; + self.explain_verbose = false; + self.explain_costs = false; + self.explain_buffers = false; + self.explain_timing = false; + self.is_new_browser_tab = is_new_browser_tab; + self.has_more_rows = false; + self.fetching_rows = false; + self.close_on_save = false; + + // We do not allow to call the start multiple times. + if (self.gridView) + return; + + self.gridView = new SQLEditorView({ + el: self.container, + handler: self + }); + self.transId = self.gridView.transId = self.container.data('transId'); - dataView.addItem(item); - self.handler.data_store.added[_key] = {'err': false, 'data': item}; - self.handler.data_store.added_index[data_length] = _key; - // Fetch data type & add it for the column - var temp = {}; - temp[column.name] = _.where(this.columns, {pos: column.pos})[0]['type']; - grid.updateRowCount(); - grid.render(); + self.gridView.editor_title = _.unescape(editor_title); + self.gridView.current_file = undefined; - // Enable save button - $("#btn-save").prop('disabled', false); - }.bind(editor_data)); - - // Listen grid viewportChanged event to load next chunk of data. - grid.onViewportChanged.subscribe(function(e, args) { - var rendered_range = args.grid.getRenderedRange(), - data_len = args.grid.getDataLength(); - // start fetching next batch of records before reaching to bottom. - if (self.handler.has_more_rows && !self.handler.fetching_rows && rendered_range.bottom > data_len - 100) { - // fetch asynchronous - setTimeout(self.fetch_next.bind(self)); - } - }) - // Resize SlickGrid when window resize - $( window ).resize( function() { - // Resize grid only when 'Data Output' panel is visible. - if(self.data_output_panel.isVisible()) { - self.grid_resize(grid); - } - }); + // Render the header + self.gridView.render(); - // Resize SlickGrid when output Panel resize - self.data_output_panel.on(wcDocker.EVENT.RESIZE_ENDED, function() { - // Resize grid only when 'Data Output' panel is visible. - if(self.data_output_panel.isVisible()) { - self.grid_resize(grid); - } - }); + // Listen to the file manager button events + pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:select_file', self._select_file_handler, self); + pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', self._save_file_handler, self); - // Resize SlickGrid when output Panel gets focus - self.data_output_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() { - // Resize grid only if output panel is visible - if(self.data_output_panel.isVisible()) - self.grid_resize(grid); - }); + // Listen to the codemirror on text change event + // only in query editor tool + if (self.is_query_tool) { + self.get_preferences(); + self.gridView.query_tool_obj.on('change', self._on_query_change.bind(self)); + } - for (var i = 0; i < collection.length; i++) { - // Convert to dict from 2darray - var item = {}; - for (var j = 1; j < grid_columns.length; j++) { - item[grid_columns[j]['field']] = collection[i][grid_columns[j]['pos']] + // Listen on events come from SQLEditorView for the button clicked. + self.on('pgadmin-sqleditor:button:load_file', self._load_file, self); + self.on('pgadmin-sqleditor:button:save', self._save, self); + self.on('pgadmin-sqleditor:button:deleterow', self._delete, self); + self.on('pgadmin-sqleditor:button:show_filter', self._show_filter, self); + self.on('pgadmin-sqleditor:button:include_filter', self._include_filter, self); + self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self); + self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self); + self.on('pgadmin-sqleditor:button:apply_filter', self._apply_filter, self); + self.on('pgadmin-sqleditor:button:copy_row', self._copy_row, self); + self.on('pgadmin-sqleditor:button:paste_row', self._paste_row, self); + self.on('pgadmin-sqleditor:button:limit', self._set_limit, self); + self.on('pgadmin-sqleditor:button:cancel-query', self._cancel_query, self); + self.on('pgadmin-sqleditor:button:auto_rollback', self._auto_rollback, self); + self.on('pgadmin-sqleditor:button:auto_commit', self._auto_commit, self); + self.on('pgadmin-sqleditor:button:explain-verbose', self._explain_verbose, self); + self.on('pgadmin-sqleditor:button:explain-costs', self._explain_costs, self); + self.on('pgadmin-sqleditor:button:explain-buffers', self._explain_buffers, self); + self.on('pgadmin-sqleditor:button:explain-timing', self._explain_timing, self); + // Indentation related + self.on('pgadmin-sqleditor:indent_selected_code', self._indent_selected_code, self); + self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self); + + if (self.is_query_tool) { + self.gridView.query_tool_obj.refresh(); + if (script_sql && script_sql !== '') { + self.gridView.query_tool_obj.setValue(script_sql); } - - item[self.client_primary_key] = (self.client_primary_key_counter++).toString(); - collection[i] = item; } - dataView.setItems(collection, self.client_primary_key); - }, - fetch_next_all: function(cb) { - this.fetch_next(true, cb); + else { + // Disable codemirror by setting cursor to nocursor and background to dark. + self.gridView.query_tool_obj.setOption("readOnly", 'nocursor'); + var cm = self.gridView.query_tool_obj.getWrapperElement(); + if (cm) { + cm.className += ' bg-gray-1 opacity-5'; + } + self.disable_tool_buttons(true); + self.execute_data_query(); + } }, - fetch_next: function(fetch_all, cb) { - var self = this, url = ''; - // This will prevent fetch operation if previous fetch operation is - // already in progress. - self.handler.fetching_rows = true; - - $("#btn-flash").prop('disabled', true); + // This function checks if there is any dirty data in the grid before + // it executes the sql query + execute_data_query: function () { + var self = this; - if (fetch_all) { - self.handler.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext('Fetching all records...') - ); - url = url_for('sqleditor.fetch_all', {'trans_id': self.transId, 'fetch_all': 1}); + // Check if the data grid has any changes before running query + if (_.has(self, 'data_store') && + ( _.size(self.data_store.added) || + _.size(self.data_store.updated) || + _.size(self.data_store.deleted)) + ) { + alertify.confirm(gettext("Unsaved changes"), + gettext("The data has been modified, but not saved. Are you sure you wish to discard the changes?"), + function () { + // Do nothing as user do not want to save, just continue + self._run_query(); + }, + function () { + // Stop, User wants to save + return true; + } + ).set('labels', {ok: 'Yes', cancel: 'No'}); } else { - url = url = url_for('sqleditor.fetch', {'trans_id': self.transId}); + self._run_query(); } + }, - $.ajax({ - url: url, - method: 'GET', - success: function(res) { - self.handler.has_more_rows = res.data.has_more_rows; - $("#btn-flash").prop('disabled', false); - self.handler.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.update_grid_data(res.data.result); - self.handler.fetching_rows = false; - if (typeof cb == "function") { - cb(); - } - }, - error: function(e) { - $("#btn-flash").prop('disabled', false); - self.handler.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.handler.has_more_rows = false; - self.handler.fetching_rows = false; - if (typeof cb == "function") { - cb(); + // This function makes the ajax call to execute the sql query. + _run_query: function () { + var self = this; + self.query_start_time = new Date(); + self.rows_affected = 0; + self._init_polling_flags(); + // keep track of newly added rows + self.rows_to_disable = new Array(); + // Temporarily hold new rows added + self.temp_new_rows = new Array(); + self.has_more_rows = false; + self.fetching_rows = false; + + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Initializing query execution.") + ); + + $("#btn-flash").prop('disabled', true); + + self.trigger( + 'pgadmin-sqleditor:loading-icon:message', + gettext("Waiting for the query execution to complete...") + ); + + $.ajax({ + url: url_for('sqleditor.view_data_start', {'trans_id': self.transId}), + method: 'GET', + success: function (res) { + if (res.data.status) { + + self.can_edit = res.data.can_edit; + self.can_filter = res.data.can_filter; + self.info_notifier_timeout = res.data.info_notifier_timeout; + + // Set the sql query to the SQL panel + self.gridView.query_tool_obj.setValue(res.data.sql); + self.query = res.data.sql; + + + /* If filter is applied then remove class 'btn-default' + * and add 'btn-warning' to change the colour of the button. + */ + if (self.can_filter && res.data.filter_applied) { + $('#btn-filter').removeClass('btn-default'); + $('#btn-filter-dropdown').removeClass('btn-default'); + $('#btn-filter').addClass('btn-warning'); + $('#btn-filter-dropdown').addClass('btn-warning'); + } + else { + $('#btn-filter').removeClass('btn-warning'); + $('#btn-filter-dropdown').removeClass('btn-warning'); + $('#btn-filter').addClass('btn-default'); + $('#btn-filter-dropdown').addClass('btn-default'); + } + $("#btn-save").prop('disabled', true); + $("#btn-file-menu-dropdown").prop('disabled', true); + $("#btn-copy-row").prop('disabled', true); + $("#btn-paste-row").prop('disabled', true); + + // Set the combo box value + $(".limit").val(res.data.limit); + + // If status is True then poll the result. + self._poll(); + } + else { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + self.update_msg_history(false, res.data.result); } + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); if (e.readyState == 0) { self.update_msg_history(false, - gettext('Not connected to the server or the connection to the server has been closed.') + gettext("Not connected to the server or the connection to the server has been closed.") ); return; } + + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; + + self.update_msg_history(false, msg); } }); }, - update_grid_data: function(data) { - this.dataView.beginUpdate(); + // This is a wrapper to call_render function + // We need this because we have separated columns route & result route + // We need to combine both result here in wrapper before rendering grid + call_render_after_poll: function (res) { + var self = this; + self.query_end_time = new Date(); + self.rows_affected = res.rows_affected, + self.has_more_rows = res.has_more_rows; - for (var i = 0; i < data.length; i++) { - // Convert 2darray to dict. - var item = {}; - for (var j = 1; j < this.grid_columns.length; j++) { - item[this.grid_columns[j]['field']] = data[i][this.grid_columns[j]['pos']] + /* If no column information is available it means query + runs successfully with no result to display. In this + case no need to call render function. + */ + if (res.colinfo != null) + self._render(res); + else { + // Show message in message and history tab in case of query tool + self.total_time = self.get_query_run_time(self.query_start_time, self.query_end_time); + var msg = S(gettext("Query returned successfully in %s.")).sprintf(self.total_time).value(); + res.result += "\n\n" + msg; + self.update_msg_history(true, res.result, false); + // Display the notifier if the timeout is set to >= 0 + if (self.info_notifier_timeout >= 0) { + alertify.success(msg, self.info_notifier_timeout); + } } - item[this.client_primary_key] = (this.client_primary_key_counter++).toString(); - this.dataView.addItem(item); + // Enable/Disable query tool button only if is_query_tool is true. + if (self.is_query_tool) { + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); } - - this.dataView.endUpdate(); + is_query_running = false; + self.trigger('pgadmin-sqleditor:loading-icon:hide'); }, - /* This function is responsible to render output grid */ - grid_resize: function(grid) { - var h = $($('#editor-panel').find('.wcFrame')[1]).height() - 35; - $('#datagrid').css({'height': h + 'px'}); - grid.resizeCanvas(); - }, - /* This function is responsible to create and render the - * new backgrid for the history tab. + /* This function makes the ajax call to poll the result, + * if status is Busy then recursively call the poll function + * till the status is 'Success' or 'NotConnected'. If status is + * 'Success' then call the render method to render the data. */ - render_history_grid: function() { + _poll: function () { var self = this; - self.history_collection = new HistoryBundle.HistoryCollection([]); - - var historyComponent; - var historyCollectionReactElement = React.createElement( - queryHistory.QueryHistory, { - historyCollection: self.history_collection, - ref: function(component) { - historyComponent = component; - }, - }); - ReactDOM.render(historyCollectionReactElement, $('#history_grid')[0]); - - self.history_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function () { - historyComponent.refocus(); - }); - }, + setTimeout( + function () { + $.ajax({ + url: url_for('sqleditor.poll', {'trans_id': self.transId}), + method: 'GET', + success: function (res) { + if (res.data.status === 'Success') { + self.trigger( + 'pgadmin-sqleditor:loading-icon:message', + gettext("Loading data from the database server and rendering...") + ); - // Callback function for Add New Row button click. - on_delete: function() { - var self = this; + self.call_render_after_poll(res.data); + } + else if (res.data.status === 'Busy') { + // If status is Busy then poll the result by recursive call to the poll function + self._poll(); + is_query_running = true; + if (res.data.result) { + self.update_msg_history(res.data.status, res.data.result, false); + } + } + else if (res.data.status === 'NotConnected') { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + // Enable/Disable query tool button only if is_query_tool is true. + if (self.is_query_tool) { + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); + } + self.update_msg_history(false, res.data.result, true); + } + else if (res.data.status === 'Cancel') { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + self.update_msg_history(false, "Execution Cancelled!", true) + } + }, + error: function (e) { + // Enable/Disable query tool button only if is_query_tool is true. + self.resetQueryHistoryObject(self); + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + if (self.is_query_tool) { + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); + } - // Trigger the addrow signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:deleterow', - self, - self.handler - ); - }, + if (e.readyState == 0) { + self.update_msg_history(false, + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; + } - _stopEventPropogation: function(ev) { - ev = ev || window.event; - ev.cancelBubble = true; - ev.stopPropagation(); - ev.stopImmediatePropagation(); - ev.preventDefault(); - }, + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - _closeDropDown: function(ev) { - var target = ev && (ev.currentTarget || ev.target); - if (target) { - $(target).closest('.open').removeClass('open').find('.dropdown-backdrop').remove(); - } + self.update_msg_history(false, msg); + // Highlight the error in the sql panel + self._highlight_error(msg); + } + }); + }, self.POLL_FALLBACK_TIME()); }, - // Callback function for Save button click. - on_save: function(ev) { + /* This function is used to create the backgrid columns, + * create the Backbone PageableCollection and finally render + * the data in the backgrid. + */ + _render: function (data) { var self = this; + self.colinfo = data.col_info; + self.primary_keys = data.primary_keys; + self.client_primary_key = data.client_primary_key; + self.cell_selected = false; + self.selected_model = null; + self.changedModels = []; + $('.sql-editor-explain').empty(); + + /* If object don't have primary keys then set the + * can_edit flag to false. + */ + if (self.primary_keys === null || self.primary_keys === undefined + || _.size(self.primary_keys) === 0) + self.can_edit = false; + else + self.can_edit = true; + + /* If user can filter the data then we should enabled + * Filter and Limit buttons. + */ + if (self.can_filter) { + $(".limit").prop('disabled', false); + $(".limit").addClass('limit-enabled'); + $("#btn-filter").prop('disabled', false); + $("#btn-filter-dropdown").prop('disabled', false); + } - this._stopEventPropogation(ev); - this._closeDropDown(ev); + // Initial settings for delete row, copy row and paste row buttons. + $("#btn-delete-row").prop('disabled', true); + // Do not disable save button in query tool + if (!self.is_query_tool && !self.can_edit) { + $("#btn-save").prop('disabled', true); + $("#btn-file-menu-dropdown").prop('disabled', true); + } + if (!self.can_edit) { + $("#btn-delete-row").prop('disabled', true); + $("#btn-copy-row").prop('disabled', true); + $("#btn-paste-row").prop('disabled', true); + } - self.handler.close_on_save = false; - // Trigger the save signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:save', - self, - self.handler - ); - }, + // Fetch the columns metadata + self._fetch_column_metadata.call( + self, data, function () { + var self = this; - // Callback function for Save button click. - on_save_as: function(ev) { - var self = this; + self.trigger( + 'pgadmin-sqleditor:loading-icon:message', + gettext("Loading data from the database server and rendering..."), + self + ); - this._stopEventPropogation(ev); - this._closeDropDown(ev); + // Show message in message and history tab in case of query tool + self.total_time = self.get_query_run_time(self.query_start_time, self.query_end_time); + var msg1 = S(gettext("Successfully run. Total query runtime: %s.")).sprintf(self.total_time).value(); + var msg2 = S(gettext("%s rows affected.")).sprintf(self.rows_affected).value(); - self.handler.close_on_save = false; - // Trigger the save signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:save', - self, - self.handler, - true - ); - }, + // Display the notifier if the timeout is set to >= 0 + if (self.info_notifier_timeout >= 0) { + alertify.success(msg1 + ' ' + msg2, self.info_notifier_timeout); + } - // Callback function for the find button click. - on_find: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + var _msg = msg1 + '\n' + msg2; - self.query_tool_obj.execCommand("find"); - }, - // Callback function for the find next button click. - on_find_next: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + // If there is additional messages from server then add it to message + if(!_.isNull(data.additional_messages) && + !_.isUndefined(data.additional_messages)) { + _msg = data.additional_messages + '\n' + _msg; + } - self.query_tool_obj.execCommand("findNext"); - }, + self.update_msg_history(true,_msg, false); - // Callback function for the find previous button click. - on_find_previous: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + /* Add the data to the collection and render the grid. + * In case of Explain draw the graph on explain panel + * and add json formatted data to collection and render. + */ + var explain_data_array = []; + if ( + data.result && data.result.length >= 1 && + data.result[0] && data.result[0][0] && data.result[0][0][0] && + data.result[0][0][0].hasOwnProperty('Plan') && + _.isObject(data.result[0][0][0]['Plan']) + ) { + var explain_data = [JSON.stringify(data.result[0][0], null, 2)]; + explain_data_array.push(explain_data); + // Make sure - the 'Data Output' panel is visible, before - we + // start rendering the grid. + self.gridView.data_output_panel.focus(); + setTimeout( + function () { + self.gridView.render_grid( + explain_data_array, self.columns, self.can_edit, + self.client_primary_key + ); + // Make sure - the 'Explain' panel is visible, before - we + // start rendering the grid. + self.gridView.explain_panel.focus(); + pgExplain.DrawJSONPlan( + $('.sql-editor-explain'), data.result[0][0] + ); + }, 10 + ); + } else { + // Make sure - the 'Data Output' panel is visible, before - we + // start rendering the grid. + self.gridView.data_output_panel.focus(); + setTimeout( + function () { + self.gridView.render_grid(data.result, self.columns, + self.can_edit, self.client_primary_key, data.rows_affected); + }, 10 + ); + } - self.query_tool_obj.execCommand("findPrev"); + // Hide the loading icon + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + $("#btn-flash").prop('disabled', false); + }.bind(self) + ); }, - // Callback function for the replace button click. - on_replace: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + // This function creates the columns as required by the backgrid + _fetch_column_metadata: function (data, cb) { + var colinfo = data.colinfo, + primary_keys = data.primary_keys, + result = data.result, + columns = [], + self = this; + // Store pg_types in an array + var pg_types = new Array(); + _.each(data.types, function (r) { + pg_types[r.oid] = [r.typname]; + }); - self.query_tool_obj.execCommand("replace"); - }, + // Create columns required by slick grid to render + _.each(colinfo, function (c) { + var is_primary_key = false; - // Callback function for the replace all button click. - on_replace_all: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + // Check whether table have primary key + if (_.size(primary_keys) > 0) { + _.each(primary_keys, function (value, key) { + if (key === c.name) + is_primary_key = true; + }); + } - self.query_tool_obj.execCommand("replaceAll"); - }, + // To show column label and data type in multiline, + // The elements should be put inside the div. + // Create column label and type. + var col_type = '', + column_label = '', + col_cell; + var type = pg_types[c.type_code] ? + pg_types[c.type_code][0] : + // This is the case where user might + // have use casting so we will use type + // returned by cast function + pg_types[pg_types.length - 1][0] ? + pg_types[pg_types.length - 1][0] : 'unknown'; + + if (!is_primary_key) + col_type += type; + else + col_type += '[PK] ' + type; - // Callback function for the find persistent button click. - on_find_persistent: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + if (c.precision && c.precision >= 0 && c.precision != 65535) { + col_type += ' (' + c.precision; + col_type += c.scale && c.scale != 65535 ? + ',' + c.scale + ')' : + ')'; + } - self.query_tool_obj.execCommand("findPersistent"); - }, + // Identify cell type of column. + switch (type) { + case "json": + case "json[]": + case "jsonb": + case "jsonb[]": + col_cell = 'Json'; + break; + case "smallint": + case "integer": + case "bigint": + case "decimal": + case "numeric": + case "real": + case "double precision": + col_cell = 'number'; + break; + case "boolean": + col_cell = 'boolean'; + break; + case "character": + case "character[]": + case "character varying": + case "character varying[]": + if (c.internal_size && c.internal_size >= 0 && c.internal_size != 65535) { + // Update column type to display length on column header + col_type += ' (' + c.internal_size + ')'; + } + col_cell = 'string'; + break; + default: + col_cell = 'string'; + } - // Callback function for the jump button click. - on_jump: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + column_label = c.display_name + '
' + col_type; + + var col = { + 'name': c.name, + 'display_name': c.display_name, + 'column_type': col_type, + 'column_type_internal': type, + 'pos': c.pos, + 'label': column_label, + 'cell': col_cell, + 'can_edit': self.can_edit, + 'type': type, + 'not_null': c.not_null, + 'has_default_val': c.has_default_val + }; + columns.push(col); + }); - self.query_tool_obj.execCommand("jumpToLine"); + self.columns = columns; + if (cb && typeof(cb) == 'function') { + cb(); + } }, - // Callback function for filter button click. - on_show_filter: function() { - var self = this; - - // Trigger the show_filter signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:show_filter', - self, - self.handler - ); + resetQueryHistoryObject: function (history) { + history.total_time = '-'; }, - // Callback function for include filter button click. - on_include_filter: function(ev) { + // This function is used to raise appropriate message. + update_msg_history: function (status, msg, clear_grid) { var self = this; + if (clear_grid === undefined) + clear_grid = true; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + self.gridView.messages_panel.focus(); - // Trigger the include_filter signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:include_filter', - self, - self.handler - ); - }, + if (clear_grid) { + // Delete grid + if (self.gridView.handler.slickgrid) { + self.gridView.handler.slickgrid.destroy(); - // Callback function for exclude filter button click. - on_exclude_filter: function(ev) { - var self = this; + } + // Misc cleaning + self.columns = undefined; + self.collection = undefined; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + $('.sql-editor-message').text(msg); + } else { + $('.sql-editor-message').append(_.escape(msg)); + } - // Trigger the exclude_filter signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:exclude_filter', - self, - self.handler - ); + // Scroll automatically when msgs appends to element + setTimeout(function () { + $(".sql-editor-message").scrollTop($(".sql-editor-message")[0].scrollHeight); + ; + }, 10); + + if (status != 'Busy') { + $("#btn-flash").prop('disabled', false); + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + self.gridView.history_collection.add({ + 'status': status, + 'start_time': self.query_start_time, + 'query': self.query, + 'row_affected': self.rows_affected, + 'total_time': self.total_time, + 'message': msg, + }); + } }, - // Callback function for remove filter button click. - on_remove_filter: function(ev) { + // This function will return the total query execution Time. + get_query_run_time: function (start_time, end_time) { var self = this; - this._stopEventPropogation(ev); - this._closeDropDown(ev); - - // Trigger the remove_filter signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:remove_filter', - self, - self.handler - ); + // Calculate the difference in milliseconds + var difference_ms, miliseconds; + difference_ms = miliseconds = end_time.getTime() - start_time.getTime(); + //take out milliseconds + difference_ms = difference_ms / 1000; + var seconds = Math.floor(difference_ms % 60); + difference_ms = difference_ms / 60; + var minutes = Math.floor(difference_ms % 60); + + if (minutes > 0) + return minutes + ' min'; + else if (seconds > 0) { + return seconds + ' secs'; + } + else + return miliseconds + ' msec'; }, - // Callback function for ok button click. - on_apply: function() { + /* This function is used to check whether cell + * is editable or not depending on primary keys + * and staged_rows flag + */ + is_editable: function (obj) { var self = this; - - // Trigger the apply_filter signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:apply_filter', - self, - self.handler - ); + if (obj instanceof Backbone.Collection) + return false; + return (self.get('can_edit')); }, - // Callback function for cancel button click. - on_cancel: function() { - $('#filter').addClass('hidden'); - $('#editor-panel').removeClass('sql-editor-busy-fetching'); - }, + rows_to_delete: function (data) { + var self = this, + tmp_keys = self.primary_keys; - // Callback function for copy button click. - on_copy_row: function() { - var self = this; + // re-calculate rows with no primary keys + self.temp_new_rows = []; + data.forEach(function (d, idx) { + var p_keys_list = _.pick(d, tmp_keys), + is_primary_key = Object.keys(p_keys_list).length ? + p_keys_list[0] : undefined; - // Trigger the copy signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:copy_row', - self, - self.handler - ); + if (!is_primary_key) { + self.temp_new_rows.push(idx); + } + }); + self.rows_to_disable = _.clone(self.temp_new_rows); }, - // Callback function for paste button click. - on_paste_row: function() { - var self = this; + // This function will delete selected row. + _delete: function () { + var self = this, deleted_keys = [], + dgrid = document.getElementById("datagrid"), + is_added = _.size(self.data_store.added), + is_updated = _.size(self.data_store.updated); + + // Remove newly added rows from staged rows as we don't want to send them on server + if (is_added) { + _.each(self.data_store.added, function (val, key) { + if (key in self.data_store.staged_rows) { + // Remove the row from data store so that we do not send it on server + deleted_keys.push(key); + delete self.data_store.staged_rows[key]; + delete self.data_store.added[key]; + delete self.data_store.added_index[key]; + } + }); + } + // If only newly rows to delete and no data is there to send on server + // then just re-render the grid + if (_.size(self.data_store.staged_rows) == 0) { + var grid = self.slickgrid, + dataView = grid.getData(), + data = dataView.getItems(), + idx = 0; - // Trigger the paste signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:paste_row', - self, - self.handler - ); - }, + grid.resetActiveCell(); - // Callback function for the change event of combo box - on_limit_change: function() { - var self = this; + dataView.beginUpdate(); + for (var i = 0; i < deleted_keys.length; i++) { + dataView.deleteItem(deleted_keys[i]); + } + dataView.endUpdate(); + self.rows_to_delete.apply(self, [dataView.getItems()]); + grid.resetActiveCell(); + grid.setSelectedRows([]); + grid.invalidate(); - // Trigger the limit signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:limit', - self, - self.handler - ); - }, + // Nothing to copy or delete here + $("#btn-delete-row").prop('disabled', true); + $("#btn-copy-row").prop('disabled', true); + if(_.size(self.data_store.added) || is_updated) { + // Do not disable save button if there are + // any other changes present in grid data + $("#btn-save").prop('disabled', false); + } else { + $("#btn-save").prop('disabled', true); + } + alertify.success(gettext("Row(s) deleted")); + } else { + // There are other data to needs to be updated on server + if(is_updated) { + alertify.alert(gettext("Operation failed"), + gettext("There are unsaved changes in grid, Please save them first to avoid inconsistency in data") + ); + return; + } + alertify.confirm(gettext("Delete Row(s)"), + gettext("Are you sure you wish to delete selected row(s)?"), + function() { + $("#btn-delete-row").prop('disabled', true); + $("#btn-copy-row").prop('disabled', true); + // Change the state + self.data_store.deleted = self.data_store.staged_rows; + self.data_store.staged_rows = {}; + // Save the changes on server + self._save(); + }, + function() { + // Do nothing as user canceled the operation. + } + ).set('labels', {ok: gettext("Yes"), cancel:gettext("No")}); + } + }, - // Callback function for the flash button click. - on_flash: function() { - this.handler.executeQuery(); - }, + /* This function will fetch the list of changed models and make + * the ajax call to save the data into the database server. + * and will open save file dialog conditionally. + */ + _save: function (view, controller, save_as) { + var self = this, + data = [], + save_data = true; + + // Open save file dialog if query tool + if (self.is_query_tool) { + var current_file = self.gridView.current_file; + if (!_.isUndefined(current_file) && !save_as) { + self._save_file_handler(current_file); + } + else { + // provide custom option to save file dialog + var params = { + 'supported_types': ["*", "sql"], + 'dialog_type': 'create_file', + 'dialog_title': 'Save File', + 'btn_primary': 'Save' + } + pgAdmin.FileManager.init(); + pgAdmin.FileManager.show_dialog(params); + } + return; + } + $("#btn-save").prop('disabled', true); + $("#btn-file-menu-dropdown").prop('disabled', true); - // Callback function for the cancel query button click. - on_cancel_query: function() { - var self = this; + var is_added = _.size(self.data_store.added), + is_updated = _.size(self.data_store.updated), + is_deleted = _.size(self.data_store.deleted), + is_primary_error = false; - // Trigger the cancel-query signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:cancel-query', - self, - self.handler - ); - }, + if (!is_added && !is_updated && !is_deleted) { + return; // Nothing to save here + } - // Callback function for the line comment code - on_comment_line_code: function() { - this.handler.commentLineCode(); - }, + if (save_data) { - // Callback function for the line uncomment code - on_uncomment_line_code: function() { - this.handler.uncommentLineCode(); - }, + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Saving the updated data...") + ); - // Callback function for the block comment/uncomment code - on_toggle_comment_block_code: function() { - this.handler.commentBlockCode(); + // Add the columns to the data so the server can remap the data + var req_data = self.data_store; + req_data.columns = view ? view.handler.columns : self.columns; + + // Make ajax call to save the data + $.ajax({ + url: url_for('sqleditor.save', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(req_data), + success: function (res) { + var grid = self.slickgrid, + dataView = grid.getData(), + data_length = dataView.getLength(), + data = []; + if (res.data.status) { + // Remove flag is_row_copied from copied rows + _.each(data, function (row, idx) { + if (row.is_row_copied) { + delete row.is_row_copied; + } + }); + + // Remove 2d copied_rows array + if (grid.copied_rows) { + delete grid.copied_rows; + } + + // Remove deleted rows from client as well + if (is_deleted) { + var rows = grid.getSelectedRows(); + if (data_length == rows.length) { + // This means all the rows are selected, clear all data + data = []; + dataView.setItems(data, self.client_primary_key); + } else { + dataView.beginUpdate(); + for (var i = 0; i < rows.length; i++) { + var item = grid.getDataItem(rows[i]); + data.push(item); + dataView.deleteItem(item[self.client_primary_key]); + } + dataView.endUpdate(); + } + self.rows_to_delete.apply(self, [data]); + grid.setSelectedRows([]); + } + + // whether a cell is editable or not is decided in + // grid.onBeforeEditCell function (on cell click) + // but this function should do its job after save + // operation. So assign list of added rows to original + // rows_to_disable array. + if (is_added) { + self.rows_to_disable = _.clone(self.temp_new_rows); + } + + grid.setSelectedRows([]); + // Reset data store + self.data_store = { + 'added': {}, + 'updated': {}, + 'deleted': {}, + 'added_index': {}, + 'updated_index': {} + } + + // Reset old primary key data now + self.primary_keys_data = {}; + + // Clear msgs after successful save + $('.sql-editor-message').html(''); + } else { + // Something went wrong while saving data on the db server + $("#btn-flash").prop('disabled', false); + $('.sql-editor-message').text(res.data.result); + var err_msg = S(gettext("%s.")).sprintf(res.data.result).value(); + alertify.error(err_msg, 20); + grid.setSelectedRows([]); + // To highlight the row at fault + if(_.has(res.data, '_rowid') && + (!_.isUndefined(res.data._rowid)|| !_.isNull(res.data._rowid))) { + var _row_index = self._find_rowindex(res.data._rowid); + if(_row_index in self.data_store.added_index) { + // Remove new row index from temp_list if save operation + // fails + var index = self.handler.temp_new_rows.indexOf(res.data._rowid); + if (index > -1) { + self.handler.temp_new_rows.splice(index, 1); + } + self.data_store.added[self.data_store.added_index[_row_index]].err = true + } else if (_row_index in self.data_store.updated_index) { + self.data_store.updated[self.data_store.updated_index[_row_index]].err = true + } + } + grid.gotoCell(_row_index, 1); + } + + // Update the sql results in history tab + _.each(res.data.query_result, function (r) { + self.gridView.history_collection.add({ + 'status': r.status, + 'start_time': self.query_start_time, + 'query': r.sql, + 'row_affected': r.rows_affected, + 'total_time': self.total_time, + 'message': r.result, + }); + }); + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + + grid.invalidate(); + alertify.success(gettext("Data saved successfully.")); + if (self.close_on_save) { + self.close(); + } + }, + error: function(e) { + if (e.readyState == 0) { + self.update_msg_history(false, + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; + } + + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; + + self.update_msg_history(false, msg); + } + }); + } }, - on_indent_code: function() { - var self = this; - // Trigger the comment signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:indent_selected_code', - self, - self.handler - ); + // Find index of row at fault from grid data + _find_rowindex: function (rowid) { + var self = this, + grid = self.slickgrid, + dataView = grid.getData(), + data = dataView.getItems(), + _rowid, + count = 0, + _idx = -1; + + // If _rowid is object then it's update/delete operation + if (_.isObject(rowid)) { + _rowid = rowid; + } else if (_.isString(rowid)) { // Insert operation + var rowid = {}; + rowid[self.client_primary_key] = rowid; + _rowid = rowid; + } else { + // Something is wrong with unique id + return _idx; + } + + _.find(data, function (d) { + // search for unique id in row data if found than its the row + // which error out on server side + var tmp = []; //_.findWhere needs array of object to work + tmp.push(d); + if (_.findWhere(tmp, _rowid)) { + _idx = count; + // Now exit the loop by returning true + return true; + } + count++; + }); + + // Not able to find in grid Data + return _idx; + }, + + // Save as + _save_as: function () { + return this._save(true); }, - on_unindent_code: function() { + // Set panel title. + setTitle: function (title) { var self = this; - // Trigger the comment signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:unindent_selected_code', - self, - self.handler - ); + + if (self.is_new_browser_tab) { + window.document.title = title; + } else { + _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function (p) { + if (p.isVisible()) { + p.title(decodeURIComponent(title)); + } + }); + } }, - // Callback function for the clear button click. - on_clear: function(ev) { - var self = this, sql; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + // load select file dialog + _load_file: function () { + var self = this; /* If is_query_changed flag is set to false then no need to * confirm with the user for unsaved changes. */ - if (self.handler.is_query_changed) { - alertify.confirm( - gettext("Unsaved changes"), + if (self.is_query_changed) { + alertify.confirm(gettext("Unsaved changes"), gettext("Are you sure you wish to discard the current changes?"), - function() { - // Do nothing as user do not want to save, just continue - self.query_tool_obj.setValue(''); + function () { + // User do not want to save, just continue + self._open_select_file_manager(); }, - function() { + function () { return true; } - ).set('labels', {ok:'Yes', cancel:'No'}); + ).set('labels', {ok: 'Yes', cancel: 'No'}); } else { - self.query_tool_obj.setValue(''); + self._open_select_file_manager(); } - }, - // Callback function for the clear history button click. - on_clear_history: function(ev) { - var self = this; - this._stopEventPropogation(ev); - this._closeDropDown(ev); - // ask for confirmation only if anything to clear - if(!self.history_collection.length()) { return; } - - alertify.confirm(gettext("Clear history"), - gettext("Are you sure you wish to clear the history?"), - function() { - if (self.history_collection) { - self.history_collection.reset(); - } - }, - function() { - return true; - } - ).set('labels', {ok:'Yes', cancel:'No'}); }, - // Callback function for the auto commit button click. - on_auto_commit: function(ev) { - var self = this; + // Open FileManager + _open_select_file_manager: function () { + var params = { + 'supported_types': ["sql"], // file types allowed + 'dialog_type': 'select_file' // open select file dialog + } + pgAdmin.FileManager.init(); + pgAdmin.FileManager.show_dialog(params); + }, - this._stopEventPropogation(ev); + // read file data and return as response + _select_file_handler: function (e) { + var self = this, + data = { + 'file_name': decodeURI(e) + }; - // Trigger the auto-commit signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:auto_commit', - self, - self.handler + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Loading the file...") ); - }, + // set cursor to progress before file load + var $busy_icon_div = $('.sql-editor-busy-fetching'); + $busy_icon_div.addClass('show_progress'); - // Callback function for the auto rollback button click. - on_auto_rollback: function(ev) { - var self = this; + // Make ajax call to load the data from file + $.ajax({ + url: url_for('sqleditor.load_file'), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(data), + success: function (res) { + self.gridView.query_tool_obj.setValue(res); + self.gridView.current_file = e; + self.setTitle(self.gridView.current_file.split('\\').pop().split('/').pop()); + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + // hide cursor + $busy_icon_div.removeClass('show_progress'); - this._stopEventPropogation(ev); + // disable save button on file save + $("#btn-save").prop('disabled', true); + $("#btn-file-menu-save").css('display', 'none'); - // Trigger the download signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:auto_rollback', - self, - self.handler - ); - }, + // Update the flag as new content is just loaded. + self.is_query_changed = false; + }, + error: function(e) { + var errmsg = $.parseJSON(e.responseText).errormsg; + alertify.error(errmsg); + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + // hide cursor + $busy_icon_div.removeClass('show_progress'); + } + }); + }, - // Callback function for explain button click. - on_explain: function(event) { - this._stopEventPropogation(event); - this._closeDropDown(event); + // read data from codemirror and write to file + _save_file_handler: function (e) { + var self = this, + data = { + 'file_name': decodeURI(e), + 'file_content': self.gridView.query_tool_obj.getValue() + }; + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Saving the queries in the file...") + ); - this.handler.explain(event); - }, + // Make ajax call to save the data to file + $.ajax({ + url: url_for('sqleditor.save_file'), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(data), + success: function(res) { + if (res.data.status) { + alertify.success(gettext("File saved successfully.")); + self.gridView.current_file = e; + self.setTitle(self.gridView.current_file.replace(/^.*[\\\/]/g, '')); + // disable save button on file save + $("#btn-save").prop('disabled', true); + $("#btn-file-menu-save").css('display', 'none'); - // Callback function for explain analyze button click. - on_explain_analyze: function(event) { - this._stopEventPropogation(event); - this._closeDropDown(event); + // Update the flag as query is already saved. + self.is_query_changed = false; + } + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + if (self.close_on_save) { + self.close() + } + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); - this.handler.explainAnalyze(event); - }, + var errmsg = $.parseJSON(e.responseText).errormsg; + setTimeout( + function() { + alertify.error(errmsg); + }, 10 + ); + }, + }); + }, - // Callback function for explain option "verbose" button click - on_explain_verbose: function(ev) { + // codemirror text change event + _on_query_change: function (query_tool_obj) { var self = this; - this._stopEventPropogation(ev); + if (!self.is_query_changed) { + // Update the flag as query is going to changed. + self.is_query_changed = true; - // Trigger the explain "verbose" signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:explain-verbose', - self, - self.handler - ); - }, + if (self.gridView.current_file) { + var title = self.gridView.current_file.replace(/^.*[\\\/]/g, '') + ' *' + self.setTitle(title); + } else { + var title = ''; - // Callback function for explain option "costs" button click - on_explain_costs: function(ev) { - var self = this; + if (self.is_new_browser_tab) { + title = window.document.title + ' *'; + } else { + // Find the title of the visible panel + _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function (p) { + if (p.isVisible()) { + self.gridView.panel_title = p._title; + } + }); - this._stopEventPropogation(ev); + title = self.gridView.panel_title + ' *'; + } + self.setTitle(title); + } - // Trigger the explain "costs" signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:explain-costs', - self, - self.handler - ); + $("#btn-save").prop('disabled', false); + $("#btn-file-menu-save").css('display', 'block'); + $("#btn-file-menu-dropdown").prop('disabled', false); + } }, - // Callback function for explain option "buffers" button click - on_explain_buffers: function(ev) { + // This function will set the required flag for polling response data + _init_polling_flags: function () { var self = this; - this._stopEventPropogation(ev); - - // Trigger the explain "buffers" signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:explain-buffers', - self, - self.handler - ); + // To get a timeout for polling fallback timer in seconds in + // regards to elapsed time + self.POLL_FALLBACK_TIME = function () { + var seconds = parseInt((Date.now() - self.query_start_time.getTime()) / 1000); + // calculate & return fall back polling timeout + if (seconds >= 10 && seconds < 30) { + return 500; + } + else if (seconds >= 30 && seconds < 60) { + return 1000; + } + else if (seconds >= 60 && seconds < 90) { + return 2000; + } + else if (seconds >= 90) { + return 5000; + } + else + return 1; + } }, - // Callback function for explain option "timing" button click - on_explain_timing: function(ev) { + // This function will show the filter in the text area. + _show_filter: function () { var self = this; - this._stopEventPropogation(ev); - - // Trigger the explain "timing" signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:explain-timing', - self, - self.handler + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Loading the existing filter options...") ); - }, + $.ajax({ + url: url_for('sqleditor.get_filter', {'trans_id': self.transId}), + method: 'GET', + success: function (res) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + if (res.data.status) { + $('#filter').removeClass('hidden'); + $('#editor-panel').addClass('sql-editor-busy-fetching'); + self.gridView.filter_obj.refresh(); + + if (res.data.result == null) + self.gridView.filter_obj.setValue(''); + else + self.gridView.filter_obj.setValue(res.data.result); + } else { + setTimeout( + function () { + alertify.alert('Get Filter Error', res.data.result); + }, 10 + ); + } + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); - do_not_close_menu: function(ev) { - ev.stopPropagation(); + var msg; + if (e.readyState == 0) { + msg = + gettext("Not connected to the server or the connection to the server has been closed.") + } else { + msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; + } + setTimeout( + function () { + alertify.alert('Get Filter Error', msg); + }, 10 + ); + } + }); }, - // callback function for load file button click. - on_file_load: function(ev) { - var self = this; + // This function will include the filter by selection. + _include_filter: function () { + var self = this, + data = {}, grid, active_column, column_info, _values; - this._stopEventPropogation(ev); - this._closeDropDown(ev); + grid = self.slickgrid; + active_column = grid.getActiveCell(); - // Trigger the save signal to the SqlEditorController class - self.handler.trigger( - 'pgadmin-sqleditor:button:load_file', - self, - self.handler - ); - }, + // If no cell is selected then return from the function + if (_.isNull(active_column) || _.isUndefined(active_column)) + return; - on_download: function() { - this.handler.download(); - }, + column_info = grid.getColumns()[active_column.cell] - keyAction: function(event) { - keyboardShortcuts.processEvent(this.handler, event); - }, - }); + // Fetch current row data from grid + _values = grid.getDataItem(active_column.row, active_column.cell) + if (_.isNull(_values) || _.isUndefined(_values)) + return; - /* Defining controller class for data grid, which actually - * perform the operations like executing the sql query, poll the result, - * render the data in the grid, Save/Refresh the data etc... - */ - var SqlEditorController = function(container, options) { - this.initialize.apply(this, arguments); - }; - - _.extend( - SqlEditorController.prototype, - Backbone.Events, - { - initialize: function(container, opts) { - this.container = container; - }, + // Add column position and it's value to data + data[column_info.field] = _values[column_info.field] || ''; - /* This function is used to create instance of SQLEditorView, - * call the render method of the grid view to render the backgrid - * header and loading icon and start execution of the sql query. - */ - start: function(is_query_tool, editor_title, script_sql, is_new_browser_tab) { - var self = this; + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Applying the new filter...") + ); - self.is_query_tool = is_query_tool; - self.rows_affected = 0; - self.marked_line_no = 0; - self.explain_verbose = false; - self.explain_costs = false; - self.explain_buffers = false; - self.explain_timing = false; - self.is_new_browser_tab = is_new_browser_tab; - self.has_more_rows = false; - self.fetching_rows = false; - self.close_on_save = false; + // Make ajax call to include the filter by selection + $.ajax({ + url: url_for('sqleditor.inclusive_filter', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(data), + success: function (res) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (res.data.status) { + // Refresh the sql grid + queryToolActions.executeQuery(self); + } + else { + alertify.alert('Filter By Selection Error', res.data.result); + } + } + ); + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (e.readyState == 0) { + alertify.alert('Filter By Selection Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; + } - // We do not allow to call the start multiple times. - if (self.gridView) - return; + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - self.gridView = new SQLEditorView({ - el: self.container, - handler: self - }); - self.transId = self.gridView.transId = self.container.data('transId'); + alertify.alert('Filter By Selection Error', msg); + }, 10 + ); + } + }); + }, - self.gridView.editor_title = _.unescape(editor_title); - self.gridView.current_file = undefined; + // This function will exclude the filter by selection. + _exclude_filter: function () { + var self = this, + data = {}, grid, active_column, column_info, _values; - // Render the header - self.gridView.render(); + grid = self.slickgrid; + active_column = grid.getActiveCell(); - // Listen to the file manager button events - pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:select_file', self._select_file_handler, self); - pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', self._save_file_handler, self); + // If no cell is selected then return from the function + if (_.isNull(active_column) || _.isUndefined(active_column)) + return; - // Listen to the codemirror on text change event - // only in query editor tool - if (self.is_query_tool) { - self.get_preferences(); - self.gridView.query_tool_obj.on('change', self._on_query_change.bind(self)); - } + column_info = grid.getColumns()[active_column.cell] - // Listen on events come from SQLEditorView for the button clicked. - self.on('pgadmin-sqleditor:button:load_file', self._load_file, self); - self.on('pgadmin-sqleditor:button:save', self._save, self); - self.on('pgadmin-sqleditor:button:deleterow', self._delete, self); - self.on('pgadmin-sqleditor:button:show_filter', self._show_filter, self); - self.on('pgadmin-sqleditor:button:include_filter', self._include_filter, self); - self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self); - self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self); - self.on('pgadmin-sqleditor:button:apply_filter', self._apply_filter, self); - self.on('pgadmin-sqleditor:button:copy_row', self._copy_row, self); - self.on('pgadmin-sqleditor:button:paste_row', self._paste_row, self); - self.on('pgadmin-sqleditor:button:limit', self._set_limit, self); - self.on('pgadmin-sqleditor:button:cancel-query', self._cancel_query, self); - self.on('pgadmin-sqleditor:button:auto_rollback', self._auto_rollback, self); - self.on('pgadmin-sqleditor:button:auto_commit', self._auto_commit, self); - self.on('pgadmin-sqleditor:button:explain-verbose', self._explain_verbose, self); - self.on('pgadmin-sqleditor:button:explain-costs', self._explain_costs, self); - self.on('pgadmin-sqleditor:button:explain-buffers', self._explain_buffers, self); - self.on('pgadmin-sqleditor:button:explain-timing', self._explain_timing, self); - // Indentation related - self.on('pgadmin-sqleditor:indent_selected_code', self._indent_selected_code, self); - self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self); - - if (self.is_query_tool) { - self.gridView.query_tool_obj.refresh(); - if(script_sql && script_sql !== '') { - self.gridView.query_tool_obj.setValue(script_sql); - } - } - else { - // Disable codemirror by setting cursor to nocursor and background to dark. - self.gridView.query_tool_obj.setOption("readOnly", 'nocursor'); - var cm = self.gridView.query_tool_obj.getWrapperElement(); - if (cm) { - cm.className += ' bg-gray-1 opacity-5'; - } - self.disable_tool_buttons(true); - self._execute_data_query(); - } - }, - - // This function checks if there is any dirty data in the grid before - // it executes the sql query - _execute_data_query: function() { - var self = this; - - // Check if the data grid has any changes before running query - if(_.has(self, 'data_store') && - ( _.size(self.data_store.added) || - _.size(self.data_store.updated) || - _.size(self.data_store.deleted)) - ) { - alertify.confirm(gettext("Unsaved changes"), - gettext("The data has been modified, but not saved. Are you sure you wish to discard the changes?"), - function(){ - // Do nothing as user do not want to save, just continue - self._run_query(); - }, - function(){ - // Stop, User wants to save - return true; - } - ).set('labels', {ok:'Yes', cancel:'No'}); - } else { - self._run_query(); - } - }, + // Fetch current row data from grid + _values = grid.getDataItem(active_column.row, active_column.cell) + if (_.isNull(_values) || _.isUndefined(_values)) + return; - // This function makes the ajax call to execute the sql query. - _run_query: function() { - var self = this; - self.query_start_time = new Date(); - self.rows_affected = 0; - self._init_polling_flags(); - // keep track of newly added rows - self.rows_to_disable = new Array(); - // Temporarily hold new rows added - self.temp_new_rows = new Array(); - self.has_more_rows = false; - self.fetching_rows = false; + // Add column position and it's value to data + data[column_info.field] = _values[column_info.field] || ''; - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Initializing query execution.") - ); + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Applying the new filter...") + ); - $("#btn-flash").prop('disabled', true); + // Make ajax call to exclude the filter by selection. + $.ajax({ + url: url_for('sqleditor.exclusive_filter', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(data), + success: function (res) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (res.data.status) { + // Refresh the sql grid + queryToolActions.executeQuery(self); + } + else { + alertify.alert('Filter Exclude Selection Error', res.data.result); + } + }, 10 + ); + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.trigger( - 'pgadmin-sqleditor:loading-icon:message', - gettext("Waiting for the query execution to complete...") - ); + setTimeout( + function () { + if (e.readyState == 0) { + alertify.alert('Filter Exclude Selection Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; + } - $.ajax({ - url: url_for('sqleditor.view_data_start', {'trans_id': self.transId}), - method: 'GET', - success: function(res) { - if (res.data.status) { + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - self.can_edit = res.data.can_edit; - self.can_filter = res.data.can_filter; - self.info_notifier_timeout = res.data.info_notifier_timeout; + alertify.alert('Filter Exclude Selection Error', msg); + }, 10 + ); + } + }); + }, - // Set the sql query to the SQL panel - self.gridView.query_tool_obj.setValue(res.data.sql); - self.query = res.data.sql; + // This function will remove the filter. + _remove_filter: function () { + var self = this; + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Removing the filter...") + ); - /* If filter is applied then remove class 'btn-default' - * and add 'btn-warning' to change the colour of the button. - */ - if (self.can_filter && res.data.filter_applied) { - $('#btn-filter').removeClass('btn-default'); - $('#btn-filter-dropdown').removeClass('btn-default'); - $('#btn-filter').addClass('btn-warning'); - $('#btn-filter-dropdown').addClass('btn-warning'); + // Make ajax call to exclude the filter by selection. + $.ajax({ + url: url_for('sqleditor.remove_filter', {'trans_id': self.transId}), + method: 'POST', + success: function (res) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (res.data.status) { + // Refresh the sql grid + queryToolActions.executeQuery(self); } else { - $('#btn-filter').removeClass('btn-warning'); - $('#btn-filter-dropdown').removeClass('btn-warning'); - $('#btn-filter').addClass('btn-default'); - $('#btn-filter-dropdown').addClass('btn-default'); + alertify.alert('Remove Filter Error', res.data.result); + } + } + ); + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (e.readyState == 0) { + alertify.alert('Remove Filter Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; } - $("#btn-save").prop('disabled', true); - $("#btn-file-menu-dropdown").prop('disabled', true); - $("#btn-copy-row").prop('disabled', true); - $("#btn-paste-row").prop('disabled', true); - // Set the combo box value - $(".limit").val(res.data.limit); + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - // If status is True then poll the result. - self._poll(); - } - else { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.update_msg_history(false, res.data.result); - } - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - if (e.readyState == 0) { - self.update_msg_history(false, - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; + alertify.alert('Remove Filter Error', msg); } + ); + } + }); + }, - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; + // This function will apply the filter. + _apply_filter: function () { + var self = this, + sql = self.gridView.filter_obj.getValue(); - self.update_msg_history(false, msg); - } - }); - }, + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Applying the filter...") + ); - // This is a wrapper to call_render function - // We need this because we have separated columns route & result route - // We need to combine both result here in wrapper before rendering grid - call_render_after_poll: function(res) { - var self = this; - self.query_end_time = new Date(); - self.rows_affected = res.rows_affected, - self.has_more_rows = res.has_more_rows; + // Make ajax call to include the filter by selection + $.ajax({ + url: url_for('sqleditor.apply_filter', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(sql), + success: function (res) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (res.data.status) { + $('#filter').addClass('hidden'); + $('#editor-panel').removeClass('sql-editor-busy-fetching'); + // Refresh the sql grid + queryToolActions.executeQuery(self); + } + else { + alertify.alert('Apply Filter Error', res.data.result); + } + }, 10 + ); + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (e.readyState == 0) { + alertify.alert('Apply Filter Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; + } - /* If no column information is available it means query - runs successfully with no result to display. In this - case no need to call render function. - */ - if (res.colinfo != null) - self._render(res); - else { - // Show message in message and history tab in case of query tool - self.total_time = self.get_query_run_time(self.query_start_time, self.query_end_time); - var msg = S(gettext("Query returned successfully in %s.")).sprintf(self.total_time).value(); - res.result += "\n\n" + msg; - self.update_msg_history(true, res.result, false); - // Display the notifier if the timeout is set to >= 0 - if (self.info_notifier_timeout >= 0) { - alertify.success(msg, self.info_notifier_timeout); - } - } + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - // Enable/Disable query tool button only if is_query_tool is true. - if (self.is_query_tool) { - self.disable_tool_buttons(false); - $("#btn-cancel-query").prop('disabled', true); + alertify.alert('Apply Filter Error', msg); + }, 10 + ); } - is_query_running = false; - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - }, + }); + }, + + // This function will copy the selected row. + _copy_row: copyData, + + // This function will paste the selected row. + _paste_row: function () { + var self = this, col_info = {}, + grid = self.slickgrid, + dataView = grid.getData(), + data = dataView.getItems(), + count = dataView.getLength(), + rows = grid.getSelectedRows().sort( + function (a, b) { + return a - b; + } + ), + copied_rows = rows.map(function (rowIndex) { + return data[rowIndex]; + }); + rows = rows.length == 0 ? self.last_copied_rows : rows - /* This function makes the ajax call to poll the result, - * if status is Busy then recursively call the poll function - * till the status is 'Success' or 'NotConnected'. If status is - * 'Success' then call the render method to render the data. - */ - _poll: function() { - var self = this; - - setTimeout( - function() { - $.ajax({ - url: url_for('sqleditor.poll', {'trans_id': self.transId}), - method: 'GET', - success: function(res) { - if (res.data.status === 'Success') { - self.trigger( - 'pgadmin-sqleditor:loading-icon:message', - gettext("Loading data from the database server and rendering...") - ); - - self.call_render_after_poll(res.data); - } - else if (res.data.status === 'Busy') { - // If status is Busy then poll the result by recursive call to the poll function - self._poll(); - is_query_running = true; - if (res.data.result) { - self.update_msg_history(res.data.status, res.data.result, false); - } - } - else if (res.data.status === 'NotConnected') { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - // Enable/Disable query tool button only if is_query_tool is true. - if (self.is_query_tool) { - self.disable_tool_buttons(false); - $("#btn-cancel-query").prop('disabled', true); - } - self.update_msg_history(false, res.data.result, true); - } - else if (res.data.status === 'Cancel') { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.update_msg_history(false, "Execution Cancelled!", true) - } - }, - error: function(e) { - // Enable/Disable query tool button only if is_query_tool is true. - self.resetQueryHistoryObject(self); - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - if (self.is_query_tool) { - self.disable_tool_buttons(false); - $("#btn-cancel-query").prop('disabled', true); - } + self.last_copied_rows = rows; - if (e.readyState == 0) { - self.update_msg_history(false, - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } + // If there are rows to paste? + if (copied_rows.length > 0) { + // Enable save button so that user can + // save newly pasted rows on server + $("#btn-save").prop('disabled', false); - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; + var arr_to_object = function (arr) { + var obj = {}, + count = typeof(arr) == 'object' ? + Object.keys(arr).length : arr.length - self.update_msg_history(false, msg); - // Highlight the error in the sql panel - self._highlight_error(msg); + _.each(arr, function (val, i) { + if (arr[i] !== undefined) { + if (_.isObject(arr[i])) { + obj[String(i)] = JSON.stringify(arr[i]); + } else { + obj[String(i)] = arr[i]; } - }); - }, self.POLL_FALLBACK_TIME()); - }, + } + }); + return obj; + }; - /* This function is used to create the backgrid columns, - * create the Backbone PageableCollection and finally render - * the data in the backgrid. - */ - _render: function(data) { - var self = this; - self.colinfo = data.col_info; - self.primary_keys = data.primary_keys; - self.client_primary_key = data.client_primary_key; - self.cell_selected = false; - self.selected_model = null; - self.changedModels = []; - $('.sql-editor-explain').empty(); - - /* If object don't have primary keys then set the - * can_edit flag to false. - */ - if (self.primary_keys === null || self.primary_keys === undefined - || _.size(self.primary_keys) === 0) - self.can_edit = false; - else - self.can_edit = true; + // Generate Unique key for each pasted row(s) + // Convert array values to object to send to server + // Add flag is_row_copied to handle [default] and [null] + // for copied rows. + // Add index of copied row into temp_new_rows + // Trigger grid.onAddNewRow when a row is copied + // Reset selection + + dataView.beginUpdate(); + _.each(copied_rows, function (row) { + var new_row = arr_to_object(row), + _key = (self.gridView.client_primary_key_counter++).toString(); + new_row.is_row_copied = true; + self.temp_new_rows.push(count); + new_row[self.client_primary_key] = _key; + dataView.addItem(new_row); + self.data_store.added[_key] = {'err': false, 'data': new_row}; + self.data_store.added_index[count] = _key; + count++; + }); + dataView.endUpdate(); + grid.updateRowCount(); + // Pasted row/s always append so bring last row in view port. + grid.scrollRowIntoView(dataView.getLength()); + grid.setSelectedRows([]); + } + }, - /* If user can filter the data then we should enabled - * Filter and Limit buttons. - */ - if (self.can_filter) { - $(".limit").prop('disabled', false); - $(".limit").addClass('limit-enabled'); - $("#btn-filter").prop('disabled', false); - $("#btn-filter-dropdown").prop('disabled', false); - } + // This function will set the limit for SQL query + _set_limit: function () { + var self = this, + limit = parseInt($(".limit").val()); - // Initial settings for delete row, copy row and paste row buttons. - $("#btn-delete-row").prop('disabled', true); - // Do not disable save button in query tool - if(!self.is_query_tool && !self.can_edit) { - $("#btn-save").prop('disabled', true); - $("#btn-file-menu-dropdown").prop('disabled', true); - } - if (!self.can_edit) { - $("#btn-delete-row").prop('disabled', true); - $("#btn-copy-row").prop('disabled', true); - $("#btn-paste-row").prop('disabled', true); - } + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Setting the limit on the result...") + ); + // Make ajax call to change the limit + $.ajax({ + url: url_for('sqleditor.set_limit', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(limit), + success: function (res) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (res.data.status) { + // Refresh the sql grid + queryToolActions.executeQuery(self); + } + else + alertify.alert('Change limit Error', res.data.result); + }, 10 + ); + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + setTimeout( + function () { + if (e.readyState == 0) { + alertify.alert('Change limit Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; + } + + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - // Fetch the columns metadata - self._fetch_column_metadata.call( - self, data, function() { - var self = this; + alertify.alert('Change limit Error', msg); + }, 10 + ); + } + }); + }, - self.trigger( - 'pgadmin-sqleditor:loading-icon:message', - gettext("Loading data from the database server and rendering..."), - self - ); + // This function is used to enable/disable buttons + disable_tool_buttons: function (disabled) { + $("#btn-clear").prop('disabled', disabled); + $("#btn-query-dropdown").prop('disabled', disabled); + $("#btn-edit-dropdown").prop('disabled', disabled); + $("#btn-edit").prop('disabled', disabled); + $('#btn-load-file').prop('disabled', disabled); + }, - // Show message in message and history tab in case of query tool - self.total_time = self.get_query_run_time(self.query_start_time, self.query_end_time); - var msg1 = S(gettext("Successfully run. Total query runtime: %s.")).sprintf(self.total_time).value(); - var msg2 = S(gettext("%s rows affected.")).sprintf(self.rows_affected).value(); + // This function will fetch the sql query from the text box + // and execute the query. + execute: function (explain_prefix) { + var self = this, + sql = '', + history_msg = ''; - // Display the notifier if the timeout is set to >= 0 - if (self.info_notifier_timeout >= 0) { - alertify.success(msg1 + ' ' + msg2, self.info_notifier_timeout); - } + self.has_more_rows = false; + self.fetching_rows = false; - var _msg = msg1 + '\n' + msg2; + /* If code is selected in the code mirror then execute + * the selected part else execute the complete code. + */ + var selected_code = self.gridView.query_tool_obj.getSelection(); + if (selected_code.length > 0) + sql = selected_code; + else + sql = self.gridView.query_tool_obj.getValue(); + + // If it is an empty query, do nothing. + if (sql.length <= 0) return; + + self.trigger( + 'pgadmin-sqleditor:loading-icon:show', + gettext("Initializing the query execution!") + ); - // If there is additional messages from server then add it to message - if(!_.isNull(data.additional_messages) && - !_.isUndefined(data.additional_messages)) { - _msg = data.additional_messages + '\n' + _msg; - } + $("#btn-flash").prop('disabled', true); - self.update_msg_history(true, _msg, false); + if (explain_prefix != undefined && + !S.startsWith(sql.trim().toUpperCase(), "EXPLAIN")) { + sql = explain_prefix + ' ' + sql; + } - /* Add the data to the collection and render the grid. - * In case of Explain draw the graph on explain panel - * and add json formatted data to collection and render. - */ - var explain_data_array = []; - if ( - data.result && data.result.length >= 1 && - data.result[0] && data.result[0][0] && data.result[0][0][0] && - data.result[0][0][0].hasOwnProperty('Plan') && - _.isObject(data.result[0][0][0]['Plan']) - ) { - var explain_data = [JSON.stringify(data.result[0][0], null, 2)]; - explain_data_array.push(explain_data); - // Make sure - the 'Data Output' panel is visible, before - we - // start rendering the grid. - self.gridView.data_output_panel.focus(); - setTimeout( - function() { - self.gridView.render_grid( - explain_data_array, self.columns, self.can_edit, - self.client_primary_key - ); - // Make sure - the 'Explain' panel is visible, before - we - // start rendering the grid. - self.gridView.explain_panel.focus(); - pgExplain.DrawJSONPlan( - $('.sql-editor-explain'), data.result[0][0] - ); - }, 10 - ); - } else { - // Make sure - the 'Data Output' panel is visible, before - we - // start rendering the grid. - self.gridView.data_output_panel.focus(); - setTimeout( - function() { - self.gridView.render_grid(data.result, self.columns, - self.can_edit, self.client_primary_key, data.rows_affected); - }, 10 - ); - } + self.query_start_time = new Date(); + self.query = sql; + self.rows_affected = 0; + self._init_polling_flags(); + self.disable_tool_buttons(true); + $("#btn-cancel-query").prop('disabled', false); - // Hide the loading icon - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - $("#btn-flash").prop('disabled', false); - }.bind(self) - ); - }, + $.ajax({ + url: url_for('sqleditor.query_tool_start', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(sql), + success: function (res) { + // Remove marker + if (self.gridView.marker) { + self.gridView.marker.clear(); + delete self.gridView.marker; + self.gridView.marker = null; + + // Remove already existing marker + self.gridView.query_tool_obj.removeLineClass(self.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); + } - // This function creates the columns as required by the backgrid - _fetch_column_metadata: function(data, cb) { - var colinfo = data.colinfo, - primary_keys = data.primary_keys, - result = data.result, - columns = [], - self = this; - // Store pg_types in an array - var pg_types = new Array(); - _.each(data.types, function(r) { - pg_types[r.oid] = [r.typname]; - }); + if (res.data.status) { + self.trigger( + 'pgadmin-sqleditor:loading-icon:message', + gettext("Waiting for the query execution to complete...") + ); - // Create columns required by slick grid to render - _.each(colinfo, function(c) { - var is_primary_key = false; + self.can_edit = res.data.can_edit; + self.can_filter = res.data.can_filter; + self.info_notifier_timeout = res.data.info_notifier_timeout; - // Check whether table have primary key - if (_.size(primary_keys) > 0) { - _.each(primary_keys, function (value, key) { - if (key === c.name) - is_primary_key = true; - }); + // If status is True then poll the result. + self._poll(); } + else { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); + self.update_msg_history(false, res.data.result); - // To show column label and data type in multiline, - // The elements should be put inside the div. - // Create column label and type. - var col_type = '', - column_label = '', - col_cell; - var type = pg_types[c.type_code] ? - pg_types[c.type_code][0] : - // This is the case where user might - // have use casting so we will use type - // returned by cast function - pg_types[pg_types.length - 1][0] ? - pg_types[pg_types.length - 1][0] : 'unknown'; - - if (!is_primary_key) - col_type += type; - else - col_type += '[PK] ' + type; - - if (c.precision && c.precision >= 0 && c.precision != 65535) { - col_type += ' (' + c.precision; - col_type += c.scale && c.scale != 65535 ? - ',' + c.scale + ')': - ')'; + // Highlight the error in the sql panel + self._highlight_error(res.data.result); } + }, + error: function (e) { + self.trigger('pgadmin-sqleditor:loading-icon:hide'); + self.disable_tool_buttons(false); + $("#btn-cancel-query").prop('disabled', true); - // Identify cell type of column. - switch(type) { - case "json": - case "json[]": - case "jsonb": - case "jsonb[]": - col_cell = 'Json'; - break; - case "smallint": - case "integer": - case "bigint": - case "decimal": - case "numeric": - case "real": - case "double precision": - col_cell = 'number'; - break; - case "boolean": - col_cell = 'boolean'; - break; - case "character": - case "character[]": - case "character varying": - case "character varying[]": - if (c.internal_size && c.internal_size >= 0 && c.internal_size != 65535) { - // Update column type to display length on column header - col_type += ' (' + c.internal_size + ')'; - } - col_cell = 'string'; - break; - default: - col_cell = 'string'; + if (e.readyState == 0) { + self.update_msg_history(false, + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; } - column_label = c.display_name + '
' + col_type; - - var col = { - 'name': c.name, - 'display_name': c.display_name, - 'column_type': col_type, - 'column_type_internal': type, - 'pos': c.pos, - 'label': column_label, - 'cell': col_cell, - 'can_edit': self.can_edit, - 'type': type, - 'not_null': c.not_null, - 'has_default_val': c.has_default_val - }; - columns.push(col); - }); + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - self.columns = columns; - if (cb && typeof(cb) == 'function') { - cb(); + self.update_msg_history(false, msg); } - }, - - resetQueryHistoryObject: function (history) { - history.total_time = '-'; - }, + }); + }, - // This function is used to raise appropriate message. - update_msg_history: function(status, msg, clear_grid) { - var self = this; - if (clear_grid === undefined) - clear_grid = true; + /* This function is used to highlight the error line and + * underlining for the error word. + */ + _highlight_error: function (result) { + var self = this, + error_line_no = 0, + start_marker = 0, + end_marker = 0, + selected_line_no = 0; + + // Remove already existing marker + self.gridView.query_tool_obj.removeLineClass(self.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); + + // In case of selection we need to find the actual line no + if (self.gridView.query_tool_obj.getSelection().length > 0) + selected_line_no = self.gridView.query_tool_obj.getCursor(true).line; + + // Fetch the LINE string using regex from the result + var line = /LINE (\d+)/.exec(result), + // Fetch the Character string using regex from the result + char = /Character: (\d+)/.exec(result); + + // If line and character is null then no need to mark + if (line != null && char != null) { + error_line_no = self.marked_line_no = (parseInt(line[1]) - 1) + selected_line_no; + var error_char_no = (parseInt(char[1]) - 1); + + /* We need to loop through each line till the error line and + * count the total no of character to figure out the actual + * starting/ending marker point for the individual line. We + * have also added 1 per line for the "\n" character. + */ + var prev_line_chars = 0; + var loop_index = selected_line_no > 0 ? selected_line_no : 0; + for (var i = loop_index; i < error_line_no; i++) + prev_line_chars += self.gridView.query_tool_obj.getLine(i).length + 1; + + /* Marker starting point for the individual line is + * equal to error character index minus total no of + * character till the error line starts. + */ + start_marker = error_char_no - prev_line_chars; + + // Find the next space from the character or end of line + var error_line = self.gridView.query_tool_obj.getLine(error_line_no); + end_marker = error_line.indexOf(' ', start_marker); + if (end_marker < 0) + end_marker = error_line.length; + + // Mark the error text + self.gridView.marker = self.gridView.query_tool_obj.markText( + {line: error_line_no, ch: start_marker}, + {line: error_line_no, ch: end_marker}, + {className: "sql-editor-mark"} + ); - self.gridView.messages_panel.focus(); + self.gridView.query_tool_obj.addLineClass(self.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); + } + }, - if (clear_grid) { - // Delete grid - if (self.gridView.handler.slickgrid) { - self.gridView.handler.slickgrid.destroy(); + // This function will cancel the running query. + _cancel_query: function () { + var self = this; + $("#btn-cancel-query").prop('disabled', true); + $.ajax({ + url: url_for('sqleditor.cancel_transaction', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + success: function (res) { + if (res.data.status) { + self.disable_tool_buttons(false); } - // Misc cleaning - self.columns = undefined; - self.collection = undefined; - - $('.sql-editor-message').text(msg); - } else { - $('.sql-editor-message').append(_.escape(msg)); - } - - // Scroll automatically when msgs appends to element - setTimeout(function(){ - $(".sql-editor-message").scrollTop($(".sql-editor-message")[0].scrollHeight);; - }, 10); - - if(status != 'Busy') { - $("#btn-flash").prop('disabled', false); - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.gridView.history_collection.add({ - 'status' : status, - 'start_time': self.query_start_time, - 'query': self.query, - 'row_affected': self.rows_affected, - 'total_time': self.total_time, - 'message':msg, - }); - } - }, - - // This function will return the total query execution Time. - get_query_run_time: function (start_time, end_time) { - var self = this; - - // Calculate the difference in milliseconds - var difference_ms, miliseconds; - difference_ms = miliseconds = end_time.getTime() - start_time.getTime(); - //take out milliseconds - difference_ms = difference_ms/1000; - var seconds = Math.floor(difference_ms % 60); - difference_ms = difference_ms/60; - var minutes = Math.floor(difference_ms % 60); - - if (minutes > 0) - return minutes + ' min'; - else if (seconds > 0) { - return seconds + ' secs'; - } - else - return miliseconds + ' msec'; - }, - - /* This function is used to check whether cell - * is editable or not depending on primary keys - * and staged_rows flag - */ - is_editable: function(obj) { - var self = this; - if (obj instanceof Backbone.Collection) - return false; - return (self.get('can_edit')); - }, - - rows_to_delete: function(data) { - var self = this, - tmp_keys = self.primary_keys; - - // re-calculate rows with no primary keys - self.temp_new_rows = []; - data.forEach(function(d, idx) { - var p_keys_list = _.pick(d, tmp_keys), - is_primary_key = Object.keys(p_keys_list).length ? - p_keys_list[0] : undefined; - - if (!is_primary_key) { - self.temp_new_rows.push(idx); - } - }); - self.rows_to_disable = _.clone(self.temp_new_rows); - }, - - // This function will delete selected row. - _delete: function() { - var self = this, deleted_keys = [], - dgrid = document.getElementById("datagrid"), - is_added = _.size(self.data_store.added), - is_updated = _.size(self.data_store.updated); - - // Remove newly added rows from staged rows as we don't want to send them on server - if(is_added) { - _.each(self.data_store.added, function(val, key) { - if(key in self.data_store.staged_rows) { - // Remove the row from data store so that we do not send it on server - deleted_keys.push(key); - delete self.data_store.staged_rows[key]; - delete self.data_store.added[key]; - delete self.data_store.added_index[key]; - } - }); - } - // If only newly rows to delete and no data is there to send on server - // then just re-render the grid - if(_.size(self.data_store.staged_rows) == 0) { - var grid = self.slickgrid, - dataView = grid.getData(), - data = dataView.getItems(), - idx = 0; - - grid.resetActiveCell(); - - dataView.beginUpdate(); - for (var i = 0; i < deleted_keys.length; i++) { - dataView.deleteItem(deleted_keys[i]); - } - dataView.endUpdate(); - self.rows_to_delete.apply(self, [dataView.getItems()]); - grid.resetActiveCell(); - grid.setSelectedRows([]); - grid.invalidate(); - - // Nothing to copy or delete here - $("#btn-delete-row").prop('disabled', true); - $("#btn-copy-row").prop('disabled', true); - if(_.size(self.data_store.added) || is_updated) { - // Do not disable save button if there are - // any other changes present in grid data - $("#btn-save").prop('disabled', false); - } else { - $("#btn-save").prop('disabled', true); - } - alertify.success(gettext("Row(s) deleted")); - } else { - // There are other data to needs to be updated on server - if(is_updated) { - alertify.alert(gettext("Operation failed"), - gettext("There are unsaved changes in grid, Please save them first to avoid inconsistency in data") - ); - return; - } - alertify.confirm(gettext("Delete Row(s)"), - gettext("Are you sure you wish to delete selected row(s)?"), - function() { - $("#btn-delete-row").prop('disabled', true); - $("#btn-copy-row").prop('disabled', true); - // Change the state - self.data_store.deleted = self.data_store.staged_rows; - self.data_store.staged_rows = {}; - // Save the changes on server - self._save(); - }, - function() { - // Do nothing as user canceled the operation. - } - ).set('labels', {ok: gettext("Yes"), cancel:gettext("No")}); - } - }, - - /* This function will fetch the list of changed models and make - * the ajax call to save the data into the database server. - * and will open save file dialog conditionally. - */ - _save: function(view, controller, save_as) { - var self = this, - data = [], - save_data = true; - - // Open save file dialog if query tool - if (self.is_query_tool) { - var current_file = self.gridView.current_file; - if (!_.isUndefined(current_file) && !save_as) { - self._save_file_handler(current_file); - } - else { - // provide custom option to save file dialog - var params = { - 'supported_types': ["*", "sql"], - 'dialog_type': 'create_file', - 'dialog_title': 'Save File', - 'btn_primary': 'Save' - } - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(params); - } - return; - } - $("#btn-save").prop('disabled', true); - $("#btn-file-menu-dropdown").prop('disabled', true); - - var is_added = _.size(self.data_store.added), - is_updated = _.size(self.data_store.updated), - is_deleted = _.size(self.data_store.deleted), - is_primary_error = false; - - if( !is_added && !is_updated && !is_deleted ) { - return; // Nothing to save here - } - - if (save_data) { - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Saving the updated data...") - ); - - // Add the columns to the data so the server can remap the data - var req_data = self.data_store; - req_data.columns = view ? view.handler.columns : self.columns; - - // Make ajax call to save the data - $.ajax({ - url: url_for('sqleditor.save', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(req_data), - success: function(res) { - var grid = self.slickgrid, - dataView = grid.getData(), - data_length = dataView.getLength(), - data = []; - if (res.data.status) { - // Remove flag is_row_copied from copied rows - _.each(data, function(row, idx) { - if (row.is_row_copied) { - delete row.is_row_copied; - } - }); - - // Remove 2d copied_rows array - if (grid.copied_rows) { - delete grid.copied_rows; - } - - // Remove deleted rows from client as well - if(is_deleted) { - var rows = grid.getSelectedRows(); - if(data_length == rows.length) { - // This means all the rows are selected, clear all data - data = []; - dataView.setItems(data, self.client_primary_key); - } else { - dataView.beginUpdate(); - for (var i = 0; i < rows.length; i++) { - var item = grid.getDataItem(rows[i]); - data.push(item); - dataView.deleteItem(item[self.client_primary_key]); - } - dataView.endUpdate(); - } - self.rows_to_delete.apply(self, [data]); - grid.setSelectedRows([]); - } - - // whether a cell is editable or not is decided in - // grid.onBeforeEditCell function (on cell click) - // but this function should do its job after save - // operation. So assign list of added rows to original - // rows_to_disable array. - if (is_added) { - self.rows_to_disable = _.clone(self.temp_new_rows); - } - - grid.setSelectedRows([]); - // Reset data store - self.data_store = { - 'added': {}, - 'updated': {}, - 'deleted': {}, - 'added_index': {}, - 'updated_index': {} - } - - // Reset old primary key data now - self.primary_keys_data = {}; - - // Clear msgs after successful save - $('.sql-editor-message').html(''); - } else { - // Something went wrong while saving data on the db server - $("#btn-flash").prop('disabled', false); - $('.sql-editor-message').text(res.data.result); - var err_msg = S(gettext("%s.")).sprintf(res.data.result).value(); - alertify.error(err_msg, 20); - grid.setSelectedRows([]); - // To highlight the row at fault - if(_.has(res.data, '_rowid') && - (!_.isUndefined(res.data._rowid)|| !_.isNull(res.data._rowid))) { - var _row_index = self._find_rowindex(res.data._rowid); - if(_row_index in self.data_store.added_index) { - // Remove new row index from temp_list if save operation - // fails - var index = self.handler.temp_new_rows.indexOf(res.data._rowid); - if (index > -1) { - self.handler.temp_new_rows.splice(index, 1); - } - self.data_store.added[self.data_store.added_index[_row_index]].err = true - } else if (_row_index in self.data_store.updated_index) { - self.data_store.updated[self.data_store.updated_index[_row_index]].err = true - } - } - grid.gotoCell(_row_index, 1); - } - - // Update the sql results in history tab - _.each(res.data.query_result, function(r) { - self.gridView.history_collection.add({ - 'status': r.status, - 'start_time': self.query_start_time, - 'query': r.sql, - 'row_affected': r.rows_affected, - 'total_time': self.total_time, - 'message': r.result, - }); - }); - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - - grid.invalidate(); - alertify.success(gettext("Data saved successfully.")); - if (self.close_on_save) { - self.close(); - } - }, - error: function(e) { - if (e.readyState == 0) { - self.update_msg_history(false, - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - self.update_msg_history(false, msg); - } - }); - } - }, - - // Find index of row at fault from grid data - _find_rowindex: function(rowid) { - var self = this, - grid = self.slickgrid, - dataView = grid.getData(), - data = dataView.getItems(), - _rowid, - count = 0, - _idx = -1; - - // If _rowid is object then it's update/delete operation - if(_.isObject(rowid)) { - _rowid = rowid; - } else if (_.isString(rowid)) { // Insert operation - var rowid = {}; - rowid[self.client_primary_key]= rowid; - _rowid = rowid; - } else { - // Something is wrong with unique id - return _idx; - } - - _.find(data, function(d) { - // search for unique id in row data if found than its the row - // which error out on server side - var tmp = []; //_.findWhere needs array of object to work - tmp.push(d); - if(_.findWhere(tmp, _rowid)) { - _idx = count; - // Now exit the loop by returning true - return true; - } - count++; - }); - - // Not able to find in grid Data - return _idx; - }, - - // Save as - _save_as: function() { - return this._save(true); - }, - - // Set panel title. - setTitle: function(title) { - var self = this; - - if (self.is_new_browser_tab) { - window.document.title = title; - } else { - _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function(p) { - if(p.isVisible()) { - p.title(decodeURIComponent(title)); - } - }); - } - }, - - // load select file dialog - _load_file: function() { - var self = this; - - /* If is_query_changed flag is set to false then no need to - * confirm with the user for unsaved changes. - */ - if (self.is_query_changed) { - alertify.confirm(gettext("Unsaved changes"), - gettext("Are you sure you wish to discard the current changes?"), - function() { - // User do not want to save, just continue - self._open_select_file_manager(); - }, - function() { - return true; - } - ).set('labels', {ok:'Yes', cancel:'No'}); - } else { - self._open_select_file_manager(); - } - - }, - - // Open FileManager - _open_select_file_manager: function() { - var params = { - 'supported_types': ["sql"], // file types allowed - 'dialog_type': 'select_file' // open select file dialog - } - pgAdmin.FileManager.init(); - pgAdmin.FileManager.show_dialog(params); - }, - - // read file data and return as response - _select_file_handler: function(e) { - var self = this, - data = { - 'file_name': decodeURI(e) - }; - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Loading the file...") - ); - // set cursor to progress before file load - var $busy_icon_div = $('.sql-editor-busy-fetching'); - $busy_icon_div.addClass('show_progress'); - - // Make ajax call to load the data from file - $.ajax({ - url: url_for('sqleditor.load_file'), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - self.gridView.query_tool_obj.setValue(res); - self.gridView.current_file = e; - self.setTitle(self.gridView.current_file.split('\\').pop().split('/').pop()); - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - // hide cursor - $busy_icon_div.removeClass('show_progress'); - - // disable save button on file save - $("#btn-save").prop('disabled', true); - $("#btn-file-menu-save").css('display', 'none'); - - // Update the flag as new content is just loaded. - self.is_query_changed = false; - }, - error: function(e) { - var errmsg = $.parseJSON(e.responseText).errormsg; - alertify.error(errmsg); - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - // hide cursor - $busy_icon_div.removeClass('show_progress'); - } - }); - }, - - // read data from codemirror and write to file - _save_file_handler: function(e) { - var self = this, - data = { - 'file_name': decodeURI(e), - 'file_content': self.gridView.query_tool_obj.getValue() - }; - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Saving the queries in the file...") - ); - - // Make ajax call to save the data to file - $.ajax({ - url: url_for('sqleditor.save_file'), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - if (res.data.status) { - alertify.success(gettext("File saved successfully.")); - self.gridView.current_file = e; - self.setTitle(self.gridView.current_file.replace(/^.*[\\\/]/g, '')); - // disable save button on file save - $("#btn-save").prop('disabled', true); - $("#btn-file-menu-save").css('display', 'none'); - - // Update the flag as query is already saved. - self.is_query_changed = false; - } - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - if (self.close_on_save) { - self.close() - } - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - - var errmsg = $.parseJSON(e.responseText).errormsg; - setTimeout( - function() { - alertify.error(errmsg); - }, 10 - ); - }, - }); - }, - - // codemirror text change event - _on_query_change: function(query_tool_obj) { - var self = this; - - if (!self.is_query_changed) { - // Update the flag as query is going to changed. - self.is_query_changed = true; - - if(self.gridView.current_file) { - var title = self.gridView.current_file.replace(/^.*[\\\/]/g, '') + ' *' - self.setTitle(title); - } else { - var title = ''; - - if (self.is_new_browser_tab) { - title = window.document.title + ' *'; - } else { - // Find the title of the visible panel - _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function(p) { - if(p.isVisible()) { - self.gridView.panel_title = p._title; - } - }); - - title = self.gridView.panel_title + ' *'; - } - self.setTitle(title); - } - - $("#btn-save").prop('disabled', false); - $("#btn-file-menu-save").css('display', 'block'); - $("#btn-file-menu-dropdown").prop('disabled', false); - } - }, - - - // This function will run the SQL query and refresh the data in the backgrid. - executeQuery: function() { - var self = this; - - // Start execution of the query. - if (self.is_query_tool) { - $('.sql-editor-message').html(''); - self._execute(); - } else { - self._execute_data_query(); - } - }, - - // This function will set the required flag for polling response data - _init_polling_flags: function() { - var self = this; - - // To get a timeout for polling fallback timer in seconds in - // regards to elapsed time - self.POLL_FALLBACK_TIME = function() { - var seconds = parseInt((Date.now() - self.query_start_time.getTime())/1000); - // calculate & return fall back polling timeout - if(seconds >= 10 && seconds < 30) { - return 500; - } - else if(seconds >= 30 && seconds < 60) { - return 1000; - } - else if(seconds >= 60 && seconds < 90) { - return 2000; - } - else if(seconds >= 90) { - return 5000; - } - else - return 1; - } - }, - - // This function will show the filter in the text area. - _show_filter: function() { - var self = this; - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Loading the existing filter options...") - ); - $.ajax({ - url: url_for('sqleditor.get_filter', {'trans_id': self.transId}), - method: 'GET', - success: function(res) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - if (res.data.status) { - $('#filter').removeClass('hidden'); - $('#editor-panel').addClass('sql-editor-busy-fetching'); - self.gridView.filter_obj.refresh(); - - if (res.data.result == null) - self.gridView.filter_obj.setValue(''); - else - self.gridView.filter_obj.setValue(res.data.result); - } else { - setTimeout( - function() { - alertify.alert('Get Filter Error', res.data.result); - }, 10 - ); - } - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - - var msg; - if (e.readyState == 0) { - msg = - gettext("Not connected to the server or the connection to the server has been closed.") - } else { - msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - } - setTimeout( - function() { - alertify.alert('Get Filter Error', msg); - }, 10 - ); - } - }); - }, - - // This function will include the filter by selection. - _include_filter: function () { - var self = this, - data = {}, grid, active_column, column_info, _values; - - grid = self.slickgrid; - active_column = grid.getActiveCell(); - - // If no cell is selected then return from the function - if (_.isNull(active_column) || _.isUndefined(active_column)) - return; - - column_info = grid.getColumns()[active_column.cell] - - // Fetch current row data from grid - _values = grid.getDataItem(active_column.row, active_column.cell) - if (_.isNull(_values) || _.isUndefined(_values)) - return; - - // Add column position and it's value to data - data[column_info.field] = _values[column_info.field] || ''; - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Applying the new filter...") - ); - - // Make ajax call to include the filter by selection - $.ajax({ - url: url_for('sqleditor.inclusive_filter', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (res.data.status) { - // Refresh the sql grid - self.executeQuery(); - } - else { - alertify.alert('Filter By Selection Error', res.data.result); - } - } - ); - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (e.readyState == 0) { - alertify.alert('Filter By Selection Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - alertify.alert('Filter By Selection Error', msg); - }, 10 - ); - } - }); - }, - - // This function will exclude the filter by selection. - _exclude_filter: function () { - var self = this, - data = {}, grid, active_column, column_info, _values; - - grid = self.slickgrid; - active_column = grid.getActiveCell(); - - // If no cell is selected then return from the function - if (_.isNull(active_column) || _.isUndefined(active_column)) - return; - - column_info = grid.getColumns()[active_column.cell] - - // Fetch current row data from grid - _values = grid.getDataItem(active_column.row, active_column.cell) - if (_.isNull(_values) || _.isUndefined(_values)) - return; - - // Add column position and it's value to data - data[column_info.field] = _values[column_info.field] || ''; - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Applying the new filter...") - ); - - // Make ajax call to exclude the filter by selection. - $.ajax({ - url: url_for('sqleditor.exclusive_filter', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (res.data.status) { - // Refresh the sql grid - self.executeQuery(); - } - else { - alertify.alert('Filter Exclude Selection Error', res.data.result); - } - }, 10 - ); - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - - setTimeout( - function() { - if (e.readyState == 0) { - alertify.alert('Filter Exclude Selection Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - alertify.alert('Filter Exclude Selection Error', msg); - }, 10 - ); - } - }); - }, - - // This function will remove the filter. - _remove_filter: function () { - var self = this; - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Removing the filter...") - ); - - // Make ajax call to exclude the filter by selection. - $.ajax({ - url: url_for('sqleditor.remove_filter', {'trans_id': self.transId}), - method: 'POST', - success: function(res) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (res.data.status) { - // Refresh the sql grid - self.executeQuery(); - } - else { - alertify.alert('Remove Filter Error', res.data.result); - } - } - ); - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (e.readyState == 0) { - alertify.alert('Remove Filter Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - alertify.alert('Remove Filter Error', msg); - } - ); - } - }); - }, - - // This function will apply the filter. - _apply_filter: function() { - var self = this, - sql = self.gridView.filter_obj.getValue(); - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Applying the filter...") - ); - - // Make ajax call to include the filter by selection - $.ajax({ - url: url_for('sqleditor.apply_filter', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(sql), - success: function(res) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (res.data.status) { - $('#filter').addClass('hidden'); - $('#editor-panel').removeClass('sql-editor-busy-fetching'); - // Refresh the sql grid - self.executeQuery(); - } - else { - alertify.alert('Apply Filter Error',res.data.result); - } - }, 10 - ); - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (e.readyState == 0) { - alertify.alert('Apply Filter Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - alertify.alert('Apply Filter Error', msg); - }, 10 - ); - } - }); - }, - - // This function will copy the selected row. - _copy_row: copyData, - - // This function will paste the selected row. - _paste_row: function() { - var self = this, col_info = {}, - grid = self.slickgrid, - dataView = grid.getData(), - data = dataView.getItems(), - count = dataView.getLength(), - rows = grid.getSelectedRows().sort( - function (a, b) { return a - b; } - ), - copied_rows = rows.map(function (rowIndex) { - return data[rowIndex]; - }); - - rows = rows.length == 0 ? self.last_copied_rows : rows - - self.last_copied_rows = rows; - - // If there are rows to paste? - if(copied_rows.length > 0) { - // Enable save button so that user can - // save newly pasted rows on server - $("#btn-save").prop('disabled', false); - - var arr_to_object = function (arr) { - var obj = {}, - count = typeof(arr) == 'object' ? - Object.keys(arr).length: arr.length - - _.each(arr, function(val, i){ - if (arr[i] !== undefined) { - if(_.isObject(arr[i])) { - obj[String(i)] = JSON.stringify(arr[i]); - } else { - obj[String(i)] = arr[i]; - } - } - }); - return obj; - }; - - // Generate Unique key for each pasted row(s) - // Convert array values to object to send to server - // Add flag is_row_copied to handle [default] and [null] - // for copied rows. - // Add index of copied row into temp_new_rows - // Trigger grid.onAddNewRow when a row is copied - // Reset selection - - dataView.beginUpdate(); - _.each(copied_rows, function(row) { - var new_row = arr_to_object(row), - _key = (self.gridView.client_primary_key_counter++).toString(); - new_row.is_row_copied = true; - self.temp_new_rows.push(count); - new_row[self.client_primary_key] = _key; - dataView.addItem(new_row); - self.data_store.added[_key] = {'err': false, 'data': new_row}; - self.data_store.added_index[count] = _key; - count++; - }); - dataView.endUpdate(); - grid.updateRowCount(); - // Pasted row/s always append so bring last row in view port. - grid.scrollRowIntoView(dataView.getLength()); - grid.setSelectedRows([]); - } - }, - - // This function will set the limit for SQL query - _set_limit: function() { - var self = this, - limit = parseInt($(".limit").val()); - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Setting the limit on the result...") - ); - // Make ajax call to change the limit - $.ajax({ - url: url_for('sqleditor.set_limit', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(limit), - success: function(res) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (res.data.status) { - // Refresh the sql grid - self.executeQuery(); - } - else - alertify.alert('Change limit Error', res.data.result); - }, 10 - ); - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - setTimeout( - function() { - if (e.readyState == 0) { - alertify.alert('Change limit Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - alertify.alert('Change limit Error', msg); - }, 10 - ); - } - }); - }, - - // This function is used to enable/disable buttons - disable_tool_buttons: function(disabled) { - $("#btn-clear").prop('disabled', disabled); - $("#btn-query-dropdown").prop('disabled', disabled); - $("#btn-edit-dropdown").prop('disabled', disabled); - $("#btn-edit").prop('disabled', disabled); - $('#btn-load-file').prop('disabled', disabled); - }, - - // This function will fetch the sql query from the text box - // and execute the query. - _execute: function (explain_prefix) { - var self = this, - sql = '', - history_msg = ''; - - self.has_more_rows = false; - self.fetching_rows = false; - - /* If code is selected in the code mirror then execute - * the selected part else execute the complete code. - */ - var selected_code = self.gridView.query_tool_obj.getSelection(); - if (selected_code.length > 0) - sql = selected_code; - else - sql = self.gridView.query_tool_obj.getValue(); - - // If it is an empty query, do nothing. - if (sql.length <= 0) return; - - self.trigger( - 'pgadmin-sqleditor:loading-icon:show', - gettext("Initializing the query execution!") - ); - - $("#btn-flash").prop('disabled', true); - - if (explain_prefix != undefined && - !S.startsWith(sql.trim().toUpperCase(), "EXPLAIN")) { - sql = explain_prefix + ' ' + sql; - } - - self.query_start_time = new Date(); - self.query = sql; - self.rows_affected = 0; - self._init_polling_flags(); - self.disable_tool_buttons(true); - $("#btn-cancel-query").prop('disabled', false); - - $.ajax({ - url: url_for('sqleditor.query_tool_start', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(sql), - success: function(res) { - // Remove marker - if (self.gridView.marker) { - self.gridView.marker.clear(); - delete self.gridView.marker; - self.gridView.marker = null; - - // Remove already existing marker - self.gridView.query_tool_obj.removeLineClass(self.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); - } - - if (res.data.status) { - self.trigger( - 'pgadmin-sqleditor:loading-icon:message', - gettext("Waiting for the query execution to complete...") - ); - - self.can_edit = res.data.can_edit; - self.can_filter = res.data.can_filter; - self.info_notifier_timeout = res.data.info_notifier_timeout; - - // If status is True then poll the result. - self._poll(); - } - else { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.disable_tool_buttons(false); - $("#btn-cancel-query").prop('disabled', true); - self.update_msg_history(false, res.data.result); - - // Highlight the error in the sql panel - self._highlight_error(res.data.result); - } - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - self.disable_tool_buttons(false); - $("#btn-cancel-query").prop('disabled', true); - - if (e.readyState == 0) { - self.update_msg_history(false, - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - self.update_msg_history(false, msg); - } - }); - }, - - /* This function is used to highlight the error line and - * underlining for the error word. - */ - _highlight_error: function(result) { - var self = this, - error_line_no = 0, - start_marker = 0, - end_marker = 0, - selected_line_no = 0; - - // Remove already existing marker - self.gridView.query_tool_obj.removeLineClass(self.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); - - // In case of selection we need to find the actual line no - if (self.gridView.query_tool_obj.getSelection().length > 0) - selected_line_no = self.gridView.query_tool_obj.getCursor(true).line; - - // Fetch the LINE string using regex from the result - var line = /LINE (\d+)/.exec(result), - // Fetch the Character string using regex from the result - char = /Character: (\d+)/.exec(result); - - // If line and character is null then no need to mark - if (line != null && char != null) { - error_line_no = self.marked_line_no = (parseInt(line[1]) - 1) + selected_line_no; - var error_char_no = (parseInt(char[1]) - 1); - - /* We need to loop through each line till the error line and - * count the total no of character to figure out the actual - * starting/ending marker point for the individual line. We - * have also added 1 per line for the "\n" character. - */ - var prev_line_chars = 0; - var loop_index = selected_line_no > 0 ? selected_line_no : 0; - for (var i = loop_index; i < error_line_no; i++) - prev_line_chars += self.gridView.query_tool_obj.getLine(i).length + 1; - - /* Marker starting point for the individual line is - * equal to error character index minus total no of - * character till the error line starts. - */ - start_marker = error_char_no - prev_line_chars; - - // Find the next space from the character or end of line - var error_line = self.gridView.query_tool_obj.getLine(error_line_no); - end_marker = error_line.indexOf(' ', start_marker); - if (end_marker < 0) - end_marker = error_line.length; - - // Mark the error text - self.gridView.marker = self.gridView.query_tool_obj.markText( - {line: error_line_no, ch: start_marker}, - {line: error_line_no, ch: end_marker}, - {className: "sql-editor-mark"} - ); - - self.gridView.query_tool_obj.addLineClass(self.marked_line_no, 'wrap', 'CodeMirror-activeline-background'); - } - }, - - // This function will cancel the running query. - _cancel_query: function() { - var self = this; - - $("#btn-cancel-query").prop('disabled', true); - $.ajax({ - url: url_for('sqleditor.cancel_transaction', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - success: function(res) { - if (res.data.status) { - self.disable_tool_buttons(false); - } - else { - self.disable_tool_buttons(false); - alertify.alert('Cancel Query Error', res.data.result); - } - }, - error: function(e) { + else { self.disable_tool_buttons(false); - - if (e.readyState == 0) { - alertify.alert('Cancel Query Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } - - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; - - alertify.alert('Cancel Query Error', msg); + alertify.alert('Cancel Query Error', res.data.result); } - }); - }, - - // This function will download the grid data as CSV file. - download: function() { - var self = this, - selected_code = self.gridView.query_tool_obj.getSelection(), - sql = ""; - - if (selected_code.length > 0) - sql = selected_code; - else - sql = self.gridView.query_tool_obj.getValue(); - - // If it is an empty query, do nothing. - if (sql.length <= 0) return; + }, + error: function (e) { + self.disable_tool_buttons(false); - /* If download is from view data then file name should be - * the object name for which data is to be displayed. - */ - if (!self.is_query_tool) { - $.ajax({ - url: url_for('sqleditor.get_object_name', {'trans_id': self.transId}), - method: 'GET', - success: function(res) { - if (res.data.status) { - filename = res.data.result + '.csv'; - self._trigger_csv_download(sql, filename); - } - }, - error: function(e) { - if (e.readyState == 0) { - alertify.alert('Get Object Name Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } + if (e.readyState == 0) { + alertify.alert('Cancel Query Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; + } - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - alertify.alert('Get Object Name Error', msg); - } - }); - } else { - var cur_time = new Date(); - var filename = 'data-' + cur_time.getTime() + '.csv'; - self._trigger_csv_download(sql, filename); + alertify.alert('Cancel Query Error', msg); } - }, - // Trigger query result download to csv. - _trigger_csv_download: function(query, filename) { - var self = this, - link = $(this.container).find("#download-csv"), - url = url_for('sqleditor.query_tool_download', {'trans_id': self.transId}); - - url +="?" + $.param({query:query, filename:filename}); - link.attr("src", url); - }, + }); + }, - _auto_rollback: function() { - var self = this, - auto_rollback = true; + // Trigger query result download to csv. + trigger_csv_download: function (query, filename) { + var self = this, + link = $(this.container).find("#download-csv"), + url = url_for('sqleditor.query_tool_download', {'trans_id': self.transId}); - if ($('.auto-rollback').hasClass('visibility-hidden') === true) - $('.auto-rollback').removeClass('visibility-hidden'); - else { - $('.auto-rollback').addClass('visibility-hidden'); - auto_rollback = false; - } + url += "?" + $.param({query: query, filename: filename}); + link.attr("src", url); + }, - // Make ajax call to change the limit - $.ajax({ - url: url_for('sqleditor.auto_rollback', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(auto_rollback), - success: function(res) { - if (!res.data.status) - alertify.alert('Auto Rollback Error', res.data.result); - }, - error: function(e) { - if (e.readyState == 0) { - alertify.alert('Auto Rollback Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } + _auto_rollback: function () { + var self = this, + auto_rollback = true; - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; + if ($('.auto-rollback').hasClass('visibility-hidden') === true) + $('.auto-rollback').removeClass('visibility-hidden'); + else { + $('.auto-rollback').addClass('visibility-hidden'); + auto_rollback = false; + } - alertify.alert('Auto Rollback Error', msg); + // Make ajax call to change the limit + $.ajax({ + url: url_for('sqleditor.auto_rollback', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(auto_rollback), + success: function (res) { + if (!res.data.status) + alertify.alert('Auto Rollback Error', res.data.result); + }, + error: function (e) { + if (e.readyState == 0) { + alertify.alert('Auto Rollback Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; } - }); - }, - _auto_commit: function() { - var self = this, - auto_commit = true; + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - if ($('.auto-commit').hasClass('visibility-hidden') === true) - $('.auto-commit').removeClass('visibility-hidden'); - else { - $('.auto-commit').addClass('visibility-hidden'); - auto_commit = false; + alertify.alert('Auto Rollback Error', msg); } + }); + }, - // Make ajax call to change the limit - $.ajax({ - url: url_for('sqleditor.auto_commit', {'trans_id': self.transId}), - method: 'POST', - contentType: "application/json", - data: JSON.stringify(auto_commit), - success: function(res) { - if (!res.data.status) - alertify.alert('Auto Commit Error', res.data.result); - }, - error: function(e) { - if (e.readyState == 0) { - alertify.alert('Auto Commit Error', - gettext("Not connected to the server or the connection to the server has been closed.") - ); - return; - } + _auto_commit: function () { + var self = this, + auto_commit = true; - var msg = e.responseText; - if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) - msg = e.responseJSON.errormsg; + if ($('.auto-commit').hasClass('visibility-hidden') === true) + $('.auto-commit').removeClass('visibility-hidden'); + else { + $('.auto-commit').addClass('visibility-hidden'); + auto_commit = false; + } - alertify.alert('Auto Commit Error', msg); + // Make ajax call to change the limit + $.ajax({ + url: url_for('sqleditor.auto_commit', {'trans_id': self.transId}), + method: 'POST', + contentType: "application/json", + data: JSON.stringify(auto_commit), + success: function (res) { + if (!res.data.status) + alertify.alert('Auto Commit Error', res.data.result); + }, + error: function (e) { + if (e.readyState == 0) { + alertify.alert('Auto Commit Error', + gettext("Not connected to the server or the connection to the server has been closed.") + ); + return; } - }); - }, - - // This function will - explain: function() { - var self = this; - var verbose = $('.explain-verbose').hasClass('visibility-hidden') ? 'OFF' : 'ON'; - var costs = $('.explain-costs').hasClass('visibility-hidden') ? 'OFF' : 'ON'; - - // No need to check for buffers and timing option value in explain - var explain_query = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE %s, COSTS %s, BUFFERS OFF, TIMING OFF) '; - explain_query = S(explain_query).sprintf(verbose, costs).value(); - self._execute(explain_query); - }, - // This function will - explainAnalyze: function() { - var self = this; - var verbose = $('.explain-verbose').hasClass('visibility-hidden') ? 'OFF' : 'ON'; - var costs = $('.explain-costs').hasClass('visibility-hidden') ? 'OFF' : 'ON'; - var buffers = $('.explain-buffers').hasClass('visibility-hidden') ? 'OFF' : 'ON'; - var timing = $('.explain-timing').hasClass('visibility-hidden') ? 'OFF' : 'ON'; - - var explain_query = 'Explain (FORMAT JSON, ANALYZE ON, VERBOSE %s, COSTS %s, BUFFERS %s, TIMING %s) '; - explain_query = S(explain_query).sprintf(verbose, costs, buffers, timing).value(); - self._execute(explain_query); - }, + var msg = e.responseText; + if (e.responseJSON != undefined && + e.responseJSON.errormsg != undefined) + msg = e.responseJSON.errormsg; - // This function will toggle "verbose" option in explain - _explain_verbose: function() { - var self = this; - if ($('.explain-verbose').hasClass('visibility-hidden') === true) { - $('.explain-verbose').removeClass('visibility-hidden'); - self.explain_verbose = true; - } - else { - $('.explain-verbose').addClass('visibility-hidden'); - self.explain_verbose = false; + alertify.alert('Auto Commit Error', msg); } + }); + }, - // Set this option in preferences - var data = { - 'explain_verbose': self.explain_verbose - }; + // This function will toggle "verbose" option in explain + _explain_verbose: function () { + var self = this; + if ($('.explain-verbose').hasClass('visibility-hidden') === true) { + $('.explain-verbose').removeClass('visibility-hidden'); + self.explain_verbose = true; + } + else { + $('.explain-verbose').addClass('visibility-hidden'); + self.explain_verbose = false; + } - $.ajax({ - url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), - method: 'PUT', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - if(res.success == undefined || !res.success) { - alertify.alert('Explain options error', - gettext("Error occurred while setting verbose option in explain") - ); - } - }, - error: function(e) { + // Set this option in preferences + var data = { + 'explain_verbose': self.explain_verbose + }; + + $.ajax({ + url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), + method: 'PUT', + contentType: "application/json", + data: JSON.stringify(data), + success: function (res) { + if (res.success == undefined || !res.success) { alertify.alert('Explain options error', gettext("Error occurred while setting verbose option in explain") ); - return; } - }); - }, - - // This function will toggle "costs" option in explain - _explain_costs: function() { - var self = this; - if ($('.explain-costs').hasClass('visibility-hidden') === true) { - $('.explain-costs').removeClass('visibility-hidden'); - self.explain_costs = true; - } - else { - $('.explain-costs').addClass('visibility-hidden'); - self.explain_costs = false; + }, + error: function (e) { + alertify.alert('Explain options error', + gettext("Error occurred while setting verbose option in explain") + ); + return; } + }); + }, - // Set this option in preferences - var data = { - 'explain_costs': self.explain_costs - }; + // This function will toggle "costs" option in explain + _explain_costs: function () { + var self = this; + if ($('.explain-costs').hasClass('visibility-hidden') === true) { + $('.explain-costs').removeClass('visibility-hidden'); + self.explain_costs = true; + } + else { + $('.explain-costs').addClass('visibility-hidden'); + self.explain_costs = false; + } - $.ajax({ - url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), - method: 'PUT', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - if(res.success == undefined || !res.success) { - alertify.alert('Explain options error', - gettext("Error occurred while setting costs option in explain") - ); - } - }, - error: function(e) { + // Set this option in preferences + var data = { + 'explain_costs': self.explain_costs + }; + + $.ajax({ + url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), + method: 'PUT', + contentType: "application/json", + data: JSON.stringify(data), + success: function (res) { + if (res.success == undefined || !res.success) { alertify.alert('Explain options error', gettext("Error occurred while setting costs option in explain") ); } - }); - }, - - // This function will toggle "buffers" option in explain - _explain_buffers: function() { - var self = this; - if ($('.explain-buffers').hasClass('visibility-hidden') === true) { - $('.explain-buffers').removeClass('visibility-hidden'); - self.explain_buffers = true; - } - else { - $('.explain-buffers').addClass('visibility-hidden'); - self.explain_buffers = false; + }, + error: function (e) { + alertify.alert('Explain options error', + gettext("Error occurred while setting costs option in explain") + ); } + }); + }, - // Set this option in preferences - var data = { - 'explain_buffers': self.explain_buffers - }; + // This function will toggle "buffers" option in explain + _explain_buffers: function () { + var self = this; + if ($('.explain-buffers').hasClass('visibility-hidden') === true) { + $('.explain-buffers').removeClass('visibility-hidden'); + self.explain_buffers = true; + } + else { + $('.explain-buffers').addClass('visibility-hidden'); + self.explain_buffers = false; + } - $.ajax({ - url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), - method: 'PUT', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - if(res.success == undefined || !res.success) { - alertify.alert('Explain options error', - gettext("Error occurred while setting buffers option in explain") - ); - } - }, - error: function(e) { + // Set this option in preferences + var data = { + 'explain_buffers': self.explain_buffers + }; + + $.ajax({ + url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), + method: 'PUT', + contentType: "application/json", + data: JSON.stringify(data), + success: function (res) { + if (res.success == undefined || !res.success) { alertify.alert('Explain options error', gettext("Error occurred while setting buffers option in explain") ); } - }); - }, - - // This function will toggle "timing" option in explain - _explain_timing: function() { - var self = this; - if ($('.explain-timing').hasClass('visibility-hidden') === true) { - $('.explain-timing').removeClass('visibility-hidden'); - self.explain_timing = true; - } - else { - $('.explain-timing').addClass('visibility-hidden'); - self.explain_timing = false; + }, + error: function (e) { + alertify.alert('Explain options error', + gettext("Error occurred while setting buffers option in explain") + ); } - // Set this option in preferences - var data = { - 'explain_timing': self.explain_timing - }; + }); + }, - $.ajax({ - url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), - method: 'PUT', - contentType: "application/json", - data: JSON.stringify(data), - success: function(res) { - if(res.success == undefined || !res.success) { - alertify.alert('Explain options error', - gettext("Error occurred while setting timing option in explain") - ); - } - }, - error: function(e) { + // This function will toggle "timing" option in explain + _explain_timing: function () { + var self = this; + if ($('.explain-timing').hasClass('visibility-hidden') === true) { + $('.explain-timing').removeClass('visibility-hidden'); + self.explain_timing = true; + } + else { + $('.explain-timing').addClass('visibility-hidden'); + self.explain_timing = false; + } + // Set this option in preferences + var data = { + 'explain_timing': self.explain_timing + }; + + $.ajax({ + url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), + method: 'PUT', + contentType: "application/json", + data: JSON.stringify(data), + success: function (res) { + if (res.success == undefined || !res.success) { alertify.alert('Explain options error', gettext("Error occurred while setting timing option in explain") ); } - }); - }, - - /* - * This function will comment code (Wrapper function) - */ - commentLineCode: function() { - this._toggle_comment_code('comment_line'); - }, - - /* - * This function will uncomment code (Wrapper function) - */ - uncommentLineCode: function() { - this._toggle_comment_code('uncomment_line'); - }, - - /* - * This function will comment/uncomment code (Wrapper function) - */ - commentBlockCode: function() { - this._toggle_comment_code('block'); - }, - - /* - * This function will comment/uncomment code (Main function) - */ - _toggle_comment_code: function(of_type) { - var self = this, editor = self.gridView.query_tool_obj, - selected_code = editor.getSelection(), - sql = selected_code.length > 0 ? selected_code : editor.getValue(); - // If it is an empty query, do nothing. - if (sql.length <= 0) return; - - // Find the code selection range - var range = { - from: editor.getCursor(true), - to: editor.getCursor(false) }, - option = { lineComment: '--' }; - - if(of_type == 'comment_line') { - // Comment line - editor.lineComment(range.from, range.to, option); - } else if(of_type == 'uncomment_line') { - // Uncomment line - editor.uncomment(range.from, range.to, option); - } else if(of_type == 'block') { - editor.toggleComment(range.from, range.to); + error: function (e) { + alertify.alert('Explain options error', + gettext("Error occurred while setting timing option in explain") + ); } - }, + }); + }, - /* - * This function will indent selected code - */ - _indent_selected_code: function() { - var self = this, editor = self.gridView.query_tool_obj; - editor.execCommand("indentMore"); - }, + /* + * This function will indent selected code + */ + _indent_selected_code: function () { + var self = this, editor = self.gridView.query_tool_obj; + editor.execCommand("indentMore"); + }, - /* - * This function will unindent selected code - */ - _unindent_selected_code: function() { - var self = this, editor = self.gridView.query_tool_obj; - editor.execCommand("indentLess"); - }, + /* + * This function will unindent selected code + */ + _unindent_selected_code: function () { + var self = this, editor = self.gridView.query_tool_obj; + editor.execCommand("indentLess"); + }, - isQueryRunning: function () { - return is_query_running; - }, + isQueryRunning: function () { + return is_query_running; + }, - /* - * This function get explain options and auto rollback/auto commit - * values from preferences - */ - get_preferences: function() { - var self = this, - explain_verbose = false, - explain_costs = false, - explain_buffers = false, - explain_timing = false, - auto_commit = true, - auto_rollback = false, - updateUI = function() { - // Set Auto-commit and auto-rollback on query editor - if (auto_commit && - $('.auto-commit').hasClass('visibility-hidden') === true) - $('.auto-commit').removeClass('visibility-hidden'); - else { - $('.auto-commit').addClass('visibility-hidden'); - } - if (auto_rollback && - $('.auto-rollback').hasClass('visibility-hidden') === true) - $('.auto-rollback').removeClass('visibility-hidden'); - else { - $('.auto-rollback').addClass('visibility-hidden'); - } + /* + * This function get explain options and auto rollback/auto commit + * values from preferences + */ + get_preferences: function () { + var self = this, + explain_verbose = false, + explain_costs = false, + explain_buffers = false, + explain_timing = false, + auto_commit = true, + auto_rollback = false, + updateUI = function () { + // Set Auto-commit and auto-rollback on query editor + if (auto_commit && + $('.auto-commit').hasClass('visibility-hidden') === true) + $('.auto-commit').removeClass('visibility-hidden'); + else { + $('.auto-commit').addClass('visibility-hidden'); + } + if (auto_rollback && + $('.auto-rollback').hasClass('visibility-hidden') === true) + $('.auto-rollback').removeClass('visibility-hidden'); + else { + $('.auto-rollback').addClass('visibility-hidden'); + } - // Set explain options on query editor - if (explain_verbose && - $('.explain-verbose').hasClass('visibility-hidden') === true) - $('.explain-verbose').removeClass('visibility-hidden'); - else { - $('.explain-verbose').addClass('visibility-hidden'); - } - if (explain_costs && - $('.explain-costs').hasClass('visibility-hidden') === true) - $('.explain-costs').removeClass('visibility-hidden'); - else { - $('.explain-costs').addClass('visibility-hidden'); - } - if (explain_buffers && - $('.explain-buffers').hasClass('visibility-hidden') === true) - $('.explain-buffers').removeClass('visibility-hidden'); - else { - $('.explain-buffers').addClass('visibility-hidden'); - } - if (explain_timing && - $('.explain-timing').hasClass('visibility-hidden') === true) - $('.explain-timing').removeClass('visibility-hidden'); - else { - $('.explain-timing').addClass('visibility-hidden'); - } - }; + // Set explain options on query editor + if (explain_verbose && + $('.explain-verbose').hasClass('visibility-hidden') === true) + $('.explain-verbose').removeClass('visibility-hidden'); + else { + $('.explain-verbose').addClass('visibility-hidden'); + } + if (explain_costs && + $('.explain-costs').hasClass('visibility-hidden') === true) + $('.explain-costs').removeClass('visibility-hidden'); + else { + $('.explain-costs').addClass('visibility-hidden'); + } + if (explain_buffers && + $('.explain-buffers').hasClass('visibility-hidden') === true) + $('.explain-buffers').removeClass('visibility-hidden'); + else { + $('.explain-buffers').addClass('visibility-hidden'); + } + if (explain_timing && + $('.explain-timing').hasClass('visibility-hidden') === true) + $('.explain-timing').removeClass('visibility-hidden'); + else { + $('.explain-timing').addClass('visibility-hidden'); + } + }; + + $.ajax({ + url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), + method: 'GET', + success: function (res) { + if (res.data) { + explain_verbose = res.data.explain_verbose; + explain_costs = res.data.explain_costs; + explain_buffers = res.data.explain_buffers; + explain_timing = res.data.explain_timing; + auto_commit = res.data.auto_commit; + auto_rollback = res.data.auto_rollback; - $.ajax({ - url: url_for('sqleditor.query_tool_preferences', {'trans_id': self.transId}), - method: 'GET', - success: function(res) { - if (res.data) { - explain_verbose = res.data.explain_verbose; - explain_costs = res.data.explain_costs; - explain_buffers = res.data.explain_buffers; - explain_timing = res.data.explain_timing; - auto_commit = res.data.auto_commit; - auto_rollback = res.data.auto_rollback; - - updateUI(); - } - }, - error: function(e) { updateUI(); - alertify.alert('Get Preferences error', - gettext("Error occurred while getting query tool options ") - ); } - }); - }, - close: function() { - var self= this; - _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function(panel) { - if(panel.isVisible()) { - window.onbeforeunload = null; - panel.off(wcDocker.EVENT.CLOSING); - // remove col_size object on panel close - if (!_.isUndefined(self.col_size)) { - delete self.col_size; - } - window.top.pgAdmin.Browser.docker.removePanel(panel); + }, + error: function (e) { + updateUI(); + alertify.alert('Get Preferences error', + gettext("Error occurred while getting query tool options ") + ); + } + }); + }, + close: function () { + var self = this; + _.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function (panel) { + if (panel.isVisible()) { + window.onbeforeunload = null; + panel.off(wcDocker.EVENT.CLOSING); + // remove col_size object on panel close + if (!_.isUndefined(self.col_size)) { + delete self.col_size; } - }); - } + window.top.pgAdmin.Browser.docker.removePanel(panel); + } + }); } - ); - - pgAdmin.SqlEditor = { - // This function is used to create and return the object of grid controller. - create: function(container) { - return new SqlEditorController(container); - }, - jquery: $, - S: S - }; - - return pgAdmin.SqlEditor; - }); + } + ); + + pgAdmin.SqlEditor = { + // This function is used to create and return the object of grid controller. + create: function (container) { + return new SqlEditorController(container); + }, + jquery: $, + S: S + }; + + return pgAdmin.SqlEditor; +}); diff --git a/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js b/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js index 9f89482a..d16ac562 100644 --- a/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js +++ b/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js @@ -8,6 +8,7 @@ ////////////////////////////////////////////////////////////////////////// import keyboardShortcuts from 'sources/sqleditor/keyboard_shortcuts'; +import {queryToolActions} from 'sources/sqleditor/query_tool_actions'; describe('the keyboard shortcuts', () => { const F1_KEY = 112, @@ -15,11 +16,9 @@ describe('the keyboard shortcuts', () => { F7_KEY = 118, F8_KEY = 119, PERIOD_KEY = 190, - FWD_SLASH_KEY = 191, - isMacSystem = window.navigator.platform.search('Mac') != -1; + FWD_SLASH_KEY = 191; - let sqlEditorControllerSpy; - let event; + let sqlEditorControllerSpy, event, queryToolActionsSpy; beforeEach(() => { event = { shift: false, @@ -30,15 +29,27 @@ describe('the keyboard shortcuts', () => { stopImmediatePropagation: jasmine.createSpy('stopImmediatePropagation'), }; + let gridView = { + query_tool_obj: { + getSelection: jasmine.createSpy('getSelection'), + getValue: jasmine.createSpy('getValue'), + }, + }; + sqlEditorControllerSpy = jasmine.createSpyObj('SqlEditorController', [ 'isQueryRunning', - 'download', - 'explain', + 'execute', + ]); + + sqlEditorControllerSpy.gridView = gridView; + queryToolActionsSpy = jasmine.createSpyObj(queryToolActions, [ 'explainAnalyze', - 'executeQuery', + 'explain', + 'download', + 'commentBlockCode', 'commentLineCode', 'uncommentLineCode', - 'commentBlockCode', + 'executeQuery', ]); }); @@ -46,7 +57,7 @@ describe('the keyboard shortcuts', () => { beforeEach(() => { event.which = F1_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); }); it('should allow event to propagate', () => { @@ -58,11 +69,11 @@ describe('the keyboard shortcuts', () => { describe('when there is no query already running', () => { beforeEach(() => { event.keyCode = F5_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); }); it('should execute the query', () => { - expect(sqlEditorControllerSpy.executeQuery).toHaveBeenCalled(); + expect(queryToolActionsSpy.executeQuery).toHaveBeenCalledWith(sqlEditorControllerSpy); }); it('should stop event propagation', () => { @@ -75,9 +86,9 @@ describe('the keyboard shortcuts', () => { event.keyCode = F5_KEY; sqlEditorControllerSpy.isQueryRunning.and.returnValue(true); - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); - expect(sqlEditorControllerSpy.executeQuery).not.toHaveBeenCalled(); + expect(queryToolActionsSpy.executeQuery).not.toHaveBeenCalled(); }); }); }); @@ -86,11 +97,11 @@ describe('the keyboard shortcuts', () => { describe('when there is not a query already running', () => { beforeEach(() => { event.which = F7_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); }); it('should explain the query plan', () => { - expect(sqlEditorControllerSpy.explain).toHaveBeenCalledWith(event); + expect(queryToolActionsSpy.explain).toHaveBeenCalledWith(sqlEditorControllerSpy); }); expectEventPropagationToStop(); @@ -101,23 +112,23 @@ describe('the keyboard shortcuts', () => { event.keyCode = F7_KEY; sqlEditorControllerSpy.isQueryRunning.and.returnValue(true); - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); - expect(sqlEditorControllerSpy.explain).not.toHaveBeenCalled(); + expect(queryToolActionsSpy.explain).not.toHaveBeenCalled(); }); }); }); describe('Shift+F7', () => { - describe('when therre is not a query already running', () => { + describe('when there is not a query already running', () => { beforeEach(() => { event.shiftKey = true; event.which = F7_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); }); it('should analyze explain the query plan', () => { - expect(sqlEditorControllerSpy.explainAnalyze).toHaveBeenCalledWith(event); + expect(queryToolActionsSpy.explainAnalyze).toHaveBeenCalledWith(sqlEditorControllerSpy); }); expectEventPropagationToStop(); @@ -129,9 +140,9 @@ describe('the keyboard shortcuts', () => { event.which = F7_KEY; sqlEditorControllerSpy.isQueryRunning.and.returnValue(true); - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); - expect(sqlEditorControllerSpy.explainAnalyze).not.toHaveBeenCalled(); + expect(queryToolActionsSpy.explainAnalyze).not.toHaveBeenCalled(); }); }); }); @@ -140,11 +151,11 @@ describe('the keyboard shortcuts', () => { describe('when there is not a query already running', () => { beforeEach(() => { event.which = F8_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); }); it('should download the query results as a CSV', () => { - expect(sqlEditorControllerSpy.download).toHaveBeenCalled(); + expect(queryToolActionsSpy.download).toHaveBeenCalled(); }); it('should stop event propagation', () => { @@ -157,102 +168,198 @@ describe('the keyboard shortcuts', () => { event.keyCode = F8_KEY; sqlEditorControllerSpy.isQueryRunning.and.returnValue(true); - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); - expect(sqlEditorControllerSpy.download).not.toHaveBeenCalled(); + expect(queryToolActionsSpy.download).not.toHaveBeenCalled(); }); }); }); describe('inlineComment', () => { describe('when there is not a query already running', () => { - beforeEach(() => { - event.metaKey = isMacSystem; - event.shiftKey = true; - event.ctrlKey = !isMacSystem; - event.which = FWD_SLASH_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + describe('and the system is a Mac', () => { + beforeEach(() => { + macKeysSetup(); + event.which = FWD_SLASH_KEY; + event.shiftKey = true; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); + + it('should comment the line', () => { + expect(queryToolActionsSpy.commentLineCode).toHaveBeenCalledWith(sqlEditorControllerSpy); + }); + + expectEventPropagationToStop(); }); - it('should comment the line', () => { - expect(sqlEditorControllerSpy.commentLineCode).toHaveBeenCalled(); - }); + describe('and the system is Windows', () => { + beforeEach(() => { + windowsKeysSetup(); + event.which = FWD_SLASH_KEY; + event.shiftKey = true; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); - expectEventPropagationToStop(); + it('should comment the line', () => { + expect(queryToolActionsSpy.commentLineCode).toHaveBeenCalledWith(sqlEditorControllerSpy); + }); + + expectEventPropagationToStop(); + }); }); describe('when the query is already running', () => { - it('does nothing', () => { - event.shiftKey = isMacSystem; - event.ctrlKey = !isMacSystem; - event.which = FWD_SLASH_KEY; + beforeEach(() => { sqlEditorControllerSpy.isQueryRunning.and.returnValue(true); + }); + describe('and the system is a Mac', () => { + beforeEach(() => { + macKeysSetup(); + event.which = FWD_SLASH_KEY; + event.shiftKey = true; + }); + + it('does nothing', () => { + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + + expect(queryToolActionsSpy.commentLineCode).not.toHaveBeenCalled(); + }); + }); + + describe('and the system is a Windows', () => { + beforeEach(() => { + windowsKeysSetup(); + event.which = FWD_SLASH_KEY; + }); - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + it('does nothing', () => { + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); - expect(sqlEditorControllerSpy.commentLineCode).not.toHaveBeenCalled(); + expect(queryToolActionsSpy.commentLineCode).not.toHaveBeenCalled(); + }); }); }); }); describe('inlineUncomment', () => { describe('when there is not a query already running', () => { - beforeEach(() => { - event.metaKey = isMacSystem; - event.shiftKey = true; - event.ctrlKey = !isMacSystem; - event.which = PERIOD_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + describe('and the system is a mac', () => { + beforeEach(() => { + macKeysSetup(); + event.which = PERIOD_KEY; + event.shiftKey = true; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); + + it('should uncomment the line', () => { + expect(queryToolActionsSpy.uncommentLineCode).toHaveBeenCalledWith(sqlEditorControllerSpy); + }); + + expectEventPropagationToStop(); }); - - it('should uncomment the line', () => { - expect(sqlEditorControllerSpy.uncommentLineCode).toHaveBeenCalled(); + describe('and the system is a windows', () => { + beforeEach(() => { + windowsKeysSetup(); + event.which = PERIOD_KEY; + event.shiftKey = true; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); + + it('should uncomment the line', () => { + expect(queryToolActionsSpy.uncommentLineCode).toHaveBeenCalledWith(sqlEditorControllerSpy); + }); + + expectEventPropagationToStop(); }); - - expectEventPropagationToStop(); }); describe('when the query is already running', () => { - it('does nothing', () => { - event.metaKey = isMacSystem; - event.shiftKey = true; - event.ctrlKey = !isMacSystem; - event.which = PERIOD_KEY; + beforeEach(() => { sqlEditorControllerSpy.isQueryRunning.and.returnValue(true); + }); + describe('and the system is a Mac', () => { + beforeEach(() => { + macKeysSetup(); + event.which = PERIOD_KEY; + event.shiftKey = true; + }); + + it('does nothing', () => { + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + expect(queryToolActionsSpy.uncommentLineCode).not.toHaveBeenCalled(); + }); + }); + describe('and the system is a Windows', () => { + beforeEach(() => { + windowsKeysSetup(); + event.which = PERIOD_KEY; + }); - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + it('does nothing', () => { + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); - expect(sqlEditorControllerSpy.uncommentLineCode).not.toHaveBeenCalled(); + expect(queryToolActionsSpy.uncommentLineCode).not.toHaveBeenCalled(); + }); }); }); }); describe('blockComment', () => { describe('when there is not a query already running', () => { - beforeEach(() => { - event.metaKey = isMacSystem; - event.ctrlKey = !isMacSystem; - event.which = FWD_SLASH_KEY; - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); + describe('and the system is a Mac', () => { + beforeEach(() => { + macKeysSetup(); + event.which = FWD_SLASH_KEY; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); + + it('should comment out the block selection', () => { + expect(queryToolActionsSpy.commentBlockCode).toHaveBeenCalledWith(sqlEditorControllerSpy); + }); + + expectEventPropagationToStop(); }); + }); - it('should comment a block of code', () => { - expect(sqlEditorControllerSpy.commentBlockCode).toHaveBeenCalled(); + describe('when there is not a query already running', () => { + describe('and the system is a Windows', () => { + beforeEach(() => { + windowsKeysSetup(); + event.which = FWD_SLASH_KEY; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); + + it('should comment out the block selection', () => { + expect(queryToolActionsSpy.commentBlockCode).toHaveBeenCalledWith(sqlEditorControllerSpy); + }); + + expectEventPropagationToStop(); }); - - expectEventPropagationToStop(); }); - describe('when the query is already running', () => { - it('does nothing', () => { - event.metaKey = isMacSystem; - event.ctrlKey = !isMacSystem; - event.which = FWD_SLASH_KEY; + describe('when there is a query already running', () => { + beforeEach(() => { sqlEditorControllerSpy.isQueryRunning.and.returnValue(true); - - keyboardShortcuts.processEvent(sqlEditorControllerSpy, event); - - expect(sqlEditorControllerSpy.commentBlockCode).not.toHaveBeenCalled(); + }); + describe('and the system is a Mac', () => { + beforeEach(() => { + macKeysSetup(); + event.which = FWD_SLASH_KEY; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); + it('does nothing', () => { + expect(queryToolActionsSpy.commentBlockCode).not.toHaveBeenCalled(); + }); + }); + describe('and the system is a Windows', () => { + beforeEach(() => { + windowsKeysSetup(); + event.which = FWD_SLASH_KEY; + keyboardShortcuts.processEvent(sqlEditorControllerSpy, queryToolActionsSpy, event); + }); + it('does nothing', () => { + expect(queryToolActionsSpy.commentBlockCode).not.toHaveBeenCalled(); + }); }); }); }); @@ -274,4 +381,16 @@ describe('the keyboard shortcuts', () => { }); }); } + + function windowsKeysSetup() { + spyOn(keyboardShortcuts, 'isMac').and.returnValue(false); + event.ctrlKey = true; + event.metaKey = false; + } + + function macKeysSetup() { + spyOn(keyboardShortcuts, 'isMac').and.returnValue(true); + event.ctrlKey = false; + event.metaKey = true; + } }); diff --git a/web/regression/javascript/sqleditor/query_tool_actions_spec.js b/web/regression/javascript/sqleditor/query_tool_actions_spec.js new file mode 100644 index 00000000..37c21839 --- /dev/null +++ b/web/regression/javascript/sqleditor/query_tool_actions_spec.js @@ -0,0 +1,398 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// +import queryToolActions from 'sources/sqleditor/query_tool_actions'; + +describe('queryToolActions', () => { + let sqlEditorController, + getSelectionSpy, getValueSpy, + selectedQueryString, entireQueryString; + + describe('executeQuery', () => { + describe('when the command is being run from the query tool', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_clearMessageTab'); + }); + + it('clears the html in the message tab', () => { + queryToolActions.executeQuery(sqlEditorController); + + expect(queryToolActions._clearMessageTab).toHaveBeenCalled(); + }); + + it('calls the execute function on the sqlEditorController', () => { + queryToolActions.executeQuery(sqlEditorController); + + expect(sqlEditorController.execute).toHaveBeenCalled(); + }); + }); + describe('when the command is being run from the view data view', () => { + beforeEach(() => { + setUpSpies('', ''); + sqlEditorController.is_query_tool = false; + }); + + it('it calls the execute_data_query function on the sqlEditorController', () => { + queryToolActions.executeQuery(sqlEditorController); + + expect(sqlEditorController.execute_data_query).toHaveBeenCalled(); + }); + + }); + }); + + describe('explainAnalyze', () => { + describe('when verbose and costs are not selected and buffers and timing are not selected', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue('OFF'); + spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); + spyOn(queryToolActions, '_buffers').and.returnValue('OFF'); + spyOn(queryToolActions, '_timing').and.returnValue('OFF'); + }); + it('calls the execute function', () => { + queryToolActions.explainAnalyze(sqlEditorController); + let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE OFF, COSTS OFF, BUFFERS OFF, TIMING OFF) '; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + }); + }); + + describe('when verbose and costs and buffers and timing are all selected', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue('ON'); + spyOn(queryToolActions, '_costsEnabled').and.returnValue('ON'); + spyOn(queryToolActions, '_buffers').and.returnValue('ON'); + spyOn(queryToolActions, '_timing').and.returnValue('ON'); + }); + it('calls the execute function', () => { + queryToolActions.explainAnalyze(sqlEditorController); + let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE ON, COSTS ON, BUFFERS ON, TIMING ON) '; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + }); + }); + + describe('when verbose is selected and costs is not selected and buffer is selected and timing is not selected', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue('ON'); + spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); + spyOn(queryToolActions, '_buffers').and.returnValue('ON'); + spyOn(queryToolActions, '_timing').and.returnValue('OFF'); + }); + it('calls the execute function', () => { + queryToolActions.explainAnalyze(sqlEditorController); + let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE ON, COSTS OFF, BUFFERS ON, TIMING OFF) '; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + }); + }); + + describe('when verbose is not selected and costs is selected and buffer is not selected and timing is selected', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue('OFF'); + spyOn(queryToolActions, '_costsEnabled').and.returnValue('ON'); + spyOn(queryToolActions, '_buffers').and.returnValue('OFF'); + spyOn(queryToolActions, '_timing').and.returnValue('ON'); + }); + it('calls the execute function', () => { + queryToolActions.explainAnalyze(sqlEditorController); + let explainAnalyzeQuery = 'EXPLAIN (FORMAT JSON, ANALYZE ON, VERBOSE OFF, COSTS ON, BUFFERS OFF, TIMING ON) '; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainAnalyzeQuery); + }); + }); + }); + + describe('explain', () => { + describe('when verbose and costs are selected', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue('ON'); + spyOn(queryToolActions, '_costsEnabled').and.returnValue('ON'); + }); + it('calls the execute function', () => { + queryToolActions.explain(sqlEditorController); + let explainQuery = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE ON, COSTS ON, BUFFERS OFF, TIMING OFF) '; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainQuery); + }); + }); + + describe('when verbose and costs are not selected', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue('OFF'); + spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); + }); + it('calls the execute function', () => { + queryToolActions.explain(sqlEditorController); + let explainQuery = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE OFF, COSTS OFF, BUFFERS OFF, TIMING OFF) '; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainQuery); + }); + }); + + describe('when verbose is selected and costs is not selected', () => { + beforeEach(() => { + setUpSpies('', ''); + spyOn(queryToolActions, '_verbose').and.returnValue('ON'); + spyOn(queryToolActions, '_costsEnabled').and.returnValue('OFF'); + }); + it('calls the execute function', () => { + queryToolActions.explain(sqlEditorController); + let explainQuery = 'EXPLAIN (FORMAT JSON, ANALYZE OFF, VERBOSE ON, COSTS OFF, BUFFERS OFF, TIMING OFF) '; + expect(sqlEditorController.execute).toHaveBeenCalledWith(explainQuery); + }); + }); + }); + + describe('download', () => { + describe('when the query is empty', () => { + beforeEach(() => { + setUpSpies('', ''); + }); + it('does nothing', () => { + queryToolActions.download(sqlEditorController); + + expect(sqlEditorController.trigger_csv_download).not.toHaveBeenCalled(); + }); + }); + + describe('when the table was opened through the queryTool', () => { + describe('when the query tool object has a selection', () => { + let time; + + beforeEach(() => { + entireQueryString = 'include some more of that yummy string cheese;'; + selectedQueryString = 'some silly string cheese'; + setUpSpies(selectedQueryString, entireQueryString); + + time = 'rightNow'; + spyOn(window, 'Date').and.callFake(() => ({ + getTime: () => { + return time; + }, + })); + }); + + it('calls trigger_csv_download with the query and the filename', () => { + let filename = 'data-' + time + '.csv'; + + queryToolActions.download(sqlEditorController); + + expect(sqlEditorController.trigger_csv_download).toHaveBeenCalledWith(selectedQueryString, filename); + }); + }); + + describe('when there is no selection', () => { + let time; + + beforeEach(() => { + selectedQueryString = ''; + entireQueryString = 'include some more of that yummy string cheese;'; + + setUpSpies(selectedQueryString, entireQueryString); + + time = 'rightNow'; + spyOn(window, 'Date').and.callFake(() => ({ + getTime: () => { + return time; + }, + })); + }); + + it('calls trigger_csv_download with the query and the filename', () => { + let filename = 'data-' + time + '.csv'; + + queryToolActions.download(sqlEditorController); + + expect(sqlEditorController.trigger_csv_download).toHaveBeenCalledWith(entireQueryString, filename); + }); + }); + }); + + describe('when the table was opened through tables, view all data', () => { + it('calls trigger_csv_download with the sqlQuery and the table name', () => { + let query = 'a very long query'; + setUpSpies('', query); + sqlEditorController.is_query_tool = false; + + queryToolActions.download(sqlEditorController); + + expect(sqlEditorController.trigger_csv_download).toHaveBeenCalledWith(query, 'iAmATable' + '.csv'); + }); + }); + + }); + + describe('commentBlockCode', () => { + describe('when there is no query text', () => { + beforeEach(() => { + setUpSpies('', ''); + }); + it('does nothing', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.commentBlockCode(sqlEditorController); + + expect(codeMirrorObj.toggleComment).not.toHaveBeenCalled(); + }); + }); + + describe('when there is empty selection', () => { + beforeEach(() => { + setUpSpies('', 'a string\nddd\nsss'); + + sqlEditorController.gridView.query_tool_obj.getCursor = (isFrom) => { + return isFrom ? 3 : 3; + }; + }); + + it('comments the current line', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.commentBlockCode(sqlEditorController); + + expect(codeMirrorObj.toggleComment).toHaveBeenCalledWith(3, 3); + }); + }); + + describe('when some part of the query is selected', () => { + beforeEach(() => { + setUpSpies('a string\nddd', 'a string\nddd\nsss'); + }); + + it('comments the selection', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.commentBlockCode(sqlEditorController); + + expect(codeMirrorObj.toggleComment).toHaveBeenCalledWith(0, 12); + }); + }); + }); + + describe('commentLineCode', () => { + describe('when there is no query text', () => { + beforeEach(() => { + setUpSpies('', ''); + }); + it('does nothing', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.commentLineCode(sqlEditorController); + + expect(codeMirrorObj.lineComment).not.toHaveBeenCalled(); + }); + }); + + describe('when there is empty selection', () => { + beforeEach(() => { + setUpSpies('', 'a string\nddd\nsss'); + + sqlEditorController.gridView.query_tool_obj.getCursor = (isFrom) => { + return isFrom ? 3 : 3; + }; + }); + + it('comments the current line', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.commentLineCode(sqlEditorController); + + expect(codeMirrorObj.lineComment).toHaveBeenCalledWith(3, 3, {lineComment: '--'}); + }); + }); + + describe('when some part of the query is selected', () => { + beforeEach(() => { + setUpSpies('tring\nddd', 'a string\nddd\nsss'); + }); + + it('comments the selection', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.commentLineCode(sqlEditorController); + + expect(codeMirrorObj.lineComment).toHaveBeenCalledWith(3, 12, {lineComment: '--'}); + }); + }); + }); + + describe('uncommentLineCode', () => { + describe('when there is no query text', () => { + beforeEach(() => { + setUpSpies('', ''); + }); + it('does nothing', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.uncommentLineCode(sqlEditorController); + + expect(codeMirrorObj.uncomment).not.toHaveBeenCalled(); + }); + }); + + describe('when there is empty selection', () => { + beforeEach(() => { + setUpSpies('', 'a string\nddd\nsss'); + + sqlEditorController.gridView.query_tool_obj.getCursor = (isFrom) => { + return isFrom ? 3 : 3; + }; + }); + + it('uncomments the current line', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.uncommentLineCode(sqlEditorController); + + expect(codeMirrorObj.uncomment).toHaveBeenCalledWith(3, 3, {lineComment: '--'}); + }); + }); + + describe('when some part of the query is selected', () => { + beforeEach(() => { + setUpSpies('tring\nddd', 'a string\nddd\nsss'); + }); + + it('uncomments the selection', () => { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + + queryToolActions.uncommentLineCode(sqlEditorController); + + expect(codeMirrorObj.uncomment).toHaveBeenCalledWith(3, 12, {lineComment: '--'}); + }); + }); + }); + + function setUpSpies(selectedQueryString, entireQueryString) { + getValueSpy = jasmine.createSpy('getValueSpy').and.returnValue(entireQueryString); + getSelectionSpy = jasmine.createSpy('getSelectionSpy').and.returnValue(selectedQueryString); + + sqlEditorController = { + gridView: { + query_tool_obj: { + getSelection: getSelectionSpy, + getValue: getValueSpy, + toggleComment: jasmine.createSpy('toggleCommentSpy'), + lineComment: jasmine.createSpy('lineCommentSpy'), + uncomment: jasmine.createSpy('uncommentSpy'), + getCursor: (isFrom) => { + return entireQueryString.indexOf(selectedQueryString) + (isFrom ? 0 : selectedQueryString.length); + }, + }, + }, + trigger_csv_download: jasmine.createSpy('trigger_csv_download'), + trigger: jasmine.createSpy('trigger'), + table_name: 'iAmATable', + is_query_tool: true, + execute: jasmine.createSpy('execute'), + execute_data_query: jasmine.createSpy('execute_data_query'), + }; + } +});