diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst index 1cb23cf97..45ef597a3 100644 --- a/docs/en_US/editgrid.rst +++ b/docs/en_US/editgrid.rst @@ -95,6 +95,29 @@ To delete a row, press the *Delete* toolbar button. A popup will open, asking y To commit the changes to the server, select the *Save* toolbar button. Modifications to a row are written to the server automatically when you select a different row. +**Sort/Filter options dialog** +You can access *Sort/Filter options dialog* by clicking on Sort/Filter button. This allows you to specify an SQL Filter to limit the data displayed and data sorting options in the edit grid window: +.. image:: images/editgrid_filter_dialog.png + :alt: Edit grid filter dialog window + +* Use *SQL Filter* to provide SQL filtering criteria. These will be added to the "WHERE" clause of the query used to retrieve the data. For example, you might enter: + +.. code-block:: sql + + id > 25 AND created > '2018-01-01' + +* Use *Data Sorting* to sort the data in the output grid + +To add new column(s) in data sorting grid, click on the [+] icon. + +* Use the drop-down *Column* to select the column you want to sort. +* Use the drop-down *Order* to select the sort order for the column. + +To delete a row from the grid, click the trash icon. + +* Click the *Help* button (?) to access online help. +* Click the *Ok* button to save work. +* Click the *Close* button to discard current changes and close the dialog. diff --git a/docs/en_US/images/editgrid_filter_dialog.png b/docs/en_US/images/editgrid_filter_dialog.png new file mode 100644 index 000000000..046d9a124 Binary files /dev/null and b/docs/en_US/images/editgrid_filter_dialog.png differ diff --git a/docs/en_US/release_notes_3_0.rst b/docs/en_US/release_notes_3_0.rst index b41bd37f0..ade1c0d28 100644 --- a/docs/en_US/release_notes_3_0.rst +++ b/docs/en_US/release_notes_3_0.rst @@ -10,7 +10,7 @@ This release contains a number of features and fixes reported since the release Features ******** -| `Feature #1305 `_ - Enable building of the runtime from the top level Makefile +| `Feature #1894 `_ - Allow sorting when viewing/editing data | `Feature #1978 `_ - Add the ability to enable/disable UI animations | `Feature #2895 `_ - Add keyboard navigation options for the main browser windows | `Feature #2896 `_ - Add keyboard navigation in Query tool module via Tab/Shift-Tab key diff --git a/web/pgadmin/static/js/sqleditor/filter_dialog.js b/web/pgadmin/static/js/sqleditor/filter_dialog.js new file mode 100644 index 000000000..0ba9e357a --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/filter_dialog.js @@ -0,0 +1,243 @@ +define([ + 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', + 'pgadmin.alertifyjs', 'sources/pgadmin', 'backbone', + 'pgadmin.backgrid', 'pgadmin.backform', 'axios', + 'sources/sqleditor/query_tool_actions', + 'sources/sqleditor/filter_dialog_model', + //'pgadmin.browser.node.ui', +], function( + gettext, url_for, $, _, S, Alertify, pgAdmin, Backbone, + Backgrid, Backform, axios, queryToolActions, filterDialogModel +) { + + let FilterDialog = { + 'dialog': function(handler) { + let title = gettext('Sort/Filter options'); + axios.get( + url_for('sqleditor.get_filter_data', { + 'trans_id': handler.transId, + }), + { headers: {'Cache-Control' : 'no-cache'} } + ).then(function (res) { + let response = res.data.data.result; + + // Check the alertify dialog already loaded then delete it to clear + // the cache + if (Alertify.filterDialog) { + delete Alertify.filterDialog; + } + + // Create Dialog + Alertify.dialog('filterDialog', function factory() { + let $container = $('
'); + return { + main: function() { + this.set('title', gettext('Sort/Filter options')); + }, + build: function() { + this.elements.content.appendChild($container.get(0)); + Alertify.pgDialogBuild.apply(this); + }, + setup: function() { + return { + buttons: [{ + text: '', + key: 112, + className: 'btn btn-default pull-left fa fa-lg fa-question', + attrs: { + name: 'dialog_help', + type: 'button', + label: gettext('Help'), + url: url_for('help.static', { + 'filename': 'editgrid.html', + }), + }, + }, { + text: gettext('Ok'), + className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button', + 'data-btn-name': 'ok', + }, { + text: gettext('Cancel'), + key: 27, + className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button', + 'data-btn-name': 'cancel', + }], + // Set options for dialog + options: { + title: title, + //disable both padding and overflow control. + padding: !1, + overflow: !1, + model: 0, + resizable: true, + maximizable: true, + pinnable: false, + closableByDimmer: false, + modal: false, + autoReset: false, + }, + }; + }, + hooks: { + // triggered when the dialog is closed + onclose: function() { + if (this.view) { + this.filterCollectionModel.stopSession(); + this.view.model.stopSession(); + this.view.remove({ + data: true, + internal: true, + silent: true, + }); + } + }, + }, + prepare: function() { + let self = this; + $container.html(''); + // Disable Ok button + this.__internal.buttons[1].element.disabled = true; + + // Status bar + this.statusBar = $('
' + + '
' + + '
' + + '
' + + ' ' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
', { + text: '', + }).appendTo($container); + + // To show progress on filter Saving/Updating on AJAX + this.showFilterProgress = $( + '').appendTo($container); + + $( + self.showFilterProgress[0] + ).removeClass('hidden'); + + self.filterCollectionModel = filterDialogModel(response); + + let fields = Backform.generateViewSchema( + null, self.filterCollectionModel, 'create', null, null, true + ); + + let view = this.view = new Backform.Dialog({ + el: '
', + model: self.filterCollectionModel, + schema: fields, + }); + + $(this.elements.body.childNodes[0]).addClass( + 'alertify_tools_dialog_properties obj_properties' + ); + + $container.append(view.render().$el); + + // Enable/disable save button and show/hide statusbar based on session + view.listenTo(view.model, 'pgadmin-session:start', function() { + view.listenTo(view.model, 'pgadmin-session:invalid', function(msg) { + self.statusBar.removeClass('hide'); + $(self.statusBar.find('.alert-text')).html(msg); + // Disable Okay button + self.__internal.buttons[1].element.disabled = true; + }); + + view.listenTo(view.model, 'pgadmin-session:valid', function() { + self.statusBar.addClass('hide'); + $(self.statusBar.find('.alert-text')).html(''); + // Enable Okay button + self.__internal.buttons[1].element.disabled = false; + }); + }); + + view.listenTo(view.model, 'pgadmin-session:stop', function() { + view.stopListening(view.model, 'pgadmin-session:invalid'); + view.stopListening(view.model, 'pgadmin-session:valid'); + }); + + // Starts monitoring changes to model + view.model.startNewSession(); + + // Set data in collection + let viewDataSortingModel = view.model.get('data_sorting'); + viewDataSortingModel.add(response['data_sorting']); + + // Hide Progress ... + $( + self.showFilterProgress[0] + ).addClass('hidden'); + + }, + // Callback functions when click on the buttons of the Alertify dialogs + callback: function(e) { + let self = this; + + if (e.button.element.name == 'dialog_help') { + e.cancel = true; + pgAdmin.Browser.showHelp(e.button.element.name, e.button.element.getAttribute('url'), + null, null, e.button.element.getAttribute('label')); + return; + } else if (e.button['data-btn-name'] === 'ok') { + e.cancel = true; // Do not close dialog + + let filterCollectionModel = this.filterCollectionModel.toJSON(); + + // Show Progress ... + $( + self.showFilterProgress[0] + ).removeClass('hidden'); + + axios.put( + url_for('sqleditor.set_filter_data', { + 'trans_id': handler.transId, + }), + filterCollectionModel + ).then(function () { + // Hide Progress ... + $( + self.showFilterProgress[0] + ).addClass('hidden'); + setTimeout( + function() { + self.close(); // Close the dialog now + Alertify.success(gettext('Filter updated successfully')); + queryToolActions.executeQuery(handler); + }, 10 + ); + + }).catch(function (error) { + // Hide Progress ... + $( + self.showFilterProgress[0] + ).addClass('hidden'); + handler.onExecuteHTTPError(error); + + setTimeout( + function() { + Alertify.error(error); + }, 10 + ); + }); + } else { + self.close(); + } + }, + }; + }); + + Alertify.filterDialog(title).resizeTo('65%', '60%'); + }); + }, + }; + return FilterDialog; +}); diff --git a/web/pgadmin/static/js/sqleditor/filter_dialog_model.js b/web/pgadmin/static/js/sqleditor/filter_dialog_model.js new file mode 100644 index 000000000..c3146a40a --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/filter_dialog_model.js @@ -0,0 +1,133 @@ +define([ + 'sources/gettext', 'underscore', 'sources/pgadmin', + 'pgadmin.backform', 'pgadmin.backgrid', +], function( + gettext, _, pgAdmin, Backform, Backgrid +) { + + let initModel = function(response) { + + let order_mapping = { + 'asc': gettext('ASC'), + 'desc': gettext('DESC'), + }; + + let DataSortingModel = pgAdmin.Browser.DataModel.extend({ + idAttribute: 'name', + defaults: { + name: undefined, + order: 'asc', + }, + schema: [{ + id: 'name', + name: 'name', + label: gettext('Column'), + cell: 'select2', + editable: true, + cellHeaderClasses: 'width_percent_60', + headerCell: Backgrid.Extension.CustomHeaderCell, + disabled: false, + control: 'select2', + select2: { + allowClear: false, + }, + options: function() { + return _.map(response.column_list, (obj) => { + return { + value: obj, + label: obj, + }; + }); + }, + }, + { + id: 'order', + name: 'order', + label: gettext('Order'), + control: 'select2', + cell: 'select2', + cellHeaderClasses: 'width_percent_40', + headerCell: Backgrid.Extension.CustomHeaderCell, + editable: true, + deps: ['type'], + select2: { + allowClear: false, + }, + options: function() { + return _.map(order_mapping, (val, key) => { + return { + value: key, + label: val, + }; + }); + }, + }, + ], + validate: function() { + let msg = null; + this.errorModel.clear(); + if (_.isUndefined(this.get('name')) || + _.isNull(this.get('name')) || + String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') { + msg = gettext('Please select a column.'); + this.errorModel.set('name', msg); + return msg; + } else if (_.isUndefined(this.get('order')) || + _.isNull(this.get('order')) || + String(this.get('order')).replace(/^\s+|\s+$/g, '') == '') { + msg = gettext('Please select the order.'); + this.errorModel.set('order', msg); + return msg; + } + return null; + }, + }); + + let FilterCollectionModel = pgAdmin.Browser.DataModel.extend({ + idAttribute: 'sql', + defaults: { + sql: response.sql || null, + }, + schema: [{ + id: 'sql', + label: gettext('SQL Filter'), + cell: 'string', + type: 'text', mode: ['create'], + control: Backform.SqlFieldControl.extend({ + render: function() { + let obj = Backform.SqlFieldControl.prototype.render.apply(this, arguments); + // We need to set focus on editor after the dialog renders + setTimeout(() => { + obj.sqlCtrl.focus(); + }, 1000); + return obj; + }, + }), + extraClasses:['custom_height_css_class'], + },{ + id: 'data_sorting', + name: 'data_sorting', + label: gettext('Data Sorting'), + model: DataSortingModel, + editable: true, + type: 'collection', + mode: ['create'], + control: 'unique-col-collection', + uniqueCol: ['name'], + canAdd: true, + canEdit: false, + canDelete: true, + visible: true, + version_compatible: true, + }], + validate: function() { + return null; + }, + }); + + let model = new FilterCollectionModel(); + return model; + }; + + return initModel; +}); diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index 2f3bb0578..b28f185c3 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -79,7 +79,7 @@
  • - {{ _('Find next') }}{% if client_platform == 'macos' -%} + {{ _('Find Next') }}{% if client_platform == 'macos' -%} {{ _(' (Cmd+G)') }} {% else %} {{ _(' (Ctrl+G)') }}{%- endif %} @@ -87,7 +87,7 @@
  • - {{ _('Find previous') }}{% if client_platform == 'macos' -%} + {{ _('Find Previous') }}{% if client_platform == 'macos' -%} {{ _(' (Cmd+Shift+G)') }} {% else %} {{ _(' (Ctrl+Shift+G)') }}{%- endif %} @@ -95,7 +95,7 @@
  • - {{ _('Persistent find') }} + {{ _('Persistent Find') }}
  • @@ -109,7 +109,7 @@
  • - {{ _('Replace all') }} + {{ _('Replace All') }}
  • @@ -194,10 +194,10 @@ @@ -341,23 +341,6 @@
    - -
    diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index f8d7d97bf..fae527c7d 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -40,6 +40,7 @@ from pgadmin.tools.sqleditor.utils.query_tool_preferences import \ RegisterQueryToolPreferences from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \ read_file_generator +from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog MODULE_NAME = 'sqleditor' @@ -92,8 +93,6 @@ class SqlEditorModule(PgAdminModule): 'sqleditor.fetch', 'sqleditor.fetch_all', 'sqleditor.save', - 'sqleditor.get_filter', - 'sqleditor.apply_filter', 'sqleditor.inclusive_filter', 'sqleditor.exclusive_filter', 'sqleditor.remove_filter', @@ -106,7 +105,9 @@ class SqlEditorModule(PgAdminModule): 'sqleditor.load_file', 'sqleditor.save_file', 'sqleditor.query_tool_download', - 'sqleditor.connection_status' + 'sqleditor.connection_status', + 'sqleditor.get_filter_data', + 'sqleditor.set_filter_data' ] def register_preferences(self): @@ -783,80 +784,6 @@ def save(trans_id): ) -@blueprint.route( - '/filter/get/', - methods=["GET"], endpoint='get_filter' -) -@login_required -def get_filter(trans_id): - """ - This method is used to get the existing filter. - - Args: - trans_id: unique transaction id - """ - - # Check the transaction and connection status - status, error_msg, conn, trans_obj, session_obj = \ - check_transaction_status(trans_id) - - if error_msg == gettext('Transaction ID not found in the session.'): - return make_json_response(success=0, errormsg=error_msg, - info='DATAGRID_TRANSACTION_REQUIRED', - status=404) - if status and conn is not None and \ - trans_obj is not None and session_obj is not None: - - res = trans_obj.get_filter() - else: - status = False - res = error_msg - - return make_json_response(data={'status': status, 'result': res}) - - -@blueprint.route( - '/filter/apply/', - methods=["PUT", "POST"], endpoint='apply_filter' -) -@login_required -def apply_filter(trans_id): - """ - This method is used to apply the filter. - - Args: - trans_id: unique transaction id - """ - if request.data: - filter_sql = json.loads(request.data, encoding='utf-8') - else: - filter_sql = request.args or request.form - - # Check the transaction and connection status - status, error_msg, conn, trans_obj, session_obj = \ - check_transaction_status(trans_id) - - if error_msg == gettext('Transaction ID not found in the session.'): - return make_json_response(success=0, errormsg=error_msg, - info='DATAGRID_TRANSACTION_REQUIRED', - status=404) - - if status and conn is not None and \ - trans_obj is not None and session_obj is not None: - - status, res = trans_obj.set_filter(filter_sql) - - # As we changed the transaction object we need to - # restore it and update the session variable. - session_obj['command_obj'] = pickle.dumps(trans_obj, -1) - update_session_grid_transaction(trans_id, session_obj) - else: - status = False - res = error_msg - - return make_json_response(data={'status': status, 'result': res}) - - @blueprint.route( '/filter/inclusive/', methods=["PUT", "POST"], endpoint='inclusive_filter' @@ -1561,3 +1488,37 @@ def query_tool_status(trans_id): return internal_server_error( errormsg=gettext("Transaction status check failed.") ) + + +@blueprint.route( + '/filter_dialog/', + methods=["GET"], endpoint='get_filter_data' +) +@login_required +def get_filter_data(trans_id): + """ + This method is used to get all the columns for data sorting dialog. + + Args: + trans_id: unique transaction id + """ + return FilterDialog.get(*check_transaction_status(trans_id)) + + +@blueprint.route( + '/filter_dialog/', + methods=["PUT"], endpoint='set_filter_data' +) +@login_required +def set_filter_data(trans_id): + """ + This method is used to update the columns for data sorting dialog. + + Args: + trans_id: unique transaction id + """ + return FilterDialog.save( + *check_transaction_status(trans_id), + request=request, + trans_id=trans_id + ) diff --git a/web/pgadmin/tools/sqleditor/command.py b/web/pgadmin/tools/sqleditor/command.py index 7ec03c581..fbe37df69 100644 --- a/web/pgadmin/tools/sqleditor/command.py +++ b/web/pgadmin/tools/sqleditor/command.py @@ -141,6 +141,10 @@ class SQLFilter(object): - This method removes the filter applied. * validate_filter(row_filter) - This method validates the given filter. + * get_data_sorting() + - This method returns columns for data sorting + * set_data_sorting() + - This method saves columns for data sorting """ def __init__(self, **kwargs): @@ -160,8 +164,8 @@ class SQLFilter(object): self.sid = kwargs['sid'] self.did = kwargs['did'] self.obj_id = kwargs['obj_id'] - self.__row_filter = kwargs['sql_filter'] if 'sql_filter' in kwargs \ - else None + self.__row_filter = kwargs.get('sql_filter', None) + self.__dara_sorting = kwargs.get('data_sorting', None) manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid) conn = manager.connection(did=self.did) @@ -210,20 +214,41 @@ class SQLFilter(object): return status, msg + def get_data_sorting(self): + """ + This function returns the filter. + """ + if self.__dara_sorting and len(self.__dara_sorting) > 0: + return self.__dara_sorting + return None + + def set_data_sorting(self, data_filter): + """ + This function validates the filter and set the + given filter to member variable. + """ + self.__dara_sorting = data_filter['data_sorting'] + def is_filter_applied(self): """ This function returns True if filter is applied else False. """ + is_filter_applied = True if self.__row_filter is None or self.__row_filter == '': - return False + is_filter_applied = False - return True + if not is_filter_applied: + if self.__dara_sorting and len(self.__dara_sorting) > 0: + is_filter_applied = True + + return is_filter_applied def remove_filter(self): """ This function remove the filter by setting value to None. """ self.__row_filter = None + self.__dara_sorting = None def append_filter(self, row_filter): """ @@ -325,13 +350,58 @@ class GridCommand(BaseCommand, SQLFilter, FetchedRowTracker): self.cmd_type = kwargs['cmd_type'] if 'cmd_type' in kwargs else None self.limit = -1 - if self.cmd_type == VIEW_FIRST_100_ROWS or \ - self.cmd_type == VIEW_LAST_100_ROWS: + if self.cmd_type in (VIEW_FIRST_100_ROWS, VIEW_LAST_100_ROWS): self.limit = 100 def get_primary_keys(self, *args, **kwargs): return None, None + def get_all_columns_with_order(self, default_conn): + """ + Responsible for fetching columns from given object + + Args: + default_conn: Connection object + + Returns: + all_sorted_columns: Columns which are already sorted which will + be used to populate the Grid in the dialog + all_columns: List of all the column for given object which will + be used to fill columns options + """ + driver = get_driver(PG_DEFAULT_DRIVER) + if default_conn is None: + manager = driver.connection_manager(self.sid) + conn = manager.connection(did=self.did, conn_id=self.conn_id) + else: + conn = default_conn + + all_sorted_columns = [] + data_sorting = self.get_data_sorting() + all_columns = [] + if conn.connected(): + # Fetch the rest of the column names + query = render_template( + "/".join([self.sql_path, 'get_columns.sql']), + obj_id=self.obj_id + ) + status, result = conn.execute_dict(query) + if not status: + raise Exception(result) + + for row in result['rows']: + all_columns.append(row['attname']) + else: + raise Exception( + gettext('Not connected to server or connection with the ' + 'server has been closed.') + ) + # If user has custom data sorting then pass as it as it is + if data_sorting and len(data_sorting) > 0: + all_sorted_columns = data_sorting + + return all_sorted_columns, all_columns + def save(self, changed_data, default_conn=None): return forbidden( errmsg=gettext("Data cannot be saved for the current object.") @@ -351,6 +421,17 @@ class GridCommand(BaseCommand, SQLFilter, FetchedRowTracker): """ self.limit = limit + def get_pk_order(self): + """ + This function gets the order required for primary keys + """ + if self.cmd_type in (VIEW_FIRST_100_ROWS, VIEW_ALL_ROWS): + return 'asc' + elif self.cmd_type == VIEW_LAST_100_ROWS: + return 'desc' + else: + return None + class TableCommand(GridCommand): """ @@ -385,6 +466,7 @@ class TableCommand(GridCommand): has_oids = self.has_oids(default_conn) sql_filter = self.get_filter() + data_sorting = self.get_data_sorting() if sql_filter is None: sql = render_template( @@ -392,7 +474,8 @@ class TableCommand(GridCommand): object_name=self.object_name, nsp_name=self.nsp_name, pk_names=pk_names, cmd_type=self.cmd_type, limit=self.limit, - primary_keys=primary_keys, has_oids=has_oids + primary_keys=primary_keys, has_oids=has_oids, + data_sorting=data_sorting ) else: sql = render_template( @@ -401,7 +484,7 @@ class TableCommand(GridCommand): nsp_name=self.nsp_name, pk_names=pk_names, cmd_type=self.cmd_type, sql_filter=sql_filter, limit=self.limit, primary_keys=primary_keys, - has_oids=has_oids + has_oids=has_oids, data_sorting=data_sorting ) return sql @@ -447,6 +530,73 @@ class TableCommand(GridCommand): return pk_names, primary_keys + def get_all_columns_with_order(self, default_conn=None): + """ + It is overridden method specially for Table because we all have to + fetch primary keys and rest of the columns both. + + Args: + default_conn: Connection object + + Returns: + all_sorted_columns: Sorted columns for the Grid + all_columns: List of columns for the select2 options + """ + driver = get_driver(PG_DEFAULT_DRIVER) + if default_conn is None: + manager = driver.connection_manager(self.sid) + conn = manager.connection(did=self.did, conn_id=self.conn_id) + else: + conn = default_conn + + all_sorted_columns = [] + data_sorting = self.get_data_sorting() + all_columns = [] + if conn.connected(): + + # Fetch the primary key column names + query = render_template( + "/".join([self.sql_path, 'primary_keys.sql']), + obj_id=self.obj_id + ) + + status, result = conn.execute_dict(query) + if not status: + raise Exception(result) + + for row in result['rows']: + all_columns.append(row['attname']) + all_sorted_columns.append( + { + 'name': row['attname'], + 'order': self.get_pk_order() + } + ) + + # Fetch the rest of the column names + query = render_template( + "/".join([self.sql_path, 'get_columns.sql']), + obj_id=self.obj_id + ) + status, result = conn.execute_dict(query) + if not status: + raise Exception(result) + + for row in result['rows']: + # Only append if not already present in the list + if row['attname'] not in all_columns: + all_columns.append(row['attname']) + else: + raise Exception( + gettext('Not connected to server or connection with the ' + 'server has been closed.') + ) + # If user has custom data sorting then pass as it as it is + if data_sorting and len(data_sorting) > 0: + all_sorted_columns = data_sorting + + return all_sorted_columns, all_columns + def can_edit(self): return True @@ -771,20 +921,22 @@ class ViewCommand(GridCommand): to fetch the data for the specified view """ sql_filter = self.get_filter() + data_sorting = self.get_data_sorting() if sql_filter is None: sql = render_template( "/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, nsp_name=self.nsp_name, cmd_type=self.cmd_type, - limit=self.limit + limit=self.limit, data_sorting=data_sorting ) else: sql = render_template( "/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, nsp_name=self.nsp_name, cmd_type=self.cmd_type, - sql_filter=sql_filter, limit=self.limit + sql_filter=sql_filter, limit=self.limit, + data_sorting=data_sorting ) return sql @@ -832,20 +984,22 @@ class ForeignTableCommand(GridCommand): to fetch the data for the specified foreign table """ sql_filter = self.get_filter() + data_sorting = self.get_data_sorting() if sql_filter is None: sql = render_template( "/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, nsp_name=self.nsp_name, cmd_type=self.cmd_type, - limit=self.limit + limit=self.limit, data_sorting=data_sorting ) else: sql = render_template( "/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, nsp_name=self.nsp_name, cmd_type=self.cmd_type, - sql_filter=sql_filter, limit=self.limit + sql_filter=sql_filter, limit=self.limit, + data_sorting=data_sorting ) return sql @@ -883,20 +1037,22 @@ class CatalogCommand(GridCommand): to fetch the data for the specified catalog object """ sql_filter = self.get_filter() + data_sorting = self.get_data_sorting() if sql_filter is None: sql = render_template( "/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, nsp_name=self.nsp_name, cmd_type=self.cmd_type, - limit=self.limit + limit=self.limit, data_sorting=data_sorting ) else: sql = render_template( "/".join([self.sql_path, 'objectquery.sql']), object_name=self.object_name, nsp_name=self.nsp_name, cmd_type=self.cmd_type, - sql_filter=sql_filter, limit=self.limit + sql_filter=sql_filter, limit=self.limit, + data_sorting=data_sorting ) return sql @@ -929,6 +1085,9 @@ class QueryToolCommand(BaseCommand, FetchedRowTracker): def get_sql(self, default_conn=None): return None + def get_all_columns_with_order(self, default_conn=None): + return None + def can_edit(self): return False diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css index 46588dcec..c54590d3d 100644 --- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css +++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css @@ -602,3 +602,26 @@ input.editor-checkbox:focus { font-size: 13px; line-height: 3em; } + +/* For Filter status bar */ +.data_sorting_dialog .pg-prop-status-bar { + position: absolute; + bottom: 37px; + z-index: 5; +} + +.data_sorting_dialog .CodeMirror-gutter-wrapper { + left: -30px !important; +} + +.data_sorting_dialog .CodeMirror-gutters { + left: 0px !important; +} + +.data_sorting_dialog .custom_height_css_class { + height: 100px; +} + +.data_sorting_dialog .data_sorting { + padding: 10px 0px; +} diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 60dacbb20..f8cb05af0 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -14,6 +14,7 @@ define('tools.querytool', [ 'sources/sqleditor_utils', 'sources/sqleditor/execute_query', 'sources/sqleditor/query_tool_http_error_handler', + 'sources/sqleditor/filter_dialog', 'sources/history/index.js', 'sources/../jsx/history/query_history', 'react', 'react-dom', @@ -33,7 +34,7 @@ define('tools.querytool', [ ], function( babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror, pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, - XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, + XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler, HistoryBundle, queryHistory, React, ReactDOM, keyboardShortcuts, queryToolActions, Datagrid, modifyAnimation, calculateQueryRunTime, callRenderAfterPoll) { @@ -112,8 +113,7 @@ define('tools.querytool', [ // This function is used to render the template. render: function() { - var self = this, - filter = self.$el.find('#sql_filter'); + var self = this; $('.editor-title').text(_.unescape(self.editor_title)); self.checkConnectionStatus(); @@ -121,31 +121,6 @@ define('tools.querytool', [ // Fetch and assign the shortcuts to current instance self.keyboardShortcutConfig = queryToolActions.getKeyboardShortcuts(self); - self.filter_obj = CodeMirror.fromTextArea(filter.get(0), { - tabindex: '0', - lineNumbers: true, - mode: self.handler.server_type === 'gpdb' ? 'text/x-gpsql' : '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, - indentWithTabs: pgAdmin.Browser.editor_options.indent_with_tabs, - indentUnit: pgAdmin.Browser.editor_options.tabSize, - 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, - }); - // Updates connection status flag self.gain_focus = function() { setTimeout(function() { @@ -2141,11 +2116,11 @@ define('tools.querytool', [ 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'); + $('#btn-filter').addClass('btn-primary'); + $('#btn-filter-dropdown').addClass('btn-primary'); } else { - $('#btn-filter').removeClass('btn-warning'); - $('#btn-filter-dropdown').removeClass('btn-warning'); + $('#btn-filter').removeClass('btn-primary'); + $('#btn-filter-dropdown').removeClass('btn-primary'); $('#btn-filter').addClass('btn-default'); $('#btn-filter-dropdown').addClass('btn-default'); } @@ -3044,50 +3019,8 @@ define('tools.querytool', [ // 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); - // Set focus on filter area - self.gridView.filter_obj.focus(); - } else { - setTimeout( - function() { - alertify.alert(gettext('Get Filter Error'), res.data.result); - }, 10 - ); - } - }, - error: function(e) { - self.trigger('pgadmin-sqleditor:loading-icon:hide'); - let msg = httpErrorHandler.handleQueryToolAjaxError( - pgAdmin, self, e, '_show_filter', [], true - ); - setTimeout( - function() { - alertify.alert(gettext('Get Filter Error'), msg); - }, 10 - ); - }, - }); + let self = this; + FilterHandler.dialog(self); }, // This function will include the filter by selection. diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql new file mode 100644 index 000000000..610747dfb --- /dev/null +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql @@ -0,0 +1,9 @@ +{# ============= Fetch the columns ============= #} +{% if obj_id %} +SELECT at.attname, ty.typname + FROM pg_attribute at + LEFT JOIN pg_type ty ON (ty.oid = at.atttypid) +WHERE attrelid={{obj_id}}::oid + AND at.attnum > 0 + AND at.attisdropped = FALSE +{% endif %} diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql index 1cb60d913..add1658ec 100644 --- a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql @@ -3,7 +3,11 @@ SELECT {% if has_oids %}oid, {% endif %}* FROM {{ conn|qtIdent(nsp_name, object_ {% if sql_filter %} WHERE {{ sql_filter }} {% endif %} -{% if primary_keys %} +{% if data_sorting and data_sorting|length > 0 %} +ORDER BY {% for obj in data_sorting %} +{{ conn|qtIdent(obj.name) }} {{ obj.order|upper }}{% if not loop.last %}, {% else %} {% endif %} +{% endfor %} +{% elif primary_keys %} ORDER BY {% for p in primary_keys %}{{conn|qtIdent(p)}}{% if cmd_type == 1 or cmd_type == 3 %} ASC{% elif cmd_type == 2 %} DESC{% endif %} {% if not loop.last %}, {% else %} {% endif %}{% endfor %} {% endif %} diff --git a/web/pgadmin/tools/sqleditor/utils/filter_dialog.py b/web/pgadmin/tools/sqleditor/utils/filter_dialog.py new file mode 100644 index 000000000..d7064979f --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/filter_dialog.py @@ -0,0 +1,95 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Code to handle data sorting in view data mode.""" +import pickle +import simplejson as json +from flask_babelex import gettext +from pgadmin.utils.ajax import make_json_response, internal_server_error +from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \ + update_session_grid_transaction + + +class FilterDialog(object): + @staticmethod + def get(*args): + """To fetch the current sorted columns""" + status, error_msg, conn, trans_obj, session_obj = args + if error_msg == gettext('Transaction ID not found in the session.'): + return make_json_response( + success=0, + errormsg=error_msg, + info='DATAGRID_TRANSACTION_REQUIRED', + status=404 + ) + column_list = [] + if status and conn is not None and \ + trans_obj is not None and session_obj is not None: + msg = gettext('Success') + columns, column_list = trans_obj.get_all_columns_with_order(conn) + sql = trans_obj.get_filter() + else: + status = False + msg = error_msg + columns = None + sql = None + + return make_json_response( + data={ + 'status': status, + 'msg': msg, + 'result': { + 'data_sorting': columns, + 'column_list': column_list, + 'sql': sql + } + } + ) + + @staticmethod + def save(*args, **kwargs): + """To save the sorted columns""" + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = args + trans_id = kwargs['trans_id'] + request = kwargs['request'] + + if request.data: + data = json.loads(request.data, encoding='utf-8') + else: + data = request.args or request.form + + if error_msg == gettext('Transaction ID not found in the session.'): + return make_json_response( + success=0, + errormsg=error_msg, + info='DATAGRID_TRANSACTION_REQUIRED', + status=404 + ) + + if status and conn is not None and \ + trans_obj is not None and session_obj is not None: + trans_obj.set_data_sorting(data) + trans_obj.set_filter(data.get('sql')) + # As we changed the transaction object we need to + # restore it and update the session variable. + session_obj['command_obj'] = pickle.dumps(trans_obj, -1) + update_session_grid_transaction(trans_id, session_obj) + res = gettext('Data sorting object updated successfully') + else: + return internal_server_error( + errormsg=gettext('Failed to update the data on server.') + ) + + return make_json_response( + data={ + 'status': status, + 'result': res + } + ) diff --git a/web/pgadmin/tools/sqleditor/utils/tests/test_filter_dialog_callbacks.py b/web/pgadmin/tools/sqleditor/utils/tests/test_filter_dialog_callbacks.py new file mode 100644 index 000000000..97479782e --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/tests/test_filter_dialog_callbacks.py @@ -0,0 +1,103 @@ +####################################################################### +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Apply Explain plan wrapper to sql object.""" +from pgadmin.utils.ajax import make_json_response, internal_server_error +from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog +from pgadmin.utils.route import BaseTestGenerator + +TX_ID_ERROR_MSG = 'Transaction ID not found in the session.' +FAILED_TX_MSG = 'Failed to update the data on server.' + + +class MockRequest(object): + "To mock request object" + def __init__(self): + self.data = None + self.args = "Test data", + + +class StartRunningDataSortingTest(BaseTestGenerator): + """ + Check that the DataSorting methods works as + intended + """ + scenarios = [ + ('When we do not find Transaction ID in session in get', dict( + input_parameters=(None, TX_ID_ERROR_MSG, None, None, None), + expected_return_response={ + 'success': 0, + 'errormsg': TX_ID_ERROR_MSG, + 'info': 'DATAGRID_TRANSACTION_REQUIRED', + 'status': 404 + }, + type='get' + )), + ('When we pass all the values as None in get', dict( + input_parameters=(None, None, None, None, None), + expected_return_response={ + 'data': { + 'status': False, + 'msg': None, + 'result': { + 'data_sorting': None, + 'column_list': [] + } + } + }, + type='get' + )), + + ('When we do not find Transaction ID in session in save', dict( + input_arg_parameters=(None, TX_ID_ERROR_MSG, None, None, None), + input_kwarg_parameters={ + 'trans_id': None, + 'request': MockRequest() + }, + expected_return_response={ + 'success': 0, + 'errormsg': TX_ID_ERROR_MSG, + 'info': 'DATAGRID_TRANSACTION_REQUIRED', + 'status': 404 + }, + type='save' + )), + + ('When we pass all the values as None in save', dict( + input_arg_parameters=(None, None, None, None, None), + input_kwarg_parameters={ + 'trans_id': None, + 'request': MockRequest() + }, + expected_return_response={ + 'status': 500, + 'success': 0, + 'errormsg': FAILED_TX_MSG + + }, + type='save' + )) + ] + + def runTest(self): + expected_response = make_json_response( + **self.expected_return_response + ) + if self.type == 'get': + result = FilterDialog.get(*self.input_parameters) + self.assertEquals( + result.status_code, expected_response.status_code + ) + else: + result = FilterDialog.save( + *self.input_arg_parameters, **self.input_kwarg_parameters + ) + self.assertEquals( + result.status_code, expected_response.status_code + ) diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js new file mode 100644 index 000000000..e13fa0974 --- /dev/null +++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js @@ -0,0 +1,31 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// +import filterDialog from 'sources/sqleditor/filter_dialog'; +import filterDialogModel from 'sources/sqleditor/filter_dialog_model'; + +describe('filterDialog', () => { + let sqlEditorController; + sqlEditorController = jasmine.createSpy('sqlEditorController') + describe('filterDialog', () => { + describe('when using filter dialog', () => { + beforeEach(() => { + spyOn(filterDialog, 'dialog'); + }); + + it("it should be defined as function", function() { + expect(filterDialog.dialog).toBeDefined(); + }); + + it('it should call without proper handler', () => { + expect(filterDialog.dialog).not.toHaveBeenCalledWith({}); + }); + + }); + }); +});