public inbox for [email protected]help / color / mirror / Atom feed
[pgAdmin4][RM#3055] Allow user to sort the data in View data mode 25+ messages / 4 participants [nested] [flat]
* [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-25 18:13 Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Murtuza Zabuawala @ 2018-03-25 18:13 UTC (permalink / raw) To: pgadmin-hackers Hi, PFA patch which allow user to sort the data in View data mode. -- Regards, Murtuza Zabuawala EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company Attachments: [application/octet-stream] RM_3055.diff (92.0K, 3-RM_3055.diff) download | inline diff: diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst index 1cb23cf..b8b58e5 100644 --- a/docs/en_US/editgrid.rst +++ b/docs/en_US/editgrid.rst @@ -46,16 +46,19 @@ Hover over an icon to display a tooltip that describes the icon's functionality. | | | | | | Use options on the *Filter* menu to quick-sort or quick-filter the data set: | | | | | | -| | * Filter: This option opens a dialog that allows you to define a filter. A filter is a | | +| | * SQL Filter: This option opens a dialog that allows you to define a filter. A filter is a | | | | condition that is supplied to an arbitrary WHERE clause that restricts the result set. | | | | | | -| | * Remove Filter: This option removes all selection / exclusion filter conditions. | | +| | * Data Sorting: This option allows user to sort their data as per respective columns order | | +| | using data sorting dialog. | | | | | | | | * By Selection: This option refreshes the data set and displays only those rows whose | | | | column value matches the value in the cell currently selected. | | | | | | | | * Exclude Selection: This option refreshes the data set and excludes those rows whose | | | | column value matches the value in the cell currently selected. | | +| | | | +| | * Remove Filter: This option removes any existing filter conditions. | | +----------------------+---------------------------------------------------------------------------------------------------+-------------+ | *No limit* | Use the *No limit* drop-down listbox to specify how many rows to display in the output panel. | | | | Select from: *No limit* (the default), *1000 rows*, *500 rows*, or *100 rows*. | | @@ -95,6 +98,20 @@ 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. +**The Data Sorting dialog** + +.. image:: images/editgrid_data_sorting_dialog.png + :alt: Edit grid data sorting dialog + +Provide information about the data sorting in the edit grid window: + +To add new column for data sorting, 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 discard newly added or an existing row(s) 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. \ No newline at end of file diff --git a/docs/en_US/images/editgrid_data_sorting_dialog.png b/docs/en_US/images/editgrid_data_sorting_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..4db53dbe0fc6be9b3e2168646b2add7f0f4f8701 GIT binary patch literal 42262 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 TVxu_mAO3<FnCVk*IzRa@1dm;R literal 0 HcmV?d00001 diff --git a/web/pgadmin/static/js/sqleditor/data_sorting.js b/web/pgadmin/static/js/sqleditor/data_sorting.js new file mode 100644 index 0000000..2c3849c --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/data_sorting.js @@ -0,0 +1,339 @@ +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', + //'pgadmin.browser.node.ui', +], function( + gettext, url_for, $, _, S, Alertify, pgAdmin, Backbone, + Backgrid, Backform, axios, queryToolActions +) { + + let order_mapping = { + 'asc': gettext('ASC'), + 'desc': gettext('DESC'), + }; + + let dataSorting = { + 'dialog': function(handler) { + let title = gettext('Data Sorting'); + axios.get( + url_for('sqleditor.get_data_sorting', { + 'trans_id': handler.transId, + }), + { headers: {'Cache-Control' : 'no-cache'} } + ).then(function (res) { + let response = res.data.data.result; + let FilterModel = 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 DataSortingCollectionModel = pgAdmin.Browser.DataModel.extend({ + idAttribute: 'data_sorting', + schema: [{ + id: 'data_sorting', + name: 'data_sorting', + label: gettext('Please select column(s)'), + model: FilterModel, + 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; + }, + }); + + // Check the alertify dialog already loaded then delete it to clear + // the cache + if (Alertify.dataSorting) { + delete Alertify.dataSorting; + } + + // Create Dialog + Alertify.dialog('dataSorting', function factory() { + let $container = $('<div class=\'data_sorting_dialog\'></div>'); + return { + main: function() { + this.set('title', gettext('Data Sorting')); + }, + build: function() { + this.elements.content.appendChild($container.get(0)); + Alertify.pgDialogBuild.apply(this); + }, + setup: function() { + return { + buttons: [{ + text: '', + key: 27, + 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'), + key: 27, + 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.dataSortingCollectionModel.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 = $('<div class=\'pg-prop-status-bar pg-el-xs-12 hide\'>' + + ' <div class=\'media error-in-footer bg-red-1 border-red-2 font-red-3 text-14\'>' + + ' <div class=\'media-body media-middle\'>' + + ' <div class=\'alert-icon error-icon\'>' + + ' <i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>' + + ' </div>' + + ' <div class=\'alert-text\'>' + + ' </div>' + + ' </div>' + + ' </div>' + + '</div>', { + text: '', + }).appendTo($container); + + // To show progress on filter Saving/Updating on AJAX + this.showFilterProgress = $( + '<div id="show_filter_progress" class="wcLoadingIconContainer busy-fetching hidden">' + + '<div class="wcLoadingBackground"></div>' + + '<span class="wcLoadingIcon fa fa-spinner fa-pulse"></span>' + + '<span class="busy-text wcLoadingLabel">' + gettext('Loading data...') + '</span>' + + '</div>').appendTo($container); + + $( + self.showFilterProgress[0] + ).removeClass('hidden'); + + self.dataSortingCollectionModel = new DataSortingCollectionModel(); + + let fields = Backform.generateViewSchema( + null, self.dataSortingCollectionModel, 'create', null, null, true + ); + + let view = this.view = new Backform.Dialog({ + el: '<div></div>', + model: self.dataSortingCollectionModel, + 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 viewModel = view.model.get('data_sorting'); + viewModel.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) { + 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; + } + let self = this; + + if (e.button['data-btn-name'] === 'ok') { + e.cancel = true; // Do not close dialog + + let dataSortingCollectionModel = this.dataSortingCollectionModel.toJSON(); + + // Show Progress ... + $( + self.showFilterProgress[0] + ).removeClass('hidden'); + + axios.put( + url_for('sqleditor.set_data_sorting', { + 'trans_id': handler.transId, + }), + dataSortingCollectionModel + ).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'); + + setTimeout( + function() { + Alertify.error(error); + }, 10 + ); + }); + } + }, + }; + }); + + Alertify.dataSorting(title).resizeTo('65%', '60%'); + }); + }, + }; + return dataSorting; +}); diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index 2f3bb05..1ad1c9a 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -194,10 +194,11 @@ </button> <ul class="dropdown-menu dropdown-menu-right"> <li> - <a id="btn-filter-menu" href="#" tabindex="0">{{ _('Filter') }}</a> - <a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a> + <a id="btn-filter-menu" href="#" tabindex="0">{{ _('SQL Filter') }}</a> + <a id="btn-data-sorting" href="#" tabindex="0">{{ _('Data Sorting') }}</a> <a id="btn-include-filter" href="#" tabindex="0">{{ _('By Selection') }}</a> <a id="btn-exclude-filter" href="#" tabindex="0">{{ _('Exclude Selection') }}</a> + <a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a> </li> </ul> </div> diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 6f5d5b7..ed609d2 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.data_sorting import DataSorting MODULE_NAME = 'sqleditor' @@ -106,7 +107,9 @@ class SqlEditorModule(PgAdminModule): 'sqleditor.load_file', 'sqleditor.save_file', 'sqleditor.query_tool_download', - 'sqleditor.connection_status' + 'sqleditor.connection_status', + 'sqleditor.get_data_sorting', + 'sqleditor.set_data_sorting' ] def register_preferences(self): @@ -1561,3 +1564,37 @@ def query_tool_status(trans_id): return internal_server_error( errormsg=gettext("Transaction status check failed.") ) + + [email protected]( + '/data_sorting/<int:trans_id>', + methods=["GET"], endpoint='get_data_sorting' +) +@login_required +def get_data_sorting(trans_id): + """ + This method is used to get all the columns for data sorting dialog. + + Args: + trans_id: unique transaction id + """ + return DataSorting.get(*check_transaction_status(trans_id)) + + [email protected]( + '/data_sorting/<int:trans_id>', + methods=["PUT"], endpoint='set_data_sorting' +) +@login_required +def set_data_sorting(trans_id): + """ + This method is used to update the columns for data sorting dialog. + + Args: + trans_id: unique transaction id + """ + return DataSorting.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 8cc96e0..993b0d9 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 46588dc..25599a4 100644 --- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css +++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css @@ -602,3 +602,10 @@ 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; +} diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 923ccea..e2b23be 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/is_new_transaction_required', + 'sources/sqleditor/data_sorting', 'sources/history/index.js', 'sources/../jsx/history/query_history', 'react', 'react-dom', @@ -30,7 +31,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, transaction, + XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, transaction, DataSortingHandler, HistoryBundle, queryHistory, React, ReactDOM, keyboardShortcuts, queryToolActions, Datagrid) { /* Return back, this has been called more than once */ @@ -71,6 +72,7 @@ define('tools.querytool', [ 'click #btn-delete-row': 'on_delete', 'click #btn-filter': 'on_show_filter', 'click #btn-filter-menu': 'on_show_filter', + 'click #btn-data-sorting': 'on_data_sorting', 'click #btn-include-filter': 'on_include_filter', 'click #btn-exclude-filter': 'on_exclude_filter', 'click #btn-remove-filter': 'on_remove_filter', @@ -1366,6 +1368,21 @@ define('tools.querytool', [ ); }, + // Callback function for data sorting button click. + on_data_sorting: function(ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + // Trigger the data_sorting signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:data_sorting', + self, + self.handler + ); + }, + // Callback function for include filter button click. on_include_filter: function(ev) { var self = this; @@ -2057,6 +2074,7 @@ define('tools.querytool', [ 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:data_sorting', self._data_sorting, 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); @@ -3245,6 +3263,12 @@ define('tools.querytool', [ }); }, + // This function will used when user wants custom data sorting + _data_sorting: function() { + let self = this; + DataSortingHandler.dialog(self); + }, + // This function will include the filter by selection. _include_filter: function() { var self = this, 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 0000000..610747d --- /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 1cb60d9..add1658 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/data_sorting.py b/web/pgadmin/tools/sqleditor/utils/data_sorting.py new file mode 100644 index 0000000..7a4c406 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/data_sorting.py @@ -0,0 +1,91 @@ +########################################################################## +# +# 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_babel 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 DataSorting(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) + else: + status = False + msg = error_msg + columns = None + + return make_json_response( + data={ + 'status': status, + 'msg': msg, + 'result': { + 'data_sorting': columns, + 'column_list': column_list + } + } + ) + + @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: + sorting_data = json.loads(request.data, encoding='utf-8') + else: + sorting_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(sorting_data) + # 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_data_sorting_callbacks.py b/web/pgadmin/tools/sqleditor/utils/tests/test_data_sorting_callbacks.py new file mode 100644 index 0000000..6364cfb --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/tests/test_data_sorting_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.data_sorting import DataSorting +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 = DataSorting.get(*self.input_parameters) + self.assertEquals( + result.status_code, expected_response.status_code + ) + else: + result = DataSorting.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/data_sorting_specs.js b/web/regression/javascript/sqleditor/data_sorting_specs.js new file mode 100644 index 0000000..cb19556 --- /dev/null +++ b/web/regression/javascript/sqleditor/data_sorting_specs.js @@ -0,0 +1,30 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// +import dataSorting from 'sources/sqleditor/data_sorting'; + +describe('dataSorting', () => { + let sqlEditorController; + sqlEditorController = jasmine.createSpy('sqlEditorController') + describe('dataSorting', () => { + describe('when data sorting filter dialog', () => { + beforeEach(() => { + spyOn(dataSorting, 'dialog'); + }); + + it("it should be defined as function", function() { + expect(dataSorting.dialog).toBeDefined(); + }); + + it('it should call without proper handler', () => { + expect(dataSorting.dialog).not.toHaveBeenCalledWith({}); + }); + + }); + }); +}); ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-26 12:22 Dave Page <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Dave Page @ 2018-03-26 12:22 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: pgadmin-hackers Hi On Sun, Mar 25, 2018 at 7:13 PM, Murtuza Zabuawala < [email protected]> wrote: > Hi, > > PFA patch which allow user to sort the data in View data mode. > The patch looks good in general, however I'm not sure about the UI, in particular that the closely-linked dialogue for filtering is a completely different design. I think it would be better to combine the Sort/Filter options and use a single dialogue for both, as pgAdmin 3 did (though, maybe not using separate tabs for each part, but the top and bottom of the same dialogue. That would certainly fix the consistency of the dialogues (obviously, as there would only be one!), and I think would perhaps be a more simple overall UI, particularly for those that want to sort and filter. Thoughts? Thanks. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-26 16:13 Murtuza Zabuawala <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Murtuza Zabuawala @ 2018-03-26 16:13 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: pgadmin-hackers On Mon, Mar 26, 2018 at 5:52 PM, Dave Page <[email protected]> wrote: > Hi > > On Sun, Mar 25, 2018 at 7:13 PM, Murtuza Zabuawala <murtuza.zabuawala@ > enterprisedb.com> wrote: > >> Hi, >> >> PFA patch which allow user to sort the data in View data mode. >> > > The patch looks good in general, however I'm not sure about the UI, in > particular that the closely-linked dialogue for filtering is a completely > different design. I think it would be better to combine the Sort/Filter > options and use a single dialogue for both, as pgAdmin 3 did (though, maybe > not using separate tabs for each part, but the top and bottom of the same > dialogue. > > That would certainly fix the consistency of the dialogues (obviously, as > there would only be one!), and I think would perhaps be a more simple > overall UI, particularly for those that want to sort and filter. > Sure, I'll send updated it accordingly. > > Thoughts? > > Thanks. > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-26 18:07 Joao De Almeida Pereira <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Joao De Almeida Pereira @ 2018-03-26 18:07 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: Dave Page <[email protected]>; pgadmin-hackers Hi Hackers, @Murtuza: The patch codewise looks good. Nice to see that we are using axios instead of jquery ajax calls and that there is some coverage for the change. Nevertheless the Javascript testing looks a bit slim and could be improved. Also the DataSorting class could have some other member functions like the model validation could be extracted out so that it is easily tested. @Hackers: This was how we tried to test this feature: 1 - Started pgAdmin 2 - Opened the query tool for a specific server 3 - Executed a SQL statment 4 - Pressed the column header to try to order, nothing happened 5 - Right clicked the column header to see if it was there the option, nothing This is the behavior that we were expecting, not to have to open Data View and then press the icon that is not even near the grid in order to sort the column. Is this really the way we want people to use the grid in pgAdmin? Should it be more intuitive? PS: Also that Orange after the selection is like a push in the eyes and not in a good way. Maybe we should think about changing the color of the icon to blue to match the rest of the website or something. Thanks Victoria & Joao On Mon, Mar 26, 2018 at 12:13 PM Murtuza Zabuawala < [email protected]> wrote: > On Mon, Mar 26, 2018 at 5:52 PM, Dave Page <[email protected]> wrote: > >> Hi >> >> On Sun, Mar 25, 2018 at 7:13 PM, Murtuza Zabuawala < >> [email protected]> wrote: >> >>> Hi, >>> >>> PFA patch which allow user to sort the data in View data mode. >>> >> >> The patch looks good in general, however I'm not sure about the UI, in >> particular that the closely-linked dialogue for filtering is a completely >> different design. I think it would be better to combine the Sort/Filter >> options and use a single dialogue for both, as pgAdmin 3 did (though, maybe >> not using separate tabs for each part, but the top and bottom of the same >> dialogue. >> >> That would certainly fix the consistency of the dialogues (obviously, as >> there would only be one!), and I think would perhaps be a more simple >> overall UI, particularly for those that want to sort and filter. >> > Sure, I'll send updated it accordingly. > > >> >> Thoughts? >> >> Thanks. >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-26 20:26 Robert Eckhardt <[email protected]> parent: Joao De Almeida Pereira <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Robert Eckhardt @ 2018-03-26 20:26 UTC (permalink / raw) To: Joao De Almeida Pereira <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Dave Page <[email protected]>; pgadmin-hackers On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < [email protected]> wrote: > Hi Hackers, > > @Murtuza: The patch codewise looks good. Nice to see that we are using > axios instead of jquery ajax calls and that there is some coverage for the > change. > Nevertheless the Javascript testing looks a bit slim and could be > improved. Also the DataSorting class could have some other member functions > like the model validation could be extracted out so that it is easily > tested. > > > @Hackers: This was how we tried to test this feature: > 1 - Started pgAdmin > 2 - Opened the query tool for a specific server > 3 - Executed a SQL statment > 4 - Pressed the column header to try to order, nothing happened > 5 - Right clicked the column header to see if it was there the option, > nothing > > This is the behavior that we were expecting, not to have to open Data View > and then press the icon that is not even near the grid in order to sort the > column. Is this really the way we want people to use the grid in pgAdmin? > Should it be more intuitive? > Have we considered making the grid behave more like excel or other grids? I think that having the ascending and descending inside the column header, we could similarly provide filtering. Something that would give users a more intuitive place to look. -- Rob > > > PS: Also that Orange after the selection is like a push in the eyes and > not in a good way. Maybe we should think about changing the color of the > icon to blue to match the rest of the website or something. > > Thanks > Victoria & Joao > > On Mon, Mar 26, 2018 at 12:13 PM Murtuza Zabuawala <murtuza.zabuawala@ > enterprisedb.com> wrote: > >> On Mon, Mar 26, 2018 at 5:52 PM, Dave Page <[email protected]> wrote: >> >>> Hi >>> >>> On Sun, Mar 25, 2018 at 7:13 PM, Murtuza Zabuawala <murtuza.zabuawala@ >>> enterprisedb.com> wrote: >>> >>>> Hi, >>>> >>>> PFA patch which allow user to sort the data in View data mode. >>>> >>> >>> The patch looks good in general, however I'm not sure about the UI, in >>> particular that the closely-linked dialogue for filtering is a completely >>> different design. I think it would be better to combine the Sort/Filter >>> options and use a single dialogue for both, as pgAdmin 3 did (though, maybe >>> not using separate tabs for each part, but the top and bottom of the same >>> dialogue. >>> >>> That would certainly fix the consistency of the dialogues (obviously, as >>> there would only be one!), and I think would perhaps be a more simple >>> overall UI, particularly for those that want to sort and filter. >>> >> Sure, I'll send updated it accordingly. >> >> >>> >>> Thoughts? >>> >>> Thanks. >>> >>> -- >>> Dave Page >>> Blog: http://pgsnake.blogspot.com >>> Twitter: @pgsnake >>> >>> EnterpriseDB UK: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >> ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-27 09:43 Dave Page <[email protected]> parent: Robert Eckhardt <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Dave Page @ 2018-03-27 09:43 UTC (permalink / raw) To: Robert Eckhardt <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt <[email protected]> wrote: > > > On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < > [email protected]> wrote: > >> Hi Hackers, >> >> @Murtuza: The patch codewise looks good. Nice to see that we are using >> axios instead of jquery ajax calls and that there is some coverage for the >> change. >> Nevertheless the Javascript testing looks a bit slim and could be >> improved. Also the DataSorting class could have some other member functions >> like the model validation could be extracted out so that it is easily >> tested. >> >> >> @Hackers: This was how we tried to test this feature: >> 1 - Started pgAdmin >> 2 - Opened the query tool for a specific server >> 3 - Executed a SQL statment >> 4 - Pressed the column header to try to order, nothing happened >> 5 - Right clicked the column header to see if it was there the option, >> nothing >> >> This is the behavior that we were expecting, not to have to open Data >> View and then press the icon that is not even near the grid in order to >> sort the column. Is this really the way we want people to use the grid in >> pgAdmin? Should it be more intuitive? >> > > Have we considered making the grid behave more like excel or other grids? > I think that having the ascending and descending inside the column header, > we could similarly provide filtering. Something that would give users a > more intuitive place to look. > Doing the sorting via header clicks is convenient but very restrictive. How do you specify multiple columns to sort by for example? The current design allows you to select columns and the sort order as you see fit. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-27 10:25 Murtuza Zabuawala <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Murtuza Zabuawala @ 2018-03-27 10:25 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Robert Eckhardt <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: > > > On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt <[email protected]> > wrote: > >> >> >> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >> [email protected]> wrote: >> >>> Hi Hackers, >>> >>> @Murtuza: The patch codewise looks good. Nice to see that we are using >>> axios instead of jquery ajax calls and that there is some coverage for the >>> change. >>> Nevertheless the Javascript testing looks a bit slim and could be >>> improved. Also the DataSorting class could have some other member functions >>> like the model validation could be extracted out so that it is easily >>> tested. >>> >>> >>> @Hackers: This was how we tried to test this feature: >>> 1 - Started pgAdmin >>> 2 - Opened the query tool for a specific server >>> 3 - Executed a SQL statment >>> 4 - Pressed the column header to try to order, nothing happened >>> 5 - Right clicked the column header to see if it was there the option, >>> nothing >>> >>> This is the behavior that we were expecting, not to have to open Data >>> View and then press the icon that is not even near the grid in order to >>> sort the column. Is this really the way we want people to use the grid in >>> pgAdmin? Should it be more intuitive? >>> >> >> Have we considered making the grid behave more like excel or other grids? >> I think that having the ascending and descending inside the column header, >> we could similarly provide filtering. Something that would give users a >> more intuitive place to look. >> > > Doing the sorting via header clicks is convenient but very restrictive. > How do you specify multiple columns to sort by for example? The current > design allows you to select columns and the sort order as you see fit. > Another reason we can't use that because w e have already occupied that behaviour for selecting entire column when user clicks on header. As Dave suggested, I will be merging it with filter dialog meaning it will be accessible via direct button on toolbar & keyboard shortcut. > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-27 13:36 Robert Eckhardt <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Robert Eckhardt @ 2018-03-27 13:36 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: Dave Page <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < [email protected]> wrote: > On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: > >> >> >> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt <[email protected]> >> wrote: >> >>> >>> >>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>> [email protected]> wrote: >>> >>>> Hi Hackers, >>>> >>>> @Murtuza: The patch codewise looks good. Nice to see that we are using >>>> axios instead of jquery ajax calls and that there is some coverage for the >>>> change. >>>> Nevertheless the Javascript testing looks a bit slim and could be >>>> improved. Also the DataSorting class could have some other member functions >>>> like the model validation could be extracted out so that it is easily >>>> tested. >>>> >>>> >>>> @Hackers: This was how we tried to test this feature: >>>> 1 - Started pgAdmin >>>> 2 - Opened the query tool for a specific server >>>> 3 - Executed a SQL statment >>>> 4 - Pressed the column header to try to order, nothing happened >>>> 5 - Right clicked the column header to see if it was there the option, >>>> nothing >>>> >>>> This is the behavior that we were expecting, not to have to open Data >>>> View and then press the icon that is not even near the grid in order to >>>> sort the column. Is this really the way we want people to use the grid in >>>> pgAdmin? Should it be more intuitive? >>>> >>> >>> Have we considered making the grid behave more like excel or other >>> grids? I think that having the ascending and descending inside the column >>> header, we could similarly provide filtering. Something that would give >>> users a more intuitive place to look. >>> >> >> Doing the sorting via header clicks is convenient but very restrictive. >> How do you specify multiple columns to sort by for example? The current >> design allows you to select columns and the sort order as you see fit. >> > Honestly I'm not sold on my idea, I was just proposing an alternative in an effort to start a discussion about the user experience. Ideally what I'd like to see, maybe this happened, is some user research. When we initial worked on refactoring the results grid we made a bunch of changes. One of the things we intended to do was to follow up to see how people were using the grid now so that we could better understand how it was now being used in order to design and implement features just like this. Clearly we haven't gotten there yet. > > Another reason we can't use that because w > e have already occupied that behaviour for selecting entire column > when user clicks on header. > As Dave suggested, I will be merging it with filter dialog meaning it will > be accessible via direct button on toolbar & keyboard shortcut. > > How are users currently interacting with that filter dialog? What I'm suggesting is that we understand how users want to interact with their results, be those the results of a query or a table view, then we can design something that meets those needs. I agree that changing the column selection behavior isn't desirable, however, I also feel like providing the best user experience is better than holding onto a particular feature implementation. -- Rob > > >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > > ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-27 13:54 Murtuza Zabuawala <[email protected]> parent: Robert Eckhardt <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Murtuza Zabuawala @ 2018-03-27 13:54 UTC (permalink / raw) To: Robert Eckhardt <[email protected]>; +Cc: Dave Page <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> wrote: > > > On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala <murtuza.zabuawala@ > enterprisedb.com> wrote: > >> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >> >>> >>> >>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt <[email protected]> >>> wrote: >>> >>>> >>>> >>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>> [email protected]> wrote: >>>> >>>>> Hi Hackers, >>>>> >>>>> @Murtuza: The patch codewise looks good. Nice to see that we are using >>>>> axios instead of jquery ajax calls and that there is some coverage for the >>>>> change. >>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>> improved. Also the DataSorting class could have some other member functions >>>>> like the model validation could be extracted out so that it is easily >>>>> tested. >>>>> >>>>> >>>>> @Hackers: This was how we tried to test this feature: >>>>> 1 - Started pgAdmin >>>>> 2 - Opened the query tool for a specific server >>>>> 3 - Executed a SQL statment >>>>> 4 - Pressed the column header to try to order, nothing happened >>>>> 5 - Right clicked the column header to see if it was there the option, >>>>> nothing >>>>> >>>>> This is the behavior that we were expecting, not to have to open Data >>>>> View and then press the icon that is not even near the grid in order to >>>>> sort the column. Is this really the way we want people to use the grid in >>>>> pgAdmin? Should it be more intuitive? >>>>> >>>> >>>> Have we considered making the grid behave more like excel or other >>>> grids? I think that having the ascending and descending inside the column >>>> header, we could similarly provide filtering. Something that would give >>>> users a more intuitive place to look. >>>> >>> >>> Doing the sorting via header clicks is convenient but very restrictive. >>> How do you specify multiple columns to sort by for example? The current >>> design allows you to select columns and the sort order as you see fit. >>> >> > Honestly I'm not sold on my idea, I was just proposing an alternative in > an effort to start a discussion about the user experience. Ideally what I'd > like to see, maybe this happened, is some user research. When we initial > worked on refactoring the results grid we made a bunch of changes. One of > the things we intended to do was to follow up to see how people were using > the grid now so that we could better understand how it was now being used > in order to design and implement features just like this. Clearly we > haven't gotten there yet. > > >> >> Another reason we can't use that because w >> e have already occupied that behaviour for selecting entire column >> when user clicks on header. >> As Dave suggested, I will be merging it with filter dialog meaning it >> will be accessible via direct button on toolbar & keyboard shortcut. >> >> > > How are users currently interacting with that filter dialog? > By clicking on the toolbar button as well as keyboard shortcut. > > What I'm suggesting is that we understand how users want to interact with > their results, be those the results of a query or a table view, then we can > design something that meets those needs. I agree that changing the column > selection behavior isn't desirable, however, I also feel like providing the > best user experience is better than holding onto a particular feature > implementation. > > > > -- Rob > > >> >> >>> -- >>> Dave Page >>> Blog: http://pgsnake.blogspot.com >>> Twitter: @pgsnake >>> >>> EnterpriseDB UK: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >> >> > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-28 00:37 Robert Eckhardt <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 2 replies; 25+ messages in thread From: Robert Eckhardt @ 2018-03-28 00:37 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: Dave Page <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala <murtuza.zabuawala@ enterprisedb.com> wrote: > > > On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> > wrote: > >> >> >> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >> [email protected]> wrote: >> >>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >>> >>>> >>>> >>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt <[email protected]> >>>> wrote: >>>> >>>>> >>>>> >>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>> [email protected]> wrote: >>>>> >>>>>> Hi Hackers, >>>>>> >>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>> for the change. >>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>> improved. Also the DataSorting class could have some other member functions >>>>>> like the model validation could be extracted out so that it is easily >>>>>> tested. >>>>>> >>>>>> >>>>>> @Hackers: This was how we tried to test this feature: >>>>>> 1 - Started pgAdmin >>>>>> 2 - Opened the query tool for a specific server >>>>>> 3 - Executed a SQL statment >>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>> 5 - Right clicked the column header to see if it was there the >>>>>> option, nothing >>>>>> >>>>>> This is the behavior that we were expecting, not to have to open Data >>>>>> View and then press the icon that is not even near the grid in order to >>>>>> sort the column. Is this really the way we want people to use the grid in >>>>>> pgAdmin? Should it be more intuitive? >>>>>> >>>>> >>>>> Have we considered making the grid behave more like excel or other >>>>> grids? I think that having the ascending and descending inside the column >>>>> header, we could similarly provide filtering. Something that would give >>>>> users a more intuitive place to look. >>>>> >>>> >>>> Doing the sorting via header clicks is convenient but very restrictive. >>>> How do you specify multiple columns to sort by for example? The current >>>> design allows you to select columns and the sort order as you see fit. >>>> >>> >> Honestly I'm not sold on my idea, I was just proposing an alternative in >> an effort to start a discussion about the user experience. Ideally what I'd >> like to see, maybe this happened, is some user research. When we initial >> worked on refactoring the results grid we made a bunch of changes. One of >> the things we intended to do was to follow up to see how people were using >> the grid now so that we could better understand how it was now being used >> in order to design and implement features just like this. Clearly we >> haven't gotten there yet. >> >> >>> >>> Another reason we can't use that because w >>> e have already occupied that behaviour for selecting entire column >>> when user clicks on header. >>> As Dave suggested, I will be merging it with filter dialog meaning it >>> will be accessible via direct button on toolbar & keyboard shortcut. >>> >>> >> >> How are users currently interacting with that filter dialog? >> > > By clicking on the toolbar button as well as keyboard shortcut. > > > > > Sorry I wasn't clear. My question was more along the lines of, do we know if people are using the filter functionality? What kind of filters are people using? What do they like about it? What do they wish they could do above and beyond sorting, etc. -- Rob > >> What I'm suggesting is that we understand how users want to interact with >> their results, be those the results of a query or a table view, then we can >> design something that meets those needs. I agree that changing the column >> selection behavior isn't desirable, however, I also feel like providing the >> best user experience is better than holding onto a particular feature >> implementation. >> >> >> > >> -- Rob >> >> >>> >>> >>>> -- >>>> Dave Page >>>> Blog: http://pgsnake.blogspot.com >>>> Twitter: @pgsnake >>>> >>>> EnterpriseDB UK: http://www.enterprisedb.com >>>> The Enterprise PostgreSQL Company >>>> >>> >>> >> > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-28 07:19 Murtuza Zabuawala <[email protected]> parent: Robert Eckhardt <[email protected]> 1 sibling, 1 reply; 25+ messages in thread From: Murtuza Zabuawala @ 2018-03-28 07:19 UTC (permalink / raw) To: Robert Eckhardt <[email protected]>; +Cc: Dave Page <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers Hi Dave, Please find updated patch with following changes, - Combined Filter and Data sorting together same as pgAdmin3. - Extracted model into separate file - Change the colour of filter button from orange to blue. - Updated docs and screenshot. @Joao, Could you please provide any reference for learning more about jasmine test framework? -- Regards, Murtuza Zabuawala EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt <[email protected]> wrote: > > > On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < > [email protected]> wrote: > >> >> >> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> >> wrote: >> >>> >>> >>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>> [email protected]> wrote: >>> >>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >>>> >>>>> >>>>> >>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt <[email protected] >>>>> > wrote: >>>>> >>>>>> >>>>>> >>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> Hi Hackers, >>>>>>> >>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>> for the change. >>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>> like the model validation could be extracted out so that it is easily >>>>>>> tested. >>>>>>> >>>>>>> >>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>> 1 - Started pgAdmin >>>>>>> 2 - Opened the query tool for a specific server >>>>>>> 3 - Executed a SQL statment >>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>> option, nothing >>>>>>> >>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>> >>>>>> >>>>>> Have we considered making the grid behave more like excel or other >>>>>> grids? I think that having the ascending and descending inside the column >>>>>> header, we could similarly provide filtering. Something that would give >>>>>> users a more intuitive place to look. >>>>>> >>>>> >>>>> Doing the sorting via header clicks is convenient but very >>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>> The current design allows you to select columns and the sort order as you >>>>> see fit. >>>>> >>>> >>> Honestly I'm not sold on my idea, I was just proposing an alternative in >>> an effort to start a discussion about the user experience. Ideally what I'd >>> like to see, maybe this happened, is some user research. When we initial >>> worked on refactoring the results grid we made a bunch of changes. One of >>> the things we intended to do was to follow up to see how people were using >>> the grid now so that we could better understand how it was now being used >>> in order to design and implement features just like this. Clearly we >>> haven't gotten there yet. >>> >>> >>>> >>>> Another reason we can't use that because w >>>> e have already occupied that behaviour for selecting entire column >>>> when user clicks on header. >>>> As Dave suggested, I will be merging it with filter dialog meaning it >>>> will be accessible via direct button on toolbar & keyboard shortcut. >>>> >>>> >>> >>> How are users currently interacting with that filter dialog? >>> >> >> By clicking on the toolbar button as well as keyboard shortcut. >> >> >> >> >> > > Sorry I wasn't clear. My question was more along the lines of, do we know > if people are using the filter functionality? What kind of filters are > people using? What do they like about it? What do they wish they could do > above and beyond sorting, etc. > I have not done any data gathering from users so I can't comment on your queries. but a s far as I understood from the feature requests that most of the users expect to have functionality which will allow then to sort columns as it was in pgAdmin3. > -- Rob > > >> >>> What I'm suggesting is that we understand how users want to interact >>> with their results, be those the results of a query or a table view, then >>> we can design something that meets those needs. I agree that changing the >>> column selection behavior isn't desirable, however, I also feel like >>> providing the best user experience is better than holding onto a particular >>> feature implementation. >>> >>> >>> >> >>> -- Rob >>> >>> >>>> >>>> >>>>> -- >>>>> Dave Page >>>>> Blog: http://pgsnake.blogspot.com >>>>> Twitter: @pgsnake >>>>> >>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>> The Enterprise PostgreSQL Company >>>>> >>>> >>>> >>> >> > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image [application/octet-stream] RM_3154_v1.diff (132.6K, 4-RM_3154_v1.diff) download | inline diff: diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst index 1cb23cf..b1a9551 100644 --- a/docs/en_US/editgrid.rst +++ b/docs/en_US/editgrid.rst @@ -95,6 +95,24 @@ 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 Filter button, This dialog provides information about the sql filter and data sorting in the edit grid window: +.. image:: images/editgrid_filter_dialog.png + :alt: Edit grid filter dialog window + +* Use *SQL filter* to provide where clause conditions +* 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 discard a data sorting, and delete the 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 0000000000000000000000000000000000000000..046d9a124363a621e618efc524d745a8a526ebed GIT binary patch literal 68460 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 BJDmUk literal 0 HcmV?d00001 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 0000000..0ba9e35 --- /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 = $('<div class=\'data_sorting_dialog\'></div>'); + 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 = $('<div class=\'pg-prop-status-bar pg-el-xs-12 hide\'>' + + ' <div class=\'media error-in-footer bg-red-1 border-red-2 font-red-3 text-14\'>' + + ' <div class=\'media-body media-middle\'>' + + ' <div class=\'alert-icon error-icon\'>' + + ' <i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>' + + ' </div>' + + ' <div class=\'alert-text\'>' + + ' </div>' + + ' </div>' + + ' </div>' + + '</div>', { + text: '', + }).appendTo($container); + + // To show progress on filter Saving/Updating on AJAX + this.showFilterProgress = $( + '<div id="show_filter_progress" class="wcLoadingIconContainer busy-fetching hidden">' + + '<div class="wcLoadingBackground"></div>' + + '<span class="wcLoadingIcon fa fa-spinner fa-pulse"></span>' + + '<span class="busy-text wcLoadingLabel">' + gettext('Loading data...') + '</span>' + + '</div>').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: '<div></div>', + 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 0000000..c3146a4 --- /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 2f3bb05..29e6b81 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -195,9 +195,9 @@ <ul class="dropdown-menu dropdown-menu-right"> <li> <a id="btn-filter-menu" href="#" tabindex="0">{{ _('Filter') }}</a> - <a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a> <a id="btn-include-filter" href="#" tabindex="0">{{ _('By Selection') }}</a> <a id="btn-exclude-filter" href="#" tabindex="0">{{ _('Exclude Selection') }}</a> + <a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a> </li> </ul> </div> @@ -341,23 +341,6 @@ <div class="editor-title" style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% else %}{{ bgcolor or '#2C76B4' }}{% endif %}; color: {{ fgcolor or 'white' }};"></div> </div> - - <div id="filter" class="filter-container hidden"> - <div class="filter-title">Filter</div> - <div class="sql-textarea"> - <textarea id="sql_filter" rows="5"></textarea> - </div> - <div class="btn-group"> - <button id="btn-cancel" type="button" class="btn btn-danger" title="{{ _('Cancel') }}" tabindex="0"> - <i class="fa fa-times" aria-hidden="true"></i> {{ _('Cancel') }} - </button> - </div> - <div class="btn-group"> - <button id="btn-apply" type="button" class="btn btn-primary" title="{{ _('Apply') }}" tabindex="0"> - <i class="fa fa-check" aria-hidden="true"></i> {{ _('Apply') }} - </button> - </div> - </div> <div id="editor-panel" tabindex="0"></div> <iframe id="download-csv" style="display:none"></iframe> </div> diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 6f5d5b7..84f000e 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): @@ -782,81 +783,6 @@ def save(trans_id): } ) - [email protected]( - '/filter/get/<int:trans_id>', - 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}) - - [email protected]( - '/filter/apply/<int:trans_id>', - 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/<int:trans_id>', methods=["PUT", "POST"], endpoint='inclusive_filter' @@ -1561,3 +1487,37 @@ def query_tool_status(trans_id): return internal_server_error( errormsg=gettext("Transaction status check failed.") ) + + [email protected]( + '/filter_dialog/<int:trans_id>', + 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)) + + [email protected]( + '/filter_dialog/<int:trans_id>', + 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 8cc96e0..993b0d9 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 46588dc..c54590d 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 923ccea..f358abc 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/is_new_transaction_required', + 'sources/sqleditor/filter_dialog', 'sources/history/index.js', 'sources/../jsx/history/query_history', 'react', 'react-dom', @@ -30,7 +31,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, transaction, + XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, transaction, FilterHandler, HistoryBundle, queryHistory, React, ReactDOM, keyboardShortcuts, queryToolActions, Datagrid) { /* Return back, this has been called more than once */ @@ -108,8 +109,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(); @@ -117,31 +117,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() { @@ -2160,11 +2135,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'); } @@ -3170,79 +3145,10 @@ define('tools.querytool', [ }; }, - // This function will show the filter in the text area. + // This function will used when user wants custom data sorting _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'); - - if (pgAdmin.Browser.UserManagement.is_pga_login_required(e)) { - self.save_state('_show_filter', []); - return pgAdmin.Browser.UserManagement.pga_login(); - } - - if(transaction.is_new_transaction_required(e)) { - self.save_state('_show_filter', []); - return self.init_transaction(); - } - - 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) { - if(e.responseJSON.errormsg != undefined) { - msg = e.responseJSON.errormsg; - } - if(e.status == 503 && e.responseJSON.info != undefined && - e.responseJSON.info == 'CONNECTION_LOST') { - setTimeout(function() { - self.save_state('_show_filter', []); - self.handle_connection_lost(false, e); - }); - } - } - } - 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 0000000..610747d --- /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 1cb60d9..add1658 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 0000000..838a35b --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/filter_dialog.py @@ -0,0 +1,96 @@ +########################################################################## +# +# 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_babel 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 0000000..9747978 --- /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 0000000..e13fa09 --- /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({}); + }); + + }); + }); +}); ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-28 08:12 Dave Page <[email protected]> parent: Robert Eckhardt <[email protected]> 1 sibling, 1 reply; 25+ messages in thread From: Dave Page @ 2018-03-28 08:12 UTC (permalink / raw) To: Robert Eckhardt <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Wed, Mar 28, 2018 at 1:37 AM, Robert Eckhardt <[email protected]> wrote: > > > On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < > [email protected]> wrote: > >> >> >> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> >> wrote: >> >>> >>> >>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>> [email protected]> wrote: >>> >>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >>>> >>>>> >>>>> >>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt <[email protected] >>>>> > wrote: >>>>> >>>>>> >>>>>> >>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> Hi Hackers, >>>>>>> >>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>> for the change. >>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>> like the model validation could be extracted out so that it is easily >>>>>>> tested. >>>>>>> >>>>>>> >>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>> 1 - Started pgAdmin >>>>>>> 2 - Opened the query tool for a specific server >>>>>>> 3 - Executed a SQL statment >>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>> option, nothing >>>>>>> >>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>> >>>>>> >>>>>> Have we considered making the grid behave more like excel or other >>>>>> grids? I think that having the ascending and descending inside the column >>>>>> header, we could similarly provide filtering. Something that would give >>>>>> users a more intuitive place to look. >>>>>> >>>>> >>>>> Doing the sorting via header clicks is convenient but very >>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>> The current design allows you to select columns and the sort order as you >>>>> see fit. >>>>> >>>> >>> Honestly I'm not sold on my idea, I was just proposing an alternative in >>> an effort to start a discussion about the user experience. Ideally what I'd >>> like to see, maybe this happened, is some user research. When we initial >>> worked on refactoring the results grid we made a bunch of changes. One of >>> the things we intended to do was to follow up to see how people were using >>> the grid now so that we could better understand how it was now being used >>> in order to design and implement features just like this. Clearly we >>> haven't gotten there yet. >>> >>> >>>> >>>> Another reason we can't use that because w >>>> e have already occupied that behaviour for selecting entire column >>>> when user clicks on header. >>>> As Dave suggested, I will be merging it with filter dialog meaning it >>>> will be accessible via direct button on toolbar & keyboard shortcut. >>>> >>>> >>> >>> How are users currently interacting with that filter dialog? >>> >> >> By clicking on the toolbar button as well as keyboard shortcut. >> >> >> >> >> > > Sorry I wasn't clear. My question was more along the lines of, do we know > if people are using the filter functionality? What kind of filters are > people using? What do they like about it? What do they wish they could do > above and beyond sorting, etc. > Yes, they are, based on the fact we've had issues reported in the past. We have no idea how they are using it. Sorting is a separate feature that is often requested. The only reason it's connected here is that both functionalities in pgAdmin 3 were managed through the same dialogue which based on lack of complaints from users, generally worked for them. I do know that the proposed "click on headers" approach will not work for me, as I have multi-part keys in databases which I like to sort by in specific ways. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-28 13:54 Robert Eckhardt <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Robert Eckhardt @ 2018-03-28 13:54 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Wed, Mar 28, 2018 at 4:12 AM, Dave Page <[email protected]> wrote: > > > On Wed, Mar 28, 2018 at 1:37 AM, Robert Eckhardt <[email protected]> > wrote: > >> >> >> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >> [email protected]> wrote: >> >>> >>> >>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> >>> wrote: >>> >>>> >>>> >>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>> [email protected]> wrote: >>>> >>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >>>>> >>>>>> >>>>>> >>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> Hi Hackers, >>>>>>>> >>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>> for the change. >>>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>>> like the model validation could be extracted out so that it is easily >>>>>>>> tested. >>>>>>>> >>>>>>>> >>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>> 1 - Started pgAdmin >>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>> 3 - Executed a SQL statment >>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>> option, nothing >>>>>>>> >>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>> >>>>>>> >>>>>>> Have we considered making the grid behave more like excel or other >>>>>>> grids? I think that having the ascending and descending inside the column >>>>>>> header, we could similarly provide filtering. Something that would give >>>>>>> users a more intuitive place to look. >>>>>>> >>>>>> >>>>>> Doing the sorting via header clicks is convenient but very >>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>> The current design allows you to select columns and the sort order as you >>>>>> see fit. >>>>>> >>>>> >>>> Honestly I'm not sold on my idea, I was just proposing an alternative >>>> in an effort to start a discussion about the user experience. Ideally what >>>> I'd like to see, maybe this happened, is some user research. When we >>>> initial worked on refactoring the results grid we made a bunch of changes. >>>> One of the things we intended to do was to follow up to see how people were >>>> using the grid now so that we could better understand how it was now being >>>> used in order to design and implement features just like this. Clearly we >>>> haven't gotten there yet. >>>> >>>> >>>>> >>>>> Another reason we can't use that because w >>>>> e have already occupied that behaviour for selecting entire column >>>>> when user clicks on header. >>>>> As Dave suggested, I will be merging it with filter dialog meaning it >>>>> will be accessible via direct button on toolbar & keyboard shortcut. >>>>> >>>>> >>>> >>>> How are users currently interacting with that filter dialog? >>>> >>> >>> By clicking on the toolbar button as well as keyboard shortcut. >>> >>> >>> >>> >>> >> >> Sorry I wasn't clear. My question was more along the lines of, do we >> know if people are using the filter functionality? What kind of filters >> are people using? What do they like about it? What do they wish they could >> do above and beyond sorting, etc. >> > > Yes, they are, based on the fact we've had issues reported in the past. We > have no idea how they are using it. > > Sorting is a separate feature that is often requested. The only reason > it's connected here is that both functionalities in pgAdmin 3 were managed > through the same dialogue which based on lack of complaints from users, > generally worked for them. I do know that the proposed "click on headers" > approach will not work for me, as I have multi-part keys in databases which > I like to sort by in specific ways. > > As far as the 'click on header' is concerned I think Murtuza's objection is very valid. I think my overall point is that I believe that sorting and filtering is a legitimate pain but it is a pain I don't fully understand. The solution as presented certainly works it just doesn't feel nice. My concern is that it won't be clear to users what they should do or expect since the UX is unique to pgAdmin. Less of a concern is that all of these changes are only happening with the 'view data' section of the code and I would assume that people who have queried the DB will also have the need to sort and filter without necessarily rewriting their SQL. -- Rob > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-28 15:20 Dave Page <[email protected]> parent: Robert Eckhardt <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Dave Page @ 2018-03-28 15:20 UTC (permalink / raw) To: Robert Eckhardt <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Wed, Mar 28, 2018 at 2:54 PM, Robert Eckhardt <[email protected]> wrote: > > > On Wed, Mar 28, 2018 at 4:12 AM, Dave Page <[email protected]> wrote: > >> >> >> On Wed, Mar 28, 2018 at 1:37 AM, Robert Eckhardt <[email protected]> >> wrote: >> >>> >>> >>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>> [email protected]> wrote: >>> >>>> >>>> >>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> >>>> wrote: >>>> >>>>> >>>>> >>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>> [email protected]> wrote: >>>>> >>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> Hi Hackers, >>>>>>>>> >>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>>> for the change. >>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>>>> like the model validation could be extracted out so that it is easily >>>>>>>>> tested. >>>>>>>>> >>>>>>>>> >>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>> 1 - Started pgAdmin >>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>> 3 - Executed a SQL statment >>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>> option, nothing >>>>>>>>> >>>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>>> >>>>>>>> >>>>>>>> Have we considered making the grid behave more like excel or other >>>>>>>> grids? I think that having the ascending and descending inside the column >>>>>>>> header, we could similarly provide filtering. Something that would give >>>>>>>> users a more intuitive place to look. >>>>>>>> >>>>>>> >>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>> The current design allows you to select columns and the sort order as you >>>>>>> see fit. >>>>>>> >>>>>> >>>>> Honestly I'm not sold on my idea, I was just proposing an alternative >>>>> in an effort to start a discussion about the user experience. Ideally what >>>>> I'd like to see, maybe this happened, is some user research. When we >>>>> initial worked on refactoring the results grid we made a bunch of changes. >>>>> One of the things we intended to do was to follow up to see how people were >>>>> using the grid now so that we could better understand how it was now being >>>>> used in order to design and implement features just like this. Clearly we >>>>> haven't gotten there yet. >>>>> >>>>> >>>>>> >>>>>> Another reason we can't use that because w >>>>>> e have already occupied that behaviour for selecting entire column >>>>>> when user clicks on header. >>>>>> As Dave suggested, I will be merging it with filter dialog meaning it >>>>>> will be accessible via direct button on toolbar & keyboard shortcut. >>>>>> >>>>>> >>>>> >>>>> How are users currently interacting with that filter dialog? >>>>> >>>> >>>> By clicking on the toolbar button as well as keyboard shortcut. >>>> >>>> >>>> >>>> >>>> >>> >>> Sorry I wasn't clear. My question was more along the lines of, do we >>> know if people are using the filter functionality? What kind of >>> filters are people using? What do they like about it? What do they wish >>> they could do above and beyond sorting, etc. >>> >> >> Yes, they are, based on the fact we've had issues reported in the past. >> We have no idea how they are using it. >> >> Sorting is a separate feature that is often requested. The only reason >> it's connected here is that both functionalities in pgAdmin 3 were managed >> through the same dialogue which based on lack of complaints from users, >> generally worked for them. I do know that the proposed "click on headers" >> approach will not work for me, as I have multi-part keys in databases which >> I like to sort by in specific ways. >> >> > As far as the 'click on header' is concerned I think Murtuza's objection > is very valid. I think my overall point is that I believe that sorting and > filtering is a legitimate pain but it is a pain I don't fully understand. > The solution as presented certainly works it just doesn't feel nice. My > concern is that it won't be clear to users what they should do or expect > since the UX is unique to pgAdmin. > Right now my concern is getting us back to feature parity in this area with pgAdmin 3, as users are complaining. Longer term we can look at further improvements or redesigns as resources allow. > Less of a concern is that all of these changes are only happening with the > 'view data' section of the code and I would assume that people who have > queried the DB will also have the need to sort and filter without > necessarily rewriting their SQL. > That doesn't concern me - there are already very distinct differences in the two modes for the tool. If/when we have a suitable parser on the front end we'll be able to dynamically switch between read-write modes and update sorting criteria in hand-crafted SQL. Until that large amount of work is done, we have two distinct modes, one with generated SQL and writeable data, the other with hand-crafted SQL and read-only data. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-28 15:28 Robert Eckhardt <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Robert Eckhardt @ 2018-03-28 15:28 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Wed, Mar 28, 2018 at 11:20 AM, Dave Page <[email protected]> wrote: > > > On Wed, Mar 28, 2018 at 2:54 PM, Robert Eckhardt <[email protected]> > wrote: > >> >> >> On Wed, Mar 28, 2018 at 4:12 AM, Dave Page <[email protected]> wrote: >> >>> >>> >>> On Wed, Mar 28, 2018 at 1:37 AM, Robert Eckhardt <[email protected]> >>> wrote: >>> >>>> >>>> >>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>> [email protected]> wrote: >>>> >>>>> >>>>> >>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected] >>>>> > wrote: >>>>> >>>>>> >>>>>> >>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> Hi Hackers, >>>>>>>>>> >>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>>>> for the change. >>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>>>>> like the model validation could be extracted out so that it is easily >>>>>>>>>> tested. >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>>> option, nothing >>>>>>>>>> >>>>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>>>> >>>>>>>>> >>>>>>>>> Have we considered making the grid behave more like excel or other >>>>>>>>> grids? I think that having the ascending and descending inside the column >>>>>>>>> header, we could similarly provide filtering. Something that would give >>>>>>>>> users a more intuitive place to look. >>>>>>>>> >>>>>>>> >>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>> see fit. >>>>>>>> >>>>>>> >>>>>> Honestly I'm not sold on my idea, I was just proposing an alternative >>>>>> in an effort to start a discussion about the user experience. Ideally what >>>>>> I'd like to see, maybe this happened, is some user research. When we >>>>>> initial worked on refactoring the results grid we made a bunch of changes. >>>>>> One of the things we intended to do was to follow up to see how people were >>>>>> using the grid now so that we could better understand how it was now being >>>>>> used in order to design and implement features just like this. Clearly we >>>>>> haven't gotten there yet. >>>>>> >>>>>> >>>>>>> >>>>>>> Another reason we can't use that because w >>>>>>> e have already occupied that behaviour for selecting entire column >>>>>>> when user clicks on header. >>>>>>> As Dave suggested, I will be merging it with filter dialog meaning >>>>>>> it will be accessible via direct button on toolbar & keyboard shortcut. >>>>>>> >>>>>>> >>>>>> >>>>>> How are users currently interacting with that filter dialog? >>>>>> >>>>> >>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>> >>>>> >>>>> >>>>> >>>>> >>>> >>>> Sorry I wasn't clear. My question was more along the lines of, do we >>>> know if people are using the filter functionality? What kind of >>>> filters are people using? What do they like about it? What do they wish >>>> they could do above and beyond sorting, etc. >>>> >>> >>> Yes, they are, based on the fact we've had issues reported in the past. >>> We have no idea how they are using it. >>> >>> Sorting is a separate feature that is often requested. The only reason >>> it's connected here is that both functionalities in pgAdmin 3 were managed >>> through the same dialogue which based on lack of complaints from users, >>> generally worked for them. I do know that the proposed "click on headers" >>> approach will not work for me, as I have multi-part keys in databases which >>> I like to sort by in specific ways. >>> >>> >> As far as the 'click on header' is concerned I think Murtuza's objection >> is very valid. I think my overall point is that I believe that sorting and >> filtering is a legitimate pain but it is a pain I don't fully understand. >> The solution as presented certainly works it just doesn't feel nice. My >> concern is that it won't be clear to users what they should do or expect >> since the UX is unique to pgAdmin. >> > > Right now my concern is getting us back to feature parity in this area > with pgAdmin 3, as users are complaining. Longer term we can look at > further improvements or redesigns as resources allow. > Totally fair. > > > >> Less of a concern is that all of these changes are only happening with >> the 'view data' section of the code and I would assume that people who have >> queried the DB will also have the need to sort and filter without >> necessarily rewriting their SQL. >> > > That doesn't concern me - there are already very distinct differences in > the two modes for the tool. If/when we have a suitable parser on the front > end we'll be able to dynamically switch between read-write modes and update > sorting criteria in hand-crafted SQL. Until that large amount of work is > done, we have two distinct modes, one with generated SQL and writeable > data, the other with hand-crafted SQL and read-only data. > Also totally fair. As an aside, we are about to spin up some user interviews for the large number of objects but during those interviews we were planning to add some follow up questions on the work we did on the data grid. If pushing this as is until it can be prioritized is something that is needed to get the heat off of you I'm ok with that. I do think we will spend a little time with users so I can better understand the issues being faced and we will absolutely share that feedback here. -- Rob > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-03-28 15:30 Dave Page <[email protected]> parent: Robert Eckhardt <[email protected]> 0 siblings, 0 replies; 25+ messages in thread From: Dave Page @ 2018-03-28 15:30 UTC (permalink / raw) To: Robert Eckhardt <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers On Wed, Mar 28, 2018 at 4:28 PM, Robert Eckhardt <[email protected]> wrote: > > > On Wed, Mar 28, 2018 at 11:20 AM, Dave Page <[email protected]> wrote: > >> >> >> On Wed, Mar 28, 2018 at 2:54 PM, Robert Eckhardt <[email protected]> >> wrote: >> >>> >>> >>> On Wed, Mar 28, 2018 at 4:12 AM, Dave Page <[email protected]> wrote: >>> >>>> >>>> >>>> On Wed, Mar 28, 2018 at 1:37 AM, Robert Eckhardt <[email protected]> >>>> wrote: >>>> >>>>> >>>>> >>>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>>> [email protected]> wrote: >>>>> >>>>>> >>>>>> >>>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>>> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>>> [email protected]> wrote: >>>>>>>>>> >>>>>>>>>>> Hi Hackers, >>>>>>>>>>> >>>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>>>>> for the change. >>>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could >>>>>>>>>>> be improved. Also the DataSorting class could have some other member >>>>>>>>>>> functions like the model validation could be extracted out so that it is >>>>>>>>>>> easily tested. >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>>>> option, nothing >>>>>>>>>>> >>>>>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>>>>> >>>>>>>>>> >>>>>>>>>> Have we considered making the grid behave more like excel or >>>>>>>>>> other grids? I think that having the ascending and descending inside the >>>>>>>>>> column header, we could similarly provide filtering. Something that would >>>>>>>>>> give users a more intuitive place to look. >>>>>>>>>> >>>>>>>>> >>>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>>> see fit. >>>>>>>>> >>>>>>>> >>>>>>> Honestly I'm not sold on my idea, I was just proposing an >>>>>>> alternative in an effort to start a discussion about the user experience. >>>>>>> Ideally what I'd like to see, maybe this happened, is some user research. >>>>>>> When we initial worked on refactoring the results grid we made a bunch of >>>>>>> changes. One of the things we intended to do was to follow up to see how >>>>>>> people were using the grid now so that we could better understand how it >>>>>>> was now being used in order to design and implement features just like >>>>>>> this. Clearly we haven't gotten there yet. >>>>>>> >>>>>>> >>>>>>>> >>>>>>>> Another reason we can't use that because w >>>>>>>> e have already occupied that behaviour for selecting entire column >>>>>>>> when user clicks on header. >>>>>>>> As Dave suggested, I will be merging it with filter dialog meaning >>>>>>>> it will be accessible via direct button on toolbar & keyboard shortcut. >>>>>>>> >>>>>>>> >>>>>>> >>>>>>> How are users currently interacting with that filter dialog? >>>>>>> >>>>>> >>>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>>> >>>>>> >>>>>> >>>>>> >>>>>> >>>>> >>>>> Sorry I wasn't clear. My question was more along the lines of, do we >>>>> know if people are using the filter functionality? What kind of >>>>> filters are people using? What do they like about it? What do they wish >>>>> they could do above and beyond sorting, etc. >>>>> >>>> >>>> Yes, they are, based on the fact we've had issues reported in the past. >>>> We have no idea how they are using it. >>>> >>>> Sorting is a separate feature that is often requested. The only reason >>>> it's connected here is that both functionalities in pgAdmin 3 were managed >>>> through the same dialogue which based on lack of complaints from users, >>>> generally worked for them. I do know that the proposed "click on headers" >>>> approach will not work for me, as I have multi-part keys in databases which >>>> I like to sort by in specific ways. >>>> >>>> >>> As far as the 'click on header' is concerned I think Murtuza's objection >>> is very valid. I think my overall point is that I believe that sorting and >>> filtering is a legitimate pain but it is a pain I don't fully understand. >>> The solution as presented certainly works it just doesn't feel nice. My >>> concern is that it won't be clear to users what they should do or expect >>> since the UX is unique to pgAdmin. >>> >> >> Right now my concern is getting us back to feature parity in this area >> with pgAdmin 3, as users are complaining. Longer term we can look at >> further improvements or redesigns as resources allow. >> > > Totally fair. > > >> >> >> >>> Less of a concern is that all of these changes are only happening with >>> the 'view data' section of the code and I would assume that people who have >>> queried the DB will also have the need to sort and filter without >>> necessarily rewriting their SQL. >>> >> >> That doesn't concern me - there are already very distinct differences in >> the two modes for the tool. If/when we have a suitable parser on the front >> end we'll be able to dynamically switch between read-write modes and update >> sorting criteria in hand-crafted SQL. Until that large amount of work is >> done, we have two distinct modes, one with generated SQL and writeable >> data, the other with hand-crafted SQL and read-only data. >> > > Also totally fair. As an aside, we are about to spin up some user > interviews for the large number of objects but during those interviews we > were planning to add some follow up questions on the work we did on the > data grid. If pushing this as is until it can be prioritized is something > that is needed to get the heat off of you I'm ok with that. > > I do think we will spend a little time with users so I can better > understand the issues being faced and we will absolutely share that > feedback here. > That will certainly be helpful, thanks. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-05 10:45 Dave Page <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Dave Page @ 2018-04-05 10:45 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: Robert Eckhardt <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers Can you rebase this please? On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala < [email protected]> wrote: > Hi Dave, > > Please find updated patch with following changes, > - Combined Filter and Data sorting together same as pgAdmin3. > - Extracted model into separate file > - Change the colour of filter button from orange to blue. > - Updated docs and screenshot. > > @Joao, > Could you please provide any reference for learning more about jasmine > test framework? > > > -- > Regards, > Murtuza Zabuawala > EnterpriseDB: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > > > On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt <[email protected]> > wrote: > >> >> >> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >> [email protected]> wrote: >> >>> >>> >>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> >>> wrote: >>> >>>> >>>> >>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>> [email protected]> wrote: >>>> >>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >>>>> >>>>>> >>>>>> >>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> Hi Hackers, >>>>>>>> >>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>> for the change. >>>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>>> like the model validation could be extracted out so that it is easily >>>>>>>> tested. >>>>>>>> >>>>>>>> >>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>> 1 - Started pgAdmin >>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>> 3 - Executed a SQL statment >>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>> option, nothing >>>>>>>> >>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>> >>>>>>> >>>>>>> Have we considered making the grid behave more like excel or other >>>>>>> grids? I think that having the ascending and descending inside the column >>>>>>> header, we could similarly provide filtering. Something that would give >>>>>>> users a more intuitive place to look. >>>>>>> >>>>>> >>>>>> Doing the sorting via header clicks is convenient but very >>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>> The current design allows you to select columns and the sort order as you >>>>>> see fit. >>>>>> >>>>> >>>> Honestly I'm not sold on my idea, I was just proposing an alternative >>>> in an effort to start a discussion about the user experience. Ideally what >>>> I'd like to see, maybe this happened, is some user research. When we >>>> initial worked on refactoring the results grid we made a bunch of changes. >>>> One of the things we intended to do was to follow up to see how people were >>>> using the grid now so that we could better understand how it was now being >>>> used in order to design and implement features just like this. Clearly we >>>> haven't gotten there yet. >>>> >>>> >>>>> >>>>> Another reason we can't use that because w >>>>> e have already occupied that behaviour for selecting entire column >>>>> when user clicks on header. >>>>> As Dave suggested, I will be merging it with filter dialog meaning it >>>>> will be accessible via direct button on toolbar & keyboard shortcut. >>>>> >>>>> >>>> >>>> How are users currently interacting with that filter dialog? >>>> >>> >>> By clicking on the toolbar button as well as keyboard shortcut. >>> >>> >>> >>> >>> >> >> Sorry I wasn't clear. My question was more along the lines of, do we >> know if people are using the filter functionality? What kind of filters >> are people using? What do they like about it? What do they wish they could >> do above and beyond sorting, etc. >> > I have not done any data gathering from users so I can't comment on your > queries. > but a > s far as I understood from the feature requests that most of the users > expect to have functionality which will allow then to sort columns as it > was in pgAdmin3. > > > >> -- Rob >> >> >>> >>>> What I'm suggesting is that we understand how users want to interact >>>> with their results, be those the results of a query or a table view, then >>>> we can design something that meets those needs. I agree that changing the >>>> column selection behavior isn't desirable, however, I also feel like >>>> providing the best user experience is better than holding onto a particular >>>> feature implementation. >>>> >>>> >>>> >>> >>>> -- Rob >>>> >>>> >>>>> >>>>> >>>>>> -- >>>>>> Dave Page >>>>>> Blog: http://pgsnake.blogspot.com >>>>>> Twitter: @pgsnake >>>>>> >>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>> The Enterprise PostgreSQL Company >>>>>> >>>>> >>>>> >>>> >>> >> > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-05 11:29 Murtuza Zabuawala <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 2 replies; 25+ messages in thread From: Murtuza Zabuawala @ 2018-04-05 11:29 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Robert Eckhardt <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers Hi Dave, Please find rebased patch. -- Regards, Murtuza Zabuawala EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: > Can you rebase this please? > > On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala <murtuza.zabuawala@ > enterprisedb.com> wrote: > >> Hi Dave, >> >> Please find updated patch with following changes, >> - Combined Filter and Data sorting together same as pgAdmin3. >> - Extracted model into separate file >> - Change the colour of filter button from orange to blue. >> - Updated docs and screenshot. >> >> @Joao, >> Could you please provide any reference for learning more about jasmine >> test framework? >> >> >> -- >> Regards, >> Murtuza Zabuawala >> EnterpriseDB: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> >> >> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt <[email protected]> >> wrote: >> >>> >>> >>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>> [email protected]> wrote: >>> >>>> >>>> >>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected]> >>>> wrote: >>>> >>>>> >>>>> >>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>> [email protected]> wrote: >>>>> >>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> Hi Hackers, >>>>>>>>> >>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>>> for the change. >>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>>>> like the model validation could be extracted out so that it is easily >>>>>>>>> tested. >>>>>>>>> >>>>>>>>> >>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>> 1 - Started pgAdmin >>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>> 3 - Executed a SQL statment >>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>> option, nothing >>>>>>>>> >>>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>>> >>>>>>>> >>>>>>>> Have we considered making the grid behave more like excel or other >>>>>>>> grids? I think that having the ascending and descending inside the column >>>>>>>> header, we could similarly provide filtering. Something that would give >>>>>>>> users a more intuitive place to look. >>>>>>>> >>>>>>> >>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>> The current design allows you to select columns and the sort order as you >>>>>>> see fit. >>>>>>> >>>>>> >>>>> Honestly I'm not sold on my idea, I was just proposing an alternative >>>>> in an effort to start a discussion about the user experience. Ideally what >>>>> I'd like to see, maybe this happened, is some user research. When we >>>>> initial worked on refactoring the results grid we made a bunch of changes. >>>>> One of the things we intended to do was to follow up to see how people were >>>>> using the grid now so that we could better understand how it was now being >>>>> used in order to design and implement features just like this. Clearly we >>>>> haven't gotten there yet. >>>>> >>>>> >>>>>> >>>>>> Another reason we can't use that because w >>>>>> e have already occupied that behaviour for selecting entire column >>>>>> when user clicks on header. >>>>>> As Dave suggested, I will be merging it with filter dialog meaning it >>>>>> will be accessible via direct button on toolbar & keyboard shortcut. >>>>>> >>>>>> >>>>> >>>>> How are users currently interacting with that filter dialog? >>>>> >>>> >>>> By clicking on the toolbar button as well as keyboard shortcut. >>>> >>>> >>>> >>>> >>>> >>> >>> Sorry I wasn't clear. My question was more along the lines of, do we >>> know if people are using the filter functionality? What kind of >>> filters are people using? What do they like about it? What do they wish >>> they could do above and beyond sorting, etc. >>> >> I have not done any data gathering from users so I can't comment on your >> queries. >> but a >> s far as I understood from the feature requests that most of the users >> expect to have functionality which will allow then to sort columns as it >> was in pgAdmin3. >> >> >> >>> -- Rob >>> >>> >>>> >>>>> What I'm suggesting is that we understand how users want to interact >>>>> with their results, be those the results of a query or a table view, then >>>>> we can design something that meets those needs. I agree that changing the >>>>> column selection behavior isn't desirable, however, I also feel like >>>>> providing the best user experience is better than holding onto a particular >>>>> feature implementation. >>>>> >>>>> >>>>> >>>> >>>>> -- Rob >>>>> >>>>> >>>>>> >>>>>> >>>>>>> -- >>>>>>> Dave Page >>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>> Twitter: @pgsnake >>>>>>> >>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>> The Enterprise PostgreSQL Company >>>>>>> >>>>>> >>>>>> >>>>> >>>> >>> >> > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image [application/octet-stream] RM_3055_v2.diff (50.0K, 4-RM_3055_v2.diff) download | inline diff: diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst index 1cb23cf..b1a9551 100644 --- a/docs/en_US/editgrid.rst +++ b/docs/en_US/editgrid.rst @@ -95,6 +95,24 @@ 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 Filter button, This dialog provides information about the sql filter and data sorting in the edit grid window: +.. image:: images/editgrid_filter_dialog.png + :alt: Edit grid filter dialog window + +* Use *SQL filter* to provide where clause conditions +* 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 discard a data sorting, and delete the 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 0000000..046d9a1 Binary files /dev/null and b/docs/en_US/images/editgrid_filter_dialog.png differ 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 0000000..0ba9e35 --- /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 = $('<div class=\'data_sorting_dialog\'></div>'); + 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 = $('<div class=\'pg-prop-status-bar pg-el-xs-12 hide\'>' + + ' <div class=\'media error-in-footer bg-red-1 border-red-2 font-red-3 text-14\'>' + + ' <div class=\'media-body media-middle\'>' + + ' <div class=\'alert-icon error-icon\'>' + + ' <i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>' + + ' </div>' + + ' <div class=\'alert-text\'>' + + ' </div>' + + ' </div>' + + ' </div>' + + '</div>', { + text: '', + }).appendTo($container); + + // To show progress on filter Saving/Updating on AJAX + this.showFilterProgress = $( + '<div id="show_filter_progress" class="wcLoadingIconContainer busy-fetching hidden">' + + '<div class="wcLoadingBackground"></div>' + + '<span class="wcLoadingIcon fa fa-spinner fa-pulse"></span>' + + '<span class="busy-text wcLoadingLabel">' + gettext('Loading data...') + '</span>' + + '</div>').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: '<div></div>', + 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 0000000..c3146a4 --- /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 2f3bb05..29e6b81 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -195,9 +195,9 @@ <ul class="dropdown-menu dropdown-menu-right"> <li> <a id="btn-filter-menu" href="#" tabindex="0">{{ _('Filter') }}</a> - <a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a> <a id="btn-include-filter" href="#" tabindex="0">{{ _('By Selection') }}</a> <a id="btn-exclude-filter" href="#" tabindex="0">{{ _('Exclude Selection') }}</a> + <a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a> </li> </ul> </div> @@ -341,23 +341,6 @@ <div class="editor-title" style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% else %}{{ bgcolor or '#2C76B4' }}{% endif %}; color: {{ fgcolor or 'white' }};"></div> </div> - - <div id="filter" class="filter-container hidden"> - <div class="filter-title">Filter</div> - <div class="sql-textarea"> - <textarea id="sql_filter" rows="5"></textarea> - </div> - <div class="btn-group"> - <button id="btn-cancel" type="button" class="btn btn-danger" title="{{ _('Cancel') }}" tabindex="0"> - <i class="fa fa-times" aria-hidden="true"></i> {{ _('Cancel') }} - </button> - </div> - <div class="btn-group"> - <button id="btn-apply" type="button" class="btn btn-primary" title="{{ _('Apply') }}" tabindex="0"> - <i class="fa fa-check" aria-hidden="true"></i> {{ _('Apply') }} - </button> - </div> - </div> <div id="editor-panel" tabindex="0"></div> <iframe id="download-csv" style="display:none"></iframe> </div> diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index f8d7d97..622b7c3 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): @@ -782,81 +783,6 @@ def save(trans_id): } ) - [email protected]( - '/filter/get/<int:trans_id>', - 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}) - - [email protected]( - '/filter/apply/<int:trans_id>', - 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/<int:trans_id>', methods=["PUT", "POST"], endpoint='inclusive_filter' @@ -1561,3 +1487,37 @@ def query_tool_status(trans_id): return internal_server_error( errormsg=gettext("Transaction status check failed.") ) + + [email protected]( + '/filter_dialog/<int:trans_id>', + 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)) + + [email protected]( + '/filter_dialog/<int:trans_id>', + 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 7ec03c5..fbe37df 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 46588dc..c54590d 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 60dacbb..f8cb05a 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 0000000..610747d --- /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 1cb60d9..add1658 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 0000000..368dbb4 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/filter_dialog.py @@ -0,0 +1,96 @@ +########################################################################## +# +# 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 0000000..9747978 --- /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 0000000..e13fa09 --- /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({}); + }); + + }); + }); +}); ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-05 13:25 Joao De Almeida Pereira <[email protected]> parent: Murtuza Zabuawala <[email protected]> 1 sibling, 0 replies; 25+ messages in thread From: Joao De Almeida Pereira @ 2018-04-05 13:25 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: Dave Page <[email protected]>; Robert Eckhardt <[email protected]>; pgadmin-hackers Murtuza I tried to apply the patch to master: git apply ~/Downloads/RM_3055_v2.diff error: cannot apply binary patch to 'docs/en_US/images/editgrid_filter_dialog.png' without full index line error: docs/en_US/images/editgrid_filter_dialog.png: patch does not apply On Thu, Apr 5, 2018 at 7:30 AM Murtuza Zabuawala < [email protected]> wrote: > Hi Dave, > > Please find rebased patch. > > -- > Regards, > Murtuza Zabuawala > EnterpriseDB: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > > > On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: > >> Can you rebase this please? >> >> On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala < >> [email protected]> wrote: >> >>> Hi Dave, >>> >>> Please find updated patch with following changes, >>> - Combined Filter and Data sorting together same as pgAdmin3. >>> - Extracted model into separate file >>> - Change the colour of filter button from orange to blue. >>> - Updated docs and screenshot. >>> >>> @Joao, >>> Could you please provide any reference for learning more about jasmine >>> test framework? >>> >>> >>> -- >>> Regards, >>> Murtuza Zabuawala >>> EnterpriseDB: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >>> >>> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt <[email protected]> >>> wrote: >>> >>>> >>>> >>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>> [email protected]> wrote: >>>> >>>>> >>>>> >>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected] >>>>> > wrote: >>>>> >>>>>> >>>>>> >>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> Hi Hackers, >>>>>>>>>> >>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>>>> for the change. >>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>>>>> like the model validation could be extracted out so that it is easily >>>>>>>>>> tested. >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>>> option, nothing >>>>>>>>>> >>>>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>>>> >>>>>>>>> >>>>>>>>> Have we considered making the grid behave more like excel or other >>>>>>>>> grids? I think that having the ascending and descending inside the column >>>>>>>>> header, we could similarly provide filtering. Something that would give >>>>>>>>> users a more intuitive place to look. >>>>>>>>> >>>>>>>> >>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>> see fit. >>>>>>>> >>>>>>> >>>>>> Honestly I'm not sold on my idea, I was just proposing an alternative >>>>>> in an effort to start a discussion about the user experience. Ideally what >>>>>> I'd like to see, maybe this happened, is some user research. When we >>>>>> initial worked on refactoring the results grid we made a bunch of changes. >>>>>> One of the things we intended to do was to follow up to see how people were >>>>>> using the grid now so that we could better understand how it was now being >>>>>> used in order to design and implement features just like this. Clearly we >>>>>> haven't gotten there yet. >>>>>> >>>>>> >>>>>>> >>>>>>> Another reason we can't use that because w >>>>>>> e have already occupied that behaviour for selecting entire column >>>>>>> when user clicks on header. >>>>>>> As Dave suggested, I will be merging it with filter dialog meaning >>>>>>> it will be accessible via direct button on toolbar & keyboard shortcut. >>>>>>> >>>>>>> >>>>>> >>>>>> How are users currently interacting with that filter dialog? >>>>>> >>>>> >>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>> >>>>> >>>>> >>>>> >>>>> >>>> >>>> Sorry I wasn't clear. My question was more along the lines of, do we >>>> know if people are using the filter functionality? What kind of >>>> filters are people using? What do they like about it? What do they wish >>>> they could do above and beyond sorting, etc. >>>> >>> I have not done any data gathering from users so I can't comment on >>> your queries. >>> but a >>> s far as I understood from the feature requests that most of the users >>> expect to have functionality which will allow then to sort columns as it >>> was in pgAdmin3. >>> >>> >>> >>>> -- Rob >>>> >>>> >>>>> >>>>>> What I'm suggesting is that we understand how users want to interact >>>>>> with their results, be those the results of a query or a table view, then >>>>>> we can design something that meets those needs. I agree that changing the >>>>>> column selection behavior isn't desirable, however, I also feel like >>>>>> providing the best user experience is better than holding onto a particular >>>>>> feature implementation. >>>>>> >>>>>> >>>>>> >>>>> >>>>>> -- Rob >>>>>> >>>>>> >>>>>>> >>>>>>> >>>>>>>> -- >>>>>>>> Dave Page >>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>> Twitter: @pgsnake >>>>>>>> >>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>> The Enterprise PostgreSQL Company >>>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>> >>>> >>> >> >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-05 15:25 Dave Page <[email protected]> parent: Murtuza Zabuawala <[email protected]> 1 sibling, 1 reply; 25+ messages in thread From: Dave Page @ 2018-04-05 15:25 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: Robert Eckhardt <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers Thanks, applied. On Thu, Apr 5, 2018 at 12:29 PM, Murtuza Zabuawala < [email protected]> wrote: > Hi Dave, > > Please find rebased patch. > > -- > Regards, > Murtuza Zabuawala > EnterpriseDB: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > > > On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: > >> Can you rebase this please? >> >> On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala < >> [email protected]> wrote: >> >>> Hi Dave, >>> >>> Please find updated patch with following changes, >>> - Combined Filter and Data sorting together same as pgAdmin3. >>> - Extracted model into separate file >>> - Change the colour of filter button from orange to blue. >>> - Updated docs and screenshot. >>> >>> @Joao, >>> Could you please provide any reference for learning more about jasmine >>> test framework? >>> >>> >>> -- >>> Regards, >>> Murtuza Zabuawala >>> EnterpriseDB: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >>> >>> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt <[email protected]> >>> wrote: >>> >>>> >>>> >>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>> [email protected]> wrote: >>>> >>>>> >>>>> >>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt <[email protected] >>>>> > wrote: >>>>> >>>>>> >>>>>> >>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> Hi Hackers, >>>>>>>>>> >>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>>>> for the change. >>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could be >>>>>>>>>> improved. Also the DataSorting class could have some other member functions >>>>>>>>>> like the model validation could be extracted out so that it is easily >>>>>>>>>> tested. >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>>> option, nothing >>>>>>>>>> >>>>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>>>> >>>>>>>>> >>>>>>>>> Have we considered making the grid behave more like excel or other >>>>>>>>> grids? I think that having the ascending and descending inside the column >>>>>>>>> header, we could similarly provide filtering. Something that would give >>>>>>>>> users a more intuitive place to look. >>>>>>>>> >>>>>>>> >>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>> see fit. >>>>>>>> >>>>>>> >>>>>> Honestly I'm not sold on my idea, I was just proposing an alternative >>>>>> in an effort to start a discussion about the user experience. Ideally what >>>>>> I'd like to see, maybe this happened, is some user research. When we >>>>>> initial worked on refactoring the results grid we made a bunch of changes. >>>>>> One of the things we intended to do was to follow up to see how people were >>>>>> using the grid now so that we could better understand how it was now being >>>>>> used in order to design and implement features just like this. Clearly we >>>>>> haven't gotten there yet. >>>>>> >>>>>> >>>>>>> >>>>>>> Another reason we can't use that because w >>>>>>> e have already occupied that behaviour for selecting entire column >>>>>>> when user clicks on header. >>>>>>> As Dave suggested, I will be merging it with filter dialog meaning >>>>>>> it will be accessible via direct button on toolbar & keyboard shortcut. >>>>>>> >>>>>>> >>>>>> >>>>>> How are users currently interacting with that filter dialog? >>>>>> >>>>> >>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>> >>>>> >>>>> >>>>> >>>>> >>>> >>>> Sorry I wasn't clear. My question was more along the lines of, do we >>>> know if people are using the filter functionality? What kind of >>>> filters are people using? What do they like about it? What do they wish >>>> they could do above and beyond sorting, etc. >>>> >>> I have not done any data gathering from users so I can't comment on >>> your queries. >>> but a >>> s far as I understood from the feature requests that most of the users >>> expect to have functionality which will allow then to sort columns as it >>> was in pgAdmin3. >>> >>> >>> >>>> -- Rob >>>> >>>> >>>>> >>>>>> What I'm suggesting is that we understand how users want to interact >>>>>> with their results, be those the results of a query or a table view, then >>>>>> we can design something that meets those needs. I agree that changing the >>>>>> column selection behavior isn't desirable, however, I also feel like >>>>>> providing the best user experience is better than holding onto a particular >>>>>> feature implementation. >>>>>> >>>>>> >>>>>> >>>>> >>>>>> -- Rob >>>>>> >>>>>> >>>>>>> >>>>>>> >>>>>>>> -- >>>>>>>> Dave Page >>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>> Twitter: @pgsnake >>>>>>>> >>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>> The Enterprise PostgreSQL Company >>>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>> >>>> >>> >> >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-05 17:50 Joao De Almeida Pereira <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Joao De Almeida Pereira @ 2018-04-05 17:50 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Robert Eckhardt <[email protected]>; pgadmin-hackers Hi Murtuza, I forgot to mention this in my review........ Do you think we can start using only ES6 instead of keep using requirejs syntax on new things that we are building? How much effort do you think it will be? Like this patch, as an example, do you think it could have been implemented without using requirejs? Thanks On Thu, Apr 5, 2018 at 11:26 AM Dave Page <[email protected]> wrote: > Thanks, applied. > > On Thu, Apr 5, 2018 at 12:29 PM, Murtuza Zabuawala < > [email protected]> wrote: > >> Hi Dave, >> >> Please find rebased patch. >> >> -- >> Regards, >> Murtuza Zabuawala >> EnterpriseDB: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> >> >> On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: >> >>> Can you rebase this please? >>> >>> On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala < >>> [email protected]> wrote: >>> >>>> Hi Dave, >>>> >>>> Please find updated patch with following changes, >>>> - Combined Filter and Data sorting together same as pgAdmin3. >>>> - Extracted model into separate file >>>> - Change the colour of filter button from orange to blue. >>>> - Updated docs and screenshot. >>>> >>>> @Joao, >>>> Could you please provide any reference for learning more about jasmine >>>> test framework? >>>> >>>> >>>> -- >>>> Regards, >>>> Murtuza Zabuawala >>>> EnterpriseDB: http://www.enterprisedb.com >>>> The Enterprise PostgreSQL Company >>>> >>>> >>>> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt <[email protected]> >>>> wrote: >>>> >>>>> >>>>> >>>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>>> [email protected]> wrote: >>>>> >>>>>> >>>>>> >>>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>>> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>>> [email protected]> wrote: >>>>>>>>>> >>>>>>>>>>> Hi Hackers, >>>>>>>>>>> >>>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we are >>>>>>>>>>> using axios instead of jquery ajax calls and that there is some coverage >>>>>>>>>>> for the change. >>>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could >>>>>>>>>>> be improved. Also the DataSorting class could have some other member >>>>>>>>>>> functions like the model validation could be extracted out so that it is >>>>>>>>>>> easily tested. >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>>>> option, nothing >>>>>>>>>>> >>>>>>>>>>> This is the behavior that we were expecting, not to have to open >>>>>>>>>>> Data View and then press the icon that is not even near the grid in order >>>>>>>>>>> to sort the column. Is this really the way we want people to use the grid >>>>>>>>>>> in pgAdmin? Should it be more intuitive? >>>>>>>>>>> >>>>>>>>>> >>>>>>>>>> Have we considered making the grid behave more like excel or >>>>>>>>>> other grids? I think that having the ascending and descending inside the >>>>>>>>>> column header, we could similarly provide filtering. Something that would >>>>>>>>>> give users a more intuitive place to look. >>>>>>>>>> >>>>>>>>> >>>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>>> see fit. >>>>>>>>> >>>>>>>> >>>>>>> Honestly I'm not sold on my idea, I was just proposing an >>>>>>> alternative in an effort to start a discussion about the user experience. >>>>>>> Ideally what I'd like to see, maybe this happened, is some user research. >>>>>>> When we initial worked on refactoring the results grid we made a bunch of >>>>>>> changes. One of the things we intended to do was to follow up to see how >>>>>>> people were using the grid now so that we could better understand how it >>>>>>> was now being used in order to design and implement features just like >>>>>>> this. Clearly we haven't gotten there yet. >>>>>>> >>>>>>> >>>>>>>> >>>>>>>> Another reason we can't use that because w >>>>>>>> e have already occupied that behaviour for selecting entire column >>>>>>>> when user clicks on header. >>>>>>>> As Dave suggested, I will be merging it with filter dialog meaning >>>>>>>> it will be accessible via direct button on toolbar & keyboard shortcut. >>>>>>>> >>>>>>>> >>>>>>> >>>>>>> How are users currently interacting with that filter dialog? >>>>>>> >>>>>> >>>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>>> >>>>>> >>>>>> >>>>>> >>>>>> >>>>> >>>>> Sorry I wasn't clear. My question was more along the lines of, do we >>>>> know if people are using the filter functionality? What kind of >>>>> filters are people using? What do they like about it? What do they wish >>>>> they could do above and beyond sorting, etc. >>>>> >>>> I have not done any data gathering from users so I can't comment on >>>> your queries. >>>> but a >>>> s far as I understood from the feature requests that most of the users >>>> expect to have functionality which will allow then to sort columns as it >>>> was in pgAdmin3. >>>> >>>> >>>> >>>>> -- Rob >>>>> >>>>> >>>>>> >>>>>>> What I'm suggesting is that we understand how users want to interact >>>>>>> with their results, be those the results of a query or a table view, then >>>>>>> we can design something that meets those needs. I agree that changing the >>>>>>> column selection behavior isn't desirable, however, I also feel like >>>>>>> providing the best user experience is better than holding onto a particular >>>>>>> feature implementation. >>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>>>> -- Rob >>>>>>> >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>>> -- >>>>>>>>> Dave Page >>>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>>> Twitter: @pgsnake >>>>>>>>> >>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>>> The Enterprise PostgreSQL Company >>>>>>>>> >>>>>>>> >>>>>>>> >>>>>>> >>>>>> >>>>> >>>> >>> >>> >>> -- >>> Dave Page >>> Blog: http://pgsnake.blogspot.com >>> Twitter: @pgsnake >>> >>> EnterpriseDB UK: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >> >> > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-06 03:43 Murtuza Zabuawala <[email protected]> parent: Joao De Almeida Pereira <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Murtuza Zabuawala @ 2018-04-06 03:43 UTC (permalink / raw) To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Robert Eckhardt <[email protected]>; pgadmin-hackers Hi Joao, On Thu, Apr 5, 2018 at 11:20 PM, Joao De Almeida Pereira < [email protected]> wrote: > Hi Murtuza, > I forgot to mention this in my review........ > Do you think we can start using only ES6 instead of keep using requirejs > syntax on new things that we are building? > How much effort do you think it will be? > Yes, we can. Let me try to change the code to use ES6 and I'll send patch. > > Like this patch, as an example, do you think it could have been > implemented without using requirejs? > > Thanks > > On Thu, Apr 5, 2018 at 11:26 AM Dave Page <[email protected]> wrote: > >> Thanks, applied. >> >> On Thu, Apr 5, 2018 at 12:29 PM, Murtuza Zabuawala <murtuza.zabuawala@ >> enterprisedb.com> wrote: >> >>> Hi Dave, >>> >>> Please find rebased patch. >>> >>> -- >>> Regards, >>> Murtuza Zabuawala >>> EnterpriseDB: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >>> >>> On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: >>> >>>> Can you rebase this please? >>>> >>>> On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala <murtuza.zabuawala@ >>>> enterprisedb.com> wrote: >>>> >>>>> Hi Dave, >>>>> >>>>> Please find updated patch with following changes, >>>>> - Combined Filter and Data sorting together same as pgAdmin3. >>>>> - Extracted model into separate file >>>>> - Change the colour of filter button from orange to blue. >>>>> - Updated docs and screenshot. >>>>> >>>>> @Joao, >>>>> Could you please provide any reference for learning more about jasmine >>>>> test framework? >>>>> >>>>> >>>>> -- >>>>> Regards, >>>>> Murtuza Zabuawala >>>>> EnterpriseDB: http://www.enterprisedb.com >>>>> The Enterprise PostgreSQL Company >>>>> >>>>> >>>>> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt <[email protected] >>>>> > wrote: >>>>> >>>>>> >>>>>> >>>>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>>>> wrote: >>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>>>> [email protected]> wrote: >>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>> >>>>>>>>>>>> Hi Hackers, >>>>>>>>>>>> >>>>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we >>>>>>>>>>>> are using axios instead of jquery ajax calls and that there is some >>>>>>>>>>>> coverage for the change. >>>>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could >>>>>>>>>>>> be improved. Also the DataSorting class could have some other member >>>>>>>>>>>> functions like the model validation could be extracted out so that it is >>>>>>>>>>>> easily tested. >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>>>>> option, nothing >>>>>>>>>>>> >>>>>>>>>>>> This is the behavior that we were expecting, not to have to >>>>>>>>>>>> open Data View and then press the icon that is not even near the grid in >>>>>>>>>>>> order to sort the column. Is this really the way we want people to use the >>>>>>>>>>>> grid in pgAdmin? Should it be more intuitive? >>>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> Have we considered making the grid behave more like excel or >>>>>>>>>>> other grids? I think that having the ascending and descending inside the >>>>>>>>>>> column header, we could similarly provide filtering. Something that would >>>>>>>>>>> give users a more intuitive place to look. >>>>>>>>>>> >>>>>>>>>> >>>>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>>>> see fit. >>>>>>>>>> >>>>>>>>> >>>>>>>> Honestly I'm not sold on my idea, I was just proposing an >>>>>>>> alternative in an effort to start a discussion about the user experience. >>>>>>>> Ideally what I'd like to see, maybe this happened, is some user research. >>>>>>>> When we initial worked on refactoring the results grid we made a bunch of >>>>>>>> changes. One of the things we intended to do was to follow up to see how >>>>>>>> people were using the grid now so that we could better understand how it >>>>>>>> was now being used in order to design and implement features just like >>>>>>>> this. Clearly we haven't gotten there yet. >>>>>>>> >>>>>>>> >>>>>>>>> >>>>>>>>> Another reason we can't use that because w >>>>>>>>> e have already occupied that behaviour for selecting entire column >>>>>>>>> when user clicks on header. >>>>>>>>> As Dave suggested, I will be merging it with filter dialog meaning >>>>>>>>> it will be accessible via direct button on toolbar & keyboard shortcut. >>>>>>>>> >>>>>>>>> >>>>>>>> >>>>>>>> How are users currently interacting with that filter dialog? >>>>>>>> >>>>>>> >>>>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>>>> >>>>>>> >>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>>> Sorry I wasn't clear. My question was more along the lines of, do we >>>>>> know if people are using the filter functionality? What kind of >>>>>> filters are people using? What do they like about it? What do they wish >>>>>> they could do above and beyond sorting, etc. >>>>>> >>>>> I have not done any data gathering from users so I can't comment on >>>>> your queries. >>>>> but a >>>>> s far as I understood from the feature requests that most of the >>>>> users expect to have functionality which will allow then to sort columns as >>>>> it was in pgAdmin3. >>>>> >>>>> >>>>> >>>>>> -- Rob >>>>>> >>>>>> >>>>>>> >>>>>>>> What I'm suggesting is that we understand how users want to >>>>>>>> interact with their results, be those the results of a query or a table >>>>>>>> view, then we can design something that meets those needs. I agree that >>>>>>>> changing the column selection behavior isn't desirable, however, I also >>>>>>>> feel like providing the best user experience is better than holding onto a >>>>>>>> particular feature implementation. >>>>>>>> >>>>>>>> >>>>>>>> >>>>>>> >>>>>>>> -- Rob >>>>>>>> >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>>> -- >>>>>>>>>> Dave Page >>>>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>>>> Twitter: @pgsnake >>>>>>>>>> >>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>>>> The Enterprise PostgreSQL Company >>>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>> >>>>>>> >>>>>> >>>>> >>>> >>>> >>>> -- >>>> Dave Page >>>> Blog: http://pgsnake.blogspot.com >>>> Twitter: @pgsnake >>>> >>>> EnterpriseDB UK: http://www.enterprisedb.com >>>> The Enterprise PostgreSQL Company >>>> >>> >>> >> >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-06 05:44 Murtuza Zabuawala <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Murtuza Zabuawala @ 2018-04-06 05:44 UTC (permalink / raw) To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Robert Eckhardt <[email protected]>; pgadmin-hackers Hi, Please find the patch which will change the syntax from requirejs syntax to ES6 from previous commit. -- Regards, Murtuza Zabuawala EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company On Fri, Apr 6, 2018 at 9:13 AM, Murtuza Zabuawala < [email protected]> wrote: > Hi Joao, > > > On Thu, Apr 5, 2018 at 11:20 PM, Joao De Almeida Pereira < > [email protected]> wrote: > >> Hi Murtuza, >> I forgot to mention this in my review........ >> Do you think we can start using only ES6 instead of keep using requirejs >> syntax on new things that we are building? >> How much effort do you think it will be? >> > Yes, we can. > Let me try to change the code to use ES6 and I'll send patch. > > >> >> Like this patch, as an example, do you think it could have been >> implemented without using requirejs? >> > >> Thanks >> >> On Thu, Apr 5, 2018 at 11:26 AM Dave Page <[email protected]> wrote: >> >>> Thanks, applied. >>> >>> On Thu, Apr 5, 2018 at 12:29 PM, Murtuza Zabuawala < >>> [email protected]> wrote: >>> >>>> Hi Dave, >>>> >>>> Please find rebased patch. >>>> >>>> -- >>>> Regards, >>>> Murtuza Zabuawala >>>> EnterpriseDB: http://www.enterprisedb.com >>>> The Enterprise PostgreSQL Company >>>> >>>> >>>> On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: >>>> >>>>> Can you rebase this please? >>>>> >>>>> On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala < >>>>> [email protected]> wrote: >>>>> >>>>>> Hi Dave, >>>>>> >>>>>> Please find updated patch with following changes, >>>>>> - Combined Filter and Data sorting together same as pgAdmin3. >>>>>> - Extracted model into separate file >>>>>> - Change the colour of filter button from orange to blue. >>>>>> - Updated docs and screenshot. >>>>>> >>>>>> @Joao, >>>>>> Could you please provide any reference for learning more about >>>>>> jasmine test framework? >>>>>> >>>>>> >>>>>> -- >>>>>> Regards, >>>>>> Murtuza Zabuawala >>>>>> EnterpriseDB: http://www.enterprisedb.com >>>>>> The Enterprise PostgreSQL Company >>>>>> >>>>>> >>>>>> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>>>>> wrote: >>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>> >>>>>>>>>>>>> Hi Hackers, >>>>>>>>>>>>> >>>>>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we >>>>>>>>>>>>> are using axios instead of jquery ajax calls and that there is some >>>>>>>>>>>>> coverage for the change. >>>>>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and could >>>>>>>>>>>>> be improved. Also the DataSorting class could have some other member >>>>>>>>>>>>> functions like the model validation could be extracted out so that it is >>>>>>>>>>>>> easily tested. >>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>>>>> 4 - Pressed the column header to try to order, nothing happened >>>>>>>>>>>>> 5 - Right clicked the column header to see if it was there the >>>>>>>>>>>>> option, nothing >>>>>>>>>>>>> >>>>>>>>>>>>> This is the behavior that we were expecting, not to have to >>>>>>>>>>>>> open Data View and then press the icon that is not even near the grid in >>>>>>>>>>>>> order to sort the column. Is this really the way we want people to use the >>>>>>>>>>>>> grid in pgAdmin? Should it be more intuitive? >>>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> Have we considered making the grid behave more like excel or >>>>>>>>>>>> other grids? I think that having the ascending and descending inside the >>>>>>>>>>>> column header, we could similarly provide filtering. Something that would >>>>>>>>>>>> give users a more intuitive place to look. >>>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>>>>> see fit. >>>>>>>>>>> >>>>>>>>>> >>>>>>>>> Honestly I'm not sold on my idea, I was just proposing an >>>>>>>>> alternative in an effort to start a discussion about the user experience. >>>>>>>>> Ideally what I'd like to see, maybe this happened, is some user research. >>>>>>>>> When we initial worked on refactoring the results grid we made a bunch of >>>>>>>>> changes. One of the things we intended to do was to follow up to see how >>>>>>>>> people were using the grid now so that we could better understand how it >>>>>>>>> was now being used in order to design and implement features just like >>>>>>>>> this. Clearly we haven't gotten there yet. >>>>>>>>> >>>>>>>>> >>>>>>>>>> >>>>>>>>>> Another reason we can't use that because w >>>>>>>>>> e have already occupied that behaviour for selecting entire column >>>>>>>>>> when user clicks on header. >>>>>>>>>> As Dave suggested, I will be merging it with filter dialog >>>>>>>>>> meaning it will be accessible via direct button on toolbar & keyboard >>>>>>>>>> shortcut. >>>>>>>>>> >>>>>>>>>> >>>>>>>>> >>>>>>>>> How are users currently interacting with that filter dialog? >>>>>>>>> >>>>>>>> >>>>>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> >>>>>>> >>>>>>> Sorry I wasn't clear. My question was more along the lines of, do >>>>>>> we know if people are using the filter functionality? What kind of >>>>>>> filters are people using? What do they like about it? What do they wish >>>>>>> they could do above and beyond sorting, etc. >>>>>>> >>>>>> I have not done any data gathering from users so I can't comment on >>>>>> your queries. >>>>>> but a >>>>>> s far as I understood from the feature requests that most of the >>>>>> users expect to have functionality which will allow then to sort columns as >>>>>> it was in pgAdmin3. >>>>>> >>>>>> >>>>>> >>>>>>> -- Rob >>>>>>> >>>>>>> >>>>>>>> >>>>>>>>> What I'm suggesting is that we understand how users want to >>>>>>>>> interact with their results, be those the results of a query or a table >>>>>>>>> view, then we can design something that meets those needs. I agree that >>>>>>>>> changing the column selection behavior isn't desirable, however, I also >>>>>>>>> feel like providing the best user experience is better than holding onto a >>>>>>>>> particular feature implementation. >>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>> >>>>>>>>> -- Rob >>>>>>>>> >>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>>> -- >>>>>>>>>>> Dave Page >>>>>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>>>>> Twitter: @pgsnake >>>>>>>>>>> >>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>>>>> The Enterprise PostgreSQL Company >>>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>> >>>>>>>> >>>>>>> >>>>>> >>>>> >>>>> >>>>> -- >>>>> Dave Page >>>>> Blog: http://pgsnake.blogspot.com >>>>> Twitter: @pgsnake >>>>> >>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>> The Enterprise PostgreSQL Company >>>>> >>>> >>>> >>> >>> >>> -- >>> Dave Page >>> Blog: http://pgsnake.blogspot.com >>> Twitter: @pgsnake >>> >>> EnterpriseDB UK: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >> > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image [application/octet-stream] RM_1894_ES6_changes.diff (25.3K, 4-RM_1894_ES6_changes.diff) download | inline diff: diff --git a/web/pgadmin/static/js/sqleditor/filter_dialog.js b/web/pgadmin/static/js/sqleditor/filter_dialog.js index 0ba9e35..8a48c86 100644 --- a/web/pgadmin/static/js/sqleditor/filter_dialog.js +++ b/web/pgadmin/static/js/sqleditor/filter_dialog.js @@ -1,243 +1,241 @@ -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 = $('<div class=\'data_sorting_dialog\'></div>'); - 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, +import gettext from 'sources/gettext'; +import url_for from 'sources/url_for'; +import $ from 'jquery'; +import Alertify from 'pgadmin.alertifyjs'; +import pgAdmin from 'sources/pgadmin'; +import Backform from 'pgadmin.backform'; +import axios from 'axios'; +import queryToolActions from 'sources/sqleditor/query_tool_actions'; +import filterDialogModel from 'sources/sqleditor/filter_dialog_model'; + +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 = $('<div class=\'data_sorting_dialog\'></div>'); + 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', + }), }, - }; - }, - 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, - }); - } + }, { + 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 = $('<div class=\'pg-prop-status-bar pg-el-xs-12 hide\'>' + - ' <div class=\'media error-in-footer bg-red-1 border-red-2 font-red-3 text-14\'>' + - ' <div class=\'media-body media-middle\'>' + - ' <div class=\'alert-icon error-icon\'>' + - ' <i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>' + - ' </div>' + - ' <div class=\'alert-text\'>' + - ' </div>' + - ' </div>' + - ' </div>' + - '</div>', { - text: '', - }).appendTo($container); - - // To show progress on filter Saving/Updating on AJAX - this.showFilterProgress = $( - '<div id="show_filter_progress" class="wcLoadingIconContainer busy-fetching hidden">' + - '<div class="wcLoadingBackground"></div>' + - '<span class="wcLoadingIcon fa fa-spinner fa-pulse"></span>' + - '<span class="busy-text wcLoadingLabel">' + gettext('Loading data...') + '</span>' + - '</div>').appendTo($container); - - $( - self.showFilterProgress[0] - ).removeClass('hidden'); - - self.filterCollectionModel = filterDialogModel(response); - - let fields = Backform.generateViewSchema( - null, self.filterCollectionModel, 'create', null, null, true - ); + }, + prepare: function() { + let self = this; + $container.html(''); + // Disable Ok button + this.__internal.buttons[1].element.disabled = true; + + // Status bar + this.statusBar = $('<div class=\'pg-prop-status-bar pg-el-xs-12 hide\'>' + + ' <div class=\'media error-in-footer bg-red-1 border-red-2 font-red-3 text-14\'>' + + ' <div class=\'media-body media-middle\'>' + + ' <div class=\'alert-icon error-icon\'>' + + ' <i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>' + + ' </div>' + + ' <div class=\'alert-text\'>' + + ' </div>' + + ' </div>' + + ' </div>' + + '</div>', { + text: '', + }).appendTo($container); + + // To show progress on filter Saving/Updating on AJAX + this.showFilterProgress = $( + '<div id="show_filter_progress" class="wcLoadingIconContainer busy-fetching hidden">' + + '<div class="wcLoadingBackground"></div>' + + '<span class="wcLoadingIcon fa fa-spinner fa-pulse"></span>' + + '<span class="busy-text wcLoadingLabel">' + gettext('Loading data...') + '</span>' + + '</div>').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: '<div></div>', + 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; + }); - let view = this.view = new Backform.Dialog({ - el: '<div></div>', - model: self.filterCollectionModel, - schema: fields, + 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; }); + }); - $(this.elements.body.childNodes[0]).addClass( - 'alertify_tools_dialog_properties obj_properties' - ); + view.listenTo(view.model, 'pgadmin-session:stop', function() { + view.stopListening(view.model, 'pgadmin-session:invalid'); + view.stopListening(view.model, 'pgadmin-session:valid'); + }); - $container.append(view.render().$el); + // Starts monitoring changes to model + view.model.startNewSession(); - // 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; - }); + // Set data in collection + let viewDataSortingModel = view.model.get('data_sorting'); + viewDataSortingModel.add(response['data_sorting']); - 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; - }); - }); + // Hide Progress ... + $( + self.showFilterProgress[0] + ).addClass('hidden'); - view.listenTo(view.model, 'pgadmin-session:stop', function() { - view.stopListening(view.model, 'pgadmin-session:invalid'); - view.stopListening(view.model, 'pgadmin-session:valid'); - }); + }, + // Callback functions when click on the buttons of the Alertify dialogs + callback: function(e) { + let self = this; - // Starts monitoring changes to model - view.model.startNewSession(); + 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 - // Set data in collection - let viewDataSortingModel = view.model.get('data_sorting'); - viewDataSortingModel.add(response['data_sorting']); + let filterCollectionModel = this.filterCollectionModel.toJSON(); - // Hide Progress ... + // Show 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(); + ).removeClass('hidden'); - // Show Progress ... + 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] - ).removeClass('hidden'); + ).addClass('hidden'); + handler.onExecuteHTTPError(error); + + setTimeout( + function() { + Alertify.error(error); + }, 10 + ); + }); + } else { + self.close(); + } + }, + }; + }); - 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%'); + }); + }, +}; - Alertify.filterDialog(title).resizeTo('65%', '60%'); - }); - }, - }; - return FilterDialog; -}); +module.exports = FilterDialog; diff --git a/web/pgadmin/static/js/sqleditor/filter_dialog_model.js b/web/pgadmin/static/js/sqleditor/filter_dialog_model.js index c3146a4..e1b977e 100644 --- a/web/pgadmin/static/js/sqleditor/filter_dialog_model.js +++ b/web/pgadmin/static/js/sqleditor/filter_dialog_model.js @@ -1,133 +1,132 @@ -define([ - 'sources/gettext', 'underscore', 'sources/pgadmin', - 'pgadmin.backform', 'pgadmin.backgrid', -], function( - gettext, _, pgAdmin, Backform, Backgrid -) { +import gettext from 'sources/gettext'; +import _ from 'underscore'; +import pgAdmin from 'sources/pgadmin'; +import Backgrid from 'pgadmin.backgrid'; +import Backform from 'pgadmin.backform'; - let initModel = function(response) { +let initModel = function(response) { - let order_mapping = { - 'asc': gettext('ASC'), - 'desc': gettext('DESC'), - }; + let order_mapping = { + 'asc': gettext('ASC'), + 'desc': gettext('DESC'), + }; - let DataSortingModel = pgAdmin.Browser.DataModel.extend({ - idAttribute: 'name', - defaults: { - name: undefined, - order: 'asc', + 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, }, - 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, - }; - }); - }, + 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, - }; - }); - }, + }, + { + 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, }, - ], - 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; + 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 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; +}; - let model = new FilterCollectionModel(); - return model; - }; - return initModel; -}); +module.exports = initModel; ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-06 09:38 Dave Page <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 25+ messages in thread From: Dave Page @ 2018-04-06 09:38 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Robert Eckhardt <[email protected]>; pgadmin-hackers Thanks, applied. On Fri, Apr 6, 2018 at 6:44 AM, Murtuza Zabuawala < [email protected]> wrote: > Hi, > > Please find the patch which will change the syntax from requirejs syntax > to ES6 from previous commit. > > -- > Regards, > Murtuza Zabuawala > EnterpriseDB: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > > > On Fri, Apr 6, 2018 at 9:13 AM, Murtuza Zabuawala <murtuza.zabuawala@ > enterprisedb.com> wrote: > >> Hi Joao, >> >> >> On Thu, Apr 5, 2018 at 11:20 PM, Joao De Almeida Pereira < >> [email protected]> wrote: >> >>> Hi Murtuza, >>> I forgot to mention this in my review........ >>> Do you think we can start using only ES6 instead of keep using requirejs >>> syntax on new things that we are building? >>> How much effort do you think it will be? >>> >> Yes, we can. >> Let me try to change the code to use ES6 and I'll send patch. >> >> >>> >>> Like this patch, as an example, do you think it could have been >>> implemented without using requirejs? >>> >> >>> Thanks >>> >>> On Thu, Apr 5, 2018 at 11:26 AM Dave Page <[email protected]> wrote: >>> >>>> Thanks, applied. >>>> >>>> On Thu, Apr 5, 2018 at 12:29 PM, Murtuza Zabuawala < >>>> [email protected]> wrote: >>>> >>>>> Hi Dave, >>>>> >>>>> Please find rebased patch. >>>>> >>>>> -- >>>>> Regards, >>>>> Murtuza Zabuawala >>>>> EnterpriseDB: http://www.enterprisedb.com >>>>> The Enterprise PostgreSQL Company >>>>> >>>>> >>>>> On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: >>>>> >>>>>> Can you rebase this please? >>>>>> >>>>>> On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> Hi Dave, >>>>>>> >>>>>>> Please find updated patch with following changes, >>>>>>> - Combined Filter and Data sorting together same as pgAdmin3. >>>>>>> - Extracted model into separate file >>>>>>> - Change the colour of filter button from orange to blue. >>>>>>> - Updated docs and screenshot. >>>>>>> >>>>>>> @Joao, >>>>>>> Could you please provide any reference for learning more about >>>>>>> jasmine test framework? >>>>>>> >>>>>>> >>>>>>> -- >>>>>>> Regards, >>>>>>> Murtuza Zabuawala >>>>>>> EnterpriseDB: http://www.enterprisedb.com >>>>>>> The Enterprise PostgreSQL Company >>>>>>> >>>>>>> >>>>>>> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>>>>>> [email protected]> wrote: >>>>>>>>>> >>>>>>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>>>>>> wrote: >>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>>> >>>>>>>>>>>>>> Hi Hackers, >>>>>>>>>>>>>> >>>>>>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we >>>>>>>>>>>>>> are using axios instead of jquery ajax calls and that there is some >>>>>>>>>>>>>> coverage for the change. >>>>>>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and >>>>>>>>>>>>>> could be improved. Also the DataSorting class could have some other member >>>>>>>>>>>>>> functions like the model validation could be extracted out so that it is >>>>>>>>>>>>>> easily tested. >>>>>>>>>>>>>> >>>>>>>>>>>>>> >>>>>>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>>>>>> 4 - Pressed the column header to try to order, nothing >>>>>>>>>>>>>> happened >>>>>>>>>>>>>> 5 - Right clicked the column header to see if it was there >>>>>>>>>>>>>> the option, nothing >>>>>>>>>>>>>> >>>>>>>>>>>>>> This is the behavior that we were expecting, not to have to >>>>>>>>>>>>>> open Data View and then press the icon that is not even near the grid in >>>>>>>>>>>>>> order to sort the column. Is this really the way we want people to use the >>>>>>>>>>>>>> grid in pgAdmin? Should it be more intuitive? >>>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> Have we considered making the grid behave more like excel or >>>>>>>>>>>>> other grids? I think that having the ascending and descending inside the >>>>>>>>>>>>> column header, we could similarly provide filtering. Something that would >>>>>>>>>>>>> give users a more intuitive place to look. >>>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>>>>>> see fit. >>>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>> Honestly I'm not sold on my idea, I was just proposing an >>>>>>>>>> alternative in an effort to start a discussion about the user experience. >>>>>>>>>> Ideally what I'd like to see, maybe this happened, is some user research. >>>>>>>>>> When we initial worked on refactoring the results grid we made a bunch of >>>>>>>>>> changes. One of the things we intended to do was to follow up to see how >>>>>>>>>> people were using the grid now so that we could better understand how it >>>>>>>>>> was now being used in order to design and implement features just like >>>>>>>>>> this. Clearly we haven't gotten there yet. >>>>>>>>>> >>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> Another reason we can't use that because w >>>>>>>>>>> e have already occupied that behaviour for selecting entire >>>>>>>>>>> column >>>>>>>>>>> when user clicks on header. >>>>>>>>>>> As Dave suggested, I will be merging it with filter dialog >>>>>>>>>>> meaning it will be accessible via direct button on toolbar & keyboard >>>>>>>>>>> shortcut. >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>> >>>>>>>>>> How are users currently interacting with that filter dialog? >>>>>>>>>> >>>>>>>>> >>>>>>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>> >>>>>>>> Sorry I wasn't clear. My question was more along the lines of, do >>>>>>>> we know if people are using the filter functionality? What kind >>>>>>>> of filters are people using? What do they like about it? What do they wish >>>>>>>> they could do above and beyond sorting, etc. >>>>>>>> >>>>>>> I have not done any data gathering from users so I can't comment on >>>>>>> your queries. >>>>>>> but a >>>>>>> s far as I understood from the feature requests that most of the >>>>>>> users expect to have functionality which will allow then to sort columns as >>>>>>> it was in pgAdmin3. >>>>>>> >>>>>>> >>>>>>> >>>>>>>> -- Rob >>>>>>>> >>>>>>>> >>>>>>>>> >>>>>>>>>> What I'm suggesting is that we understand how users want to >>>>>>>>>> interact with their results, be those the results of a query or a table >>>>>>>>>> view, then we can design something that meets those needs. I agree that >>>>>>>>>> changing the column selection behavior isn't desirable, however, I also >>>>>>>>>> feel like providing the best user experience is better than holding onto a >>>>>>>>>> particular feature implementation. >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>> >>>>>>>>>> -- Rob >>>>>>>>>> >>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>>> -- >>>>>>>>>>>> Dave Page >>>>>>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>>>>>> Twitter: @pgsnake >>>>>>>>>>>> >>>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>>>>>> The Enterprise PostgreSQL Company >>>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>> >>>>>>>>> >>>>>>>> >>>>>>> >>>>>> >>>>>> >>>>>> -- >>>>>> Dave Page >>>>>> Blog: http://pgsnake.blogspot.com >>>>>> Twitter: @pgsnake >>>>>> >>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>> The Enterprise PostgreSQL Company >>>>>> >>>>> >>>>> >>>> >>>> >>>> -- >>>> Dave Page >>>> Blog: http://pgsnake.blogspot.com >>>> Twitter: @pgsnake >>>> >>>> EnterpriseDB UK: http://www.enterprisedb.com >>>> The Enterprise PostgreSQL Company >>>> >>> >> > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: [pgAdmin4][RM#3055] Allow user to sort the data in View data mode @ 2018-04-06 13:15 Joao De Almeida Pereira <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 0 replies; 25+ messages in thread From: Joao De Almeida Pereira @ 2018-04-06 13:15 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; Robert Eckhardt <[email protected]>; pgadmin-hackers Pretty cool, Thanks Murtuza On Fri, Apr 6, 2018 at 5:38 AM Dave Page <[email protected]> wrote: > Thanks, applied. > > On Fri, Apr 6, 2018 at 6:44 AM, Murtuza Zabuawala < > [email protected]> wrote: > >> Hi, >> >> Please find the patch which will change the syntax from requirejs syntax >> to ES6 from previous commit. >> >> -- >> Regards, >> Murtuza Zabuawala >> EnterpriseDB: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> >> >> On Fri, Apr 6, 2018 at 9:13 AM, Murtuza Zabuawala < >> [email protected]> wrote: >> >>> Hi Joao, >>> >>> >>> On Thu, Apr 5, 2018 at 11:20 PM, Joao De Almeida Pereira < >>> [email protected]> wrote: >>> >>>> Hi Murtuza, >>>> I forgot to mention this in my review........ >>>> Do you think we can start using only ES6 instead of keep using >>>> requirejs syntax on new things that we are building? >>>> How much effort do you think it will be? >>>> >>> Yes, we can. >>> Let me try to change the code to use ES6 and I'll send patch. >>> >>> >>>> >>>> Like this patch, as an example, do you think it could have been >>>> implemented without using requirejs? >>>> >>> >>>> Thanks >>>> >>>> On Thu, Apr 5, 2018 at 11:26 AM Dave Page <[email protected]> wrote: >>>> >>>>> Thanks, applied. >>>>> >>>>> On Thu, Apr 5, 2018 at 12:29 PM, Murtuza Zabuawala < >>>>> [email protected]> wrote: >>>>> >>>>>> Hi Dave, >>>>>> >>>>>> Please find rebased patch. >>>>>> >>>>>> -- >>>>>> Regards, >>>>>> Murtuza Zabuawala >>>>>> EnterpriseDB: http://www.enterprisedb.com >>>>>> The Enterprise PostgreSQL Company >>>>>> >>>>>> >>>>>> On Thu, Apr 5, 2018 at 4:15 PM, Dave Page <[email protected]> wrote: >>>>>> >>>>>>> Can you rebase this please? >>>>>>> >>>>>>> On Wed, Mar 28, 2018 at 8:19 AM, Murtuza Zabuawala < >>>>>>> [email protected]> wrote: >>>>>>> >>>>>>>> Hi Dave, >>>>>>>> >>>>>>>> Please find updated patch with following changes, >>>>>>>> - Combined Filter and Data sorting together same as pgAdmin3. >>>>>>>> - Extracted model into separate file >>>>>>>> - Change the colour of filter button from orange to blue. >>>>>>>> - Updated docs and screenshot. >>>>>>>> >>>>>>>> @Joao, >>>>>>>> Could you please provide any reference for learning more about >>>>>>>> jasmine test framework? >>>>>>>> >>>>>>>> >>>>>>>> -- >>>>>>>> Regards, >>>>>>>> Murtuza Zabuawala >>>>>>>> EnterpriseDB: http://www.enterprisedb.com >>>>>>>> The Enterprise PostgreSQL Company >>>>>>>> >>>>>>>> >>>>>>>> On Wed, Mar 28, 2018 at 6:07 AM, Robert Eckhardt < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Tue, Mar 27, 2018 at 9:54 AM, Murtuza Zabuawala < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> On Tue, Mar 27, 2018 at 7:06 PM, Robert Eckhardt < >>>>>>>>>> [email protected]> wrote: >>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> On Tue, Mar 27, 2018 at 6:25 AM, Murtuza Zabuawala < >>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>> >>>>>>>>>>>> On Tue, Mar 27, 2018 at 3:13 PM, Dave Page <[email protected]> >>>>>>>>>>>> wrote: >>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> On Mon, Mar 26, 2018 at 9:26 PM, Robert Eckhardt < >>>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>>> >>>>>>>>>>>>>> >>>>>>>>>>>>>> >>>>>>>>>>>>>> On Mon, Mar 26, 2018 at 2:07 PM, Joao De Almeida Pereira < >>>>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>>>> >>>>>>>>>>>>>>> Hi Hackers, >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> @Murtuza: The patch codewise looks good. Nice to see that we >>>>>>>>>>>>>>> are using axios instead of jquery ajax calls and that there is some >>>>>>>>>>>>>>> coverage for the change. >>>>>>>>>>>>>>> Nevertheless the Javascript testing looks a bit slim and >>>>>>>>>>>>>>> could be improved. Also the DataSorting class could have some other member >>>>>>>>>>>>>>> functions like the model validation could be extracted out so that it is >>>>>>>>>>>>>>> easily tested. >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> @Hackers: This was how we tried to test this feature: >>>>>>>>>>>>>>> 1 - Started pgAdmin >>>>>>>>>>>>>>> 2 - Opened the query tool for a specific server >>>>>>>>>>>>>>> 3 - Executed a SQL statment >>>>>>>>>>>>>>> 4 - Pressed the column header to try to order, nothing >>>>>>>>>>>>>>> happened >>>>>>>>>>>>>>> 5 - Right clicked the column header to see if it was there >>>>>>>>>>>>>>> the option, nothing >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> This is the behavior that we were expecting, not to have to >>>>>>>>>>>>>>> open Data View and then press the icon that is not even near the grid in >>>>>>>>>>>>>>> order to sort the column. Is this really the way we want people to use the >>>>>>>>>>>>>>> grid in pgAdmin? Should it be more intuitive? >>>>>>>>>>>>>>> >>>>>>>>>>>>>> >>>>>>>>>>>>>> Have we considered making the grid behave more like excel or >>>>>>>>>>>>>> other grids? I think that having the ascending and descending inside the >>>>>>>>>>>>>> column header, we could similarly provide filtering. Something that would >>>>>>>>>>>>>> give users a more intuitive place to look. >>>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> Doing the sorting via header clicks is convenient but very >>>>>>>>>>>>> restrictive. How do you specify multiple columns to sort by for example? >>>>>>>>>>>>> The current design allows you to select columns and the sort order as you >>>>>>>>>>>>> see fit. >>>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>> Honestly I'm not sold on my idea, I was just proposing an >>>>>>>>>>> alternative in an effort to start a discussion about the user experience. >>>>>>>>>>> Ideally what I'd like to see, maybe this happened, is some user research. >>>>>>>>>>> When we initial worked on refactoring the results grid we made a bunch of >>>>>>>>>>> changes. One of the things we intended to do was to follow up to see how >>>>>>>>>>> people were using the grid now so that we could better understand how it >>>>>>>>>>> was now being used in order to design and implement features just like >>>>>>>>>>> this. Clearly we haven't gotten there yet. >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> Another reason we can't use that because w >>>>>>>>>>>> e have already occupied that behaviour for selecting entire >>>>>>>>>>>> column >>>>>>>>>>>> when user clicks on header. >>>>>>>>>>>> As Dave suggested, I will be merging it with filter dialog >>>>>>>>>>>> meaning it will be accessible via direct button on toolbar & keyboard >>>>>>>>>>>> shortcut. >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> How are users currently interacting with that filter dialog? >>>>>>>>>>> >>>>>>>>>> >>>>>>>>>> By clicking on the toolbar button as well as keyboard shortcut. >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>> >>>>>>>>> Sorry I wasn't clear. My question was more along the lines of, do >>>>>>>>> we know if people are using the filter functionality? What kind >>>>>>>>> of filters are people using? What do they like about it? What do they wish >>>>>>>>> they could do above and beyond sorting, etc. >>>>>>>>> >>>>>>>> I have not done any data gathering from users so I can't comment >>>>>>>> on your queries. >>>>>>>> but a >>>>>>>> s far as I understood from the feature requests that most of the >>>>>>>> users expect to have functionality which will allow then to sort columns as >>>>>>>> it was in pgAdmin3. >>>>>>>> >>>>>>>> >>>>>>>> >>>>>>>>> -- Rob >>>>>>>>> >>>>>>>>> >>>>>>>>>> >>>>>>>>>>> What I'm suggesting is that we understand how users want to >>>>>>>>>>> interact with their results, be those the results of a query or a table >>>>>>>>>>> view, then we can design something that meets those needs. I agree that >>>>>>>>>>> changing the column selection behavior isn't desirable, however, I also >>>>>>>>>>> feel like providing the best user experience is better than holding onto a >>>>>>>>>>> particular feature implementation. >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>> >>>>>>>>>>> -- Rob >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>>> -- >>>>>>>>>>>>> Dave Page >>>>>>>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>>>>>>> Twitter: @pgsnake >>>>>>>>>>>>> >>>>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>>>>>>> The Enterprise PostgreSQL Company >>>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>> >>>>>>>>> >>>>>>>> >>>>>>> >>>>>>> >>>>>>> -- >>>>>>> Dave Page >>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>> Twitter: @pgsnake >>>>>>> >>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>> The Enterprise PostgreSQL Company >>>>>>> >>>>>> >>>>>> >>>>> >>>>> >>>>> -- >>>>> Dave Page >>>>> Blog: http://pgsnake.blogspot.com >>>>> Twitter: @pgsnake >>>>> >>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>> The Enterprise PostgreSQL Company >>>>> >>>> >>> >> > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > Attachments: [image/png] image.png (87.7K, 3-image.png) download | view image ^ permalink raw reply [nested|flat] 25+ messages in thread
end of thread, other threads:[~2018-04-06 13:15 UTC | newest] Thread overview: 25+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2018-03-25 18:13 [pgAdmin4][RM#3055] Allow user to sort the data in View data mode Murtuza Zabuawala <[email protected]> 2018-03-26 12:22 ` Dave Page <[email protected]> 2018-03-26 16:13 ` Murtuza Zabuawala <[email protected]> 2018-03-26 18:07 ` Joao De Almeida Pereira <[email protected]> 2018-03-26 20:26 ` Robert Eckhardt <[email protected]> 2018-03-27 09:43 ` Dave Page <[email protected]> 2018-03-27 10:25 ` Murtuza Zabuawala <[email protected]> 2018-03-27 13:36 ` Robert Eckhardt <[email protected]> 2018-03-27 13:54 ` Murtuza Zabuawala <[email protected]> 2018-03-28 00:37 ` Robert Eckhardt <[email protected]> 2018-03-28 07:19 ` Murtuza Zabuawala <[email protected]> 2018-04-05 10:45 ` Dave Page <[email protected]> 2018-04-05 11:29 ` Murtuza Zabuawala <[email protected]> 2018-04-05 13:25 ` Joao De Almeida Pereira <[email protected]> 2018-04-05 15:25 ` Dave Page <[email protected]> 2018-04-05 17:50 ` Joao De Almeida Pereira <[email protected]> 2018-04-06 03:43 ` Murtuza Zabuawala <[email protected]> 2018-04-06 05:44 ` Murtuza Zabuawala <[email protected]> 2018-04-06 09:38 ` Dave Page <[email protected]> 2018-04-06 13:15 ` Joao De Almeida Pereira <[email protected]> 2018-03-28 08:12 ` Dave Page <[email protected]> 2018-03-28 13:54 ` Robert Eckhardt <[email protected]> 2018-03-28 15:20 ` Dave Page <[email protected]> 2018-03-28 15:28 ` Robert Eckhardt <[email protected]> 2018-03-28 15:30 ` Dave Page <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox