Allow sorting when viewing/editing data. Fixes #1894
parent
659390493d
commit
fa1854bd85
|
@ -95,6 +95,29 @@ To delete a row, press the *Delete* toolbar button. A popup will open, asking y
|
||||||
|
|
||||||
To commit the changes to the server, select the *Save* toolbar button. Modifications to a row are written to the server automatically when you select a different row.
|
To commit the changes to the server, select the *Save* toolbar button. Modifications to a row are written to the server automatically when you select a different row.
|
||||||
|
|
||||||
|
**Sort/Filter options dialog**
|
||||||
|
|
||||||
|
You can access *Sort/Filter options dialog* by clicking on Sort/Filter button. This allows you to specify an SQL Filter to limit the data displayed and data sorting options in the edit grid window:
|
||||||
|
|
||||||
|
.. image:: images/editgrid_filter_dialog.png
|
||||||
|
:alt: Edit grid filter dialog window
|
||||||
|
|
||||||
|
* Use *SQL Filter* to provide SQL filtering criteria. These will be added to the "WHERE" clause of the query used to retrieve the data. For example, you might enter:
|
||||||
|
|
||||||
|
.. code-block:: sql
|
||||||
|
|
||||||
|
id > 25 AND created > '2018-01-01'
|
||||||
|
|
||||||
|
* Use *Data Sorting* to sort the data in the output grid
|
||||||
|
|
||||||
|
To add new column(s) in data sorting grid, click on the [+] icon.
|
||||||
|
|
||||||
|
* Use the drop-down *Column* to select the column you want to sort.
|
||||||
|
* Use the drop-down *Order* to select the sort order for the column.
|
||||||
|
|
||||||
|
To delete a row from the grid, click the trash icon.
|
||||||
|
|
||||||
|
* Click the *Help* button (?) to access online help.
|
||||||
|
* Click the *Ok* button to save work.
|
||||||
|
* Click the *Close* button to discard current changes and close the dialog.
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
|
@ -10,7 +10,7 @@ This release contains a number of features and fixes reported since the release
|
||||||
Features
|
Features
|
||||||
********
|
********
|
||||||
|
|
||||||
| `Feature #1305 <https://redmine.postgresql.org/issues/1305>`_ - Enable building of the runtime from the top level Makefile
|
| `Feature #1894 <https://redmine.postgresql.org/issues/1894>`_ - Allow sorting when viewing/editing data
|
||||||
| `Feature #1978 <https://redmine.postgresql.org/issues/1978>`_ - Add the ability to enable/disable UI animations
|
| `Feature #1978 <https://redmine.postgresql.org/issues/1978>`_ - Add the ability to enable/disable UI animations
|
||||||
| `Feature #2895 <https://redmine.postgresql.org/issues/2895>`_ - Add keyboard navigation options for the main browser windows
|
| `Feature #2895 <https://redmine.postgresql.org/issues/2895>`_ - Add keyboard navigation options for the main browser windows
|
||||||
| `Feature #2896 <https://redmine.postgresql.org/issues/2896>`_ - Add keyboard navigation in Query tool module via Tab/Shift-Tab key
|
| `Feature #2896 <https://redmine.postgresql.org/issues/2896>`_ - Add keyboard navigation in Query tool module via Tab/Shift-Tab key
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
|
@ -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;
|
||||||
|
});
|
|
@ -79,7 +79,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a id="btn-find-menu-find-next" href="#" tabindex="0">
|
<a id="btn-find-menu-find-next" href="#" tabindex="0">
|
||||||
<span> {{ _('Find next') }}{% if client_platform == 'macos' -%}
|
<span> {{ _('Find Next') }}{% if client_platform == 'macos' -%}
|
||||||
{{ _(' (Cmd+G)') }}
|
{{ _(' (Cmd+G)') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _(' (Ctrl+G)') }}{%- endif %}</span>
|
{{ _(' (Ctrl+G)') }}{%- endif %}</span>
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a id="btn-find-menu-find-previous" href="#" tabindex="0">
|
<a id="btn-find-menu-find-previous" href="#" tabindex="0">
|
||||||
<span> {{ _('Find previous') }}{% if client_platform == 'macos' -%}
|
<span> {{ _('Find Previous') }}{% if client_platform == 'macos' -%}
|
||||||
{{ _(' (Cmd+Shift+G)') }}
|
{{ _(' (Cmd+Shift+G)') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _(' (Ctrl+Shift+G)') }}{%- endif %}</span>
|
{{ _(' (Ctrl+Shift+G)') }}{%- endif %}</span>
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a id="btn-find-menu-find-persistent" href="#" tabindex="0">
|
<a id="btn-find-menu-find-persistent" href="#" tabindex="0">
|
||||||
<span>{{ _('Persistent find') }}</span>
|
<span>{{ _('Persistent Find') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a id="btn-find-menu-replace-all" href="#" tabindex="0">
|
<a id="btn-find-menu-replace-all" href="#" tabindex="0">
|
||||||
<span>{{ _('Replace all') }}</span>
|
<span>{{ _('Replace All') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
@ -194,10 +194,10 @@
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-right">
|
<ul class="dropdown-menu dropdown-menu-right">
|
||||||
<li>
|
<li>
|
||||||
<a id="btn-filter-menu" href="#" tabindex="0">{{ _('Filter') }}</a>
|
<a id="btn-filter-menu" href="#" tabindex="0">{{ _('Sort/Filter') }}</a>
|
||||||
<a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a>
|
<a id="btn-include-filter" href="#" tabindex="0">{{ _('Filter by Selection') }}</a>
|
||||||
<a id="btn-include-filter" href="#" tabindex="0">{{ _('By Selection') }}</a>
|
<a id="btn-exclude-filter" href="#" tabindex="0">{{ _('Exclude by Selection') }}</a>
|
||||||
<a id="btn-exclude-filter" href="#" tabindex="0">{{ _('Exclude Selection') }}</a>
|
<a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Sort/Filter') }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -341,23 +341,6 @@
|
||||||
<div class="editor-title"
|
<div class="editor-title"
|
||||||
style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% else %}{{ bgcolor or '#2C76B4' }}{% endif %}; color: {{ fgcolor or 'white' }};"></div>
|
style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% else %}{{ bgcolor or '#2C76B4' }}{% endif %}; color: {{ fgcolor or 'white' }};"></div>
|
||||||
</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>
|
<div id="editor-panel" tabindex="0"></div>
|
||||||
<iframe id="download-csv" style="display:none"></iframe>
|
<iframe id="download-csv" style="display:none"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,6 +40,7 @@ from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
|
||||||
RegisterQueryToolPreferences
|
RegisterQueryToolPreferences
|
||||||
from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \
|
from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \
|
||||||
read_file_generator
|
read_file_generator
|
||||||
|
from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
|
||||||
|
|
||||||
MODULE_NAME = 'sqleditor'
|
MODULE_NAME = 'sqleditor'
|
||||||
|
|
||||||
|
@ -92,8 +93,6 @@ class SqlEditorModule(PgAdminModule):
|
||||||
'sqleditor.fetch',
|
'sqleditor.fetch',
|
||||||
'sqleditor.fetch_all',
|
'sqleditor.fetch_all',
|
||||||
'sqleditor.save',
|
'sqleditor.save',
|
||||||
'sqleditor.get_filter',
|
|
||||||
'sqleditor.apply_filter',
|
|
||||||
'sqleditor.inclusive_filter',
|
'sqleditor.inclusive_filter',
|
||||||
'sqleditor.exclusive_filter',
|
'sqleditor.exclusive_filter',
|
||||||
'sqleditor.remove_filter',
|
'sqleditor.remove_filter',
|
||||||
|
@ -106,7 +105,9 @@ class SqlEditorModule(PgAdminModule):
|
||||||
'sqleditor.load_file',
|
'sqleditor.load_file',
|
||||||
'sqleditor.save_file',
|
'sqleditor.save_file',
|
||||||
'sqleditor.query_tool_download',
|
'sqleditor.query_tool_download',
|
||||||
'sqleditor.connection_status'
|
'sqleditor.connection_status',
|
||||||
|
'sqleditor.get_filter_data',
|
||||||
|
'sqleditor.set_filter_data'
|
||||||
]
|
]
|
||||||
|
|
||||||
def register_preferences(self):
|
def register_preferences(self):
|
||||||
|
@ -783,80 +784,6 @@ def save(trans_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route(
|
|
||||||
'/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})
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route(
|
|
||||||
'/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(
|
@blueprint.route(
|
||||||
'/filter/inclusive/<int:trans_id>',
|
'/filter/inclusive/<int:trans_id>',
|
||||||
methods=["PUT", "POST"], endpoint='inclusive_filter'
|
methods=["PUT", "POST"], endpoint='inclusive_filter'
|
||||||
|
@ -1561,3 +1488,37 @@ def query_tool_status(trans_id):
|
||||||
return internal_server_error(
|
return internal_server_error(
|
||||||
errormsg=gettext("Transaction status check failed.")
|
errormsg=gettext("Transaction status check failed.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route(
|
||||||
|
'/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))
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route(
|
||||||
|
'/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
|
||||||
|
)
|
||||||
|
|
|
@ -141,6 +141,10 @@ class SQLFilter(object):
|
||||||
- This method removes the filter applied.
|
- This method removes the filter applied.
|
||||||
* validate_filter(row_filter)
|
* validate_filter(row_filter)
|
||||||
- This method validates the given 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):
|
def __init__(self, **kwargs):
|
||||||
|
@ -160,8 +164,8 @@ class SQLFilter(object):
|
||||||
self.sid = kwargs['sid']
|
self.sid = kwargs['sid']
|
||||||
self.did = kwargs['did']
|
self.did = kwargs['did']
|
||||||
self.obj_id = kwargs['obj_id']
|
self.obj_id = kwargs['obj_id']
|
||||||
self.__row_filter = kwargs['sql_filter'] if 'sql_filter' in kwargs \
|
self.__row_filter = kwargs.get('sql_filter', None)
|
||||||
else None
|
self.__dara_sorting = kwargs.get('data_sorting', None)
|
||||||
|
|
||||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid)
|
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid)
|
||||||
conn = manager.connection(did=self.did)
|
conn = manager.connection(did=self.did)
|
||||||
|
@ -210,20 +214,41 @@ class SQLFilter(object):
|
||||||
|
|
||||||
return status, msg
|
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):
|
def is_filter_applied(self):
|
||||||
"""
|
"""
|
||||||
This function returns True if filter is applied else False.
|
This function returns True if filter is applied else False.
|
||||||
"""
|
"""
|
||||||
|
is_filter_applied = True
|
||||||
if self.__row_filter is None or self.__row_filter == '':
|
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):
|
def remove_filter(self):
|
||||||
"""
|
"""
|
||||||
This function remove the filter by setting value to None.
|
This function remove the filter by setting value to None.
|
||||||
"""
|
"""
|
||||||
self.__row_filter = None
|
self.__row_filter = None
|
||||||
|
self.__dara_sorting = None
|
||||||
|
|
||||||
def append_filter(self, row_filter):
|
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.cmd_type = kwargs['cmd_type'] if 'cmd_type' in kwargs else None
|
||||||
self.limit = -1
|
self.limit = -1
|
||||||
|
|
||||||
if self.cmd_type == VIEW_FIRST_100_ROWS or \
|
if self.cmd_type in (VIEW_FIRST_100_ROWS, VIEW_LAST_100_ROWS):
|
||||||
self.cmd_type == VIEW_LAST_100_ROWS:
|
|
||||||
self.limit = 100
|
self.limit = 100
|
||||||
|
|
||||||
def get_primary_keys(self, *args, **kwargs):
|
def get_primary_keys(self, *args, **kwargs):
|
||||||
return None, None
|
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):
|
def save(self, changed_data, default_conn=None):
|
||||||
return forbidden(
|
return forbidden(
|
||||||
errmsg=gettext("Data cannot be saved for the current object.")
|
errmsg=gettext("Data cannot be saved for the current object.")
|
||||||
|
@ -351,6 +421,17 @@ class GridCommand(BaseCommand, SQLFilter, FetchedRowTracker):
|
||||||
"""
|
"""
|
||||||
self.limit = limit
|
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):
|
class TableCommand(GridCommand):
|
||||||
"""
|
"""
|
||||||
|
@ -385,6 +466,7 @@ class TableCommand(GridCommand):
|
||||||
has_oids = self.has_oids(default_conn)
|
has_oids = self.has_oids(default_conn)
|
||||||
|
|
||||||
sql_filter = self.get_filter()
|
sql_filter = self.get_filter()
|
||||||
|
data_sorting = self.get_data_sorting()
|
||||||
|
|
||||||
if sql_filter is None:
|
if sql_filter is None:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
|
@ -392,7 +474,8 @@ class TableCommand(GridCommand):
|
||||||
object_name=self.object_name,
|
object_name=self.object_name,
|
||||||
nsp_name=self.nsp_name, pk_names=pk_names,
|
nsp_name=self.nsp_name, pk_names=pk_names,
|
||||||
cmd_type=self.cmd_type, limit=self.limit,
|
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:
|
else:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
|
@ -401,7 +484,7 @@ class TableCommand(GridCommand):
|
||||||
nsp_name=self.nsp_name, pk_names=pk_names,
|
nsp_name=self.nsp_name, pk_names=pk_names,
|
||||||
cmd_type=self.cmd_type, sql_filter=sql_filter,
|
cmd_type=self.cmd_type, sql_filter=sql_filter,
|
||||||
limit=self.limit, primary_keys=primary_keys,
|
limit=self.limit, primary_keys=primary_keys,
|
||||||
has_oids=has_oids
|
has_oids=has_oids, data_sorting=data_sorting
|
||||||
)
|
)
|
||||||
|
|
||||||
return sql
|
return sql
|
||||||
|
@ -447,6 +530,73 @@ class TableCommand(GridCommand):
|
||||||
|
|
||||||
return pk_names, primary_keys
|
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):
|
def can_edit(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -771,20 +921,22 @@ class ViewCommand(GridCommand):
|
||||||
to fetch the data for the specified view
|
to fetch the data for the specified view
|
||||||
"""
|
"""
|
||||||
sql_filter = self.get_filter()
|
sql_filter = self.get_filter()
|
||||||
|
data_sorting = self.get_data_sorting()
|
||||||
|
|
||||||
if sql_filter is None:
|
if sql_filter is None:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([self.sql_path, 'objectquery.sql']),
|
"/".join([self.sql_path, 'objectquery.sql']),
|
||||||
object_name=self.object_name,
|
object_name=self.object_name,
|
||||||
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
||||||
limit=self.limit
|
limit=self.limit, data_sorting=data_sorting
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([self.sql_path, 'objectquery.sql']),
|
"/".join([self.sql_path, 'objectquery.sql']),
|
||||||
object_name=self.object_name,
|
object_name=self.object_name,
|
||||||
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
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
|
return sql
|
||||||
|
@ -832,20 +984,22 @@ class ForeignTableCommand(GridCommand):
|
||||||
to fetch the data for the specified foreign table
|
to fetch the data for the specified foreign table
|
||||||
"""
|
"""
|
||||||
sql_filter = self.get_filter()
|
sql_filter = self.get_filter()
|
||||||
|
data_sorting = self.get_data_sorting()
|
||||||
|
|
||||||
if sql_filter is None:
|
if sql_filter is None:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([self.sql_path, 'objectquery.sql']),
|
"/".join([self.sql_path, 'objectquery.sql']),
|
||||||
object_name=self.object_name,
|
object_name=self.object_name,
|
||||||
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
||||||
limit=self.limit
|
limit=self.limit, data_sorting=data_sorting
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([self.sql_path, 'objectquery.sql']),
|
"/".join([self.sql_path, 'objectquery.sql']),
|
||||||
object_name=self.object_name,
|
object_name=self.object_name,
|
||||||
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
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
|
return sql
|
||||||
|
@ -883,20 +1037,22 @@ class CatalogCommand(GridCommand):
|
||||||
to fetch the data for the specified catalog object
|
to fetch the data for the specified catalog object
|
||||||
"""
|
"""
|
||||||
sql_filter = self.get_filter()
|
sql_filter = self.get_filter()
|
||||||
|
data_sorting = self.get_data_sorting()
|
||||||
|
|
||||||
if sql_filter is None:
|
if sql_filter is None:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([self.sql_path, 'objectquery.sql']),
|
"/".join([self.sql_path, 'objectquery.sql']),
|
||||||
object_name=self.object_name,
|
object_name=self.object_name,
|
||||||
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
||||||
limit=self.limit
|
limit=self.limit, data_sorting=data_sorting
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([self.sql_path, 'objectquery.sql']),
|
"/".join([self.sql_path, 'objectquery.sql']),
|
||||||
object_name=self.object_name,
|
object_name=self.object_name,
|
||||||
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
|
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
|
return sql
|
||||||
|
@ -929,6 +1085,9 @@ class QueryToolCommand(BaseCommand, FetchedRowTracker):
|
||||||
def get_sql(self, default_conn=None):
|
def get_sql(self, default_conn=None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_all_columns_with_order(self, default_conn=None):
|
||||||
|
return None
|
||||||
|
|
||||||
def can_edit(self):
|
def can_edit(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -602,3 +602,26 @@ input.editor-checkbox:focus {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 3em;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ define('tools.querytool', [
|
||||||
'sources/sqleditor_utils',
|
'sources/sqleditor_utils',
|
||||||
'sources/sqleditor/execute_query',
|
'sources/sqleditor/execute_query',
|
||||||
'sources/sqleditor/query_tool_http_error_handler',
|
'sources/sqleditor/query_tool_http_error_handler',
|
||||||
|
'sources/sqleditor/filter_dialog',
|
||||||
'sources/history/index.js',
|
'sources/history/index.js',
|
||||||
'sources/../jsx/history/query_history',
|
'sources/../jsx/history/query_history',
|
||||||
'react', 'react-dom',
|
'react', 'react-dom',
|
||||||
|
@ -33,7 +34,7 @@ define('tools.querytool', [
|
||||||
], function(
|
], function(
|
||||||
babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror,
|
babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror,
|
||||||
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
|
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
|
||||||
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler,
|
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
|
||||||
HistoryBundle, queryHistory, React, ReactDOM,
|
HistoryBundle, queryHistory, React, ReactDOM,
|
||||||
keyboardShortcuts, queryToolActions, Datagrid, modifyAnimation,
|
keyboardShortcuts, queryToolActions, Datagrid, modifyAnimation,
|
||||||
calculateQueryRunTime, callRenderAfterPoll) {
|
calculateQueryRunTime, callRenderAfterPoll) {
|
||||||
|
@ -112,8 +113,7 @@ define('tools.querytool', [
|
||||||
|
|
||||||
// This function is used to render the template.
|
// This function is used to render the template.
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this,
|
var self = this;
|
||||||
filter = self.$el.find('#sql_filter');
|
|
||||||
|
|
||||||
$('.editor-title').text(_.unescape(self.editor_title));
|
$('.editor-title').text(_.unescape(self.editor_title));
|
||||||
self.checkConnectionStatus();
|
self.checkConnectionStatus();
|
||||||
|
@ -121,31 +121,6 @@ define('tools.querytool', [
|
||||||
// Fetch and assign the shortcuts to current instance
|
// Fetch and assign the shortcuts to current instance
|
||||||
self.keyboardShortcutConfig = queryToolActions.getKeyboardShortcuts(self);
|
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
|
// Updates connection status flag
|
||||||
self.gain_focus = function() {
|
self.gain_focus = function() {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
@ -2141,11 +2116,11 @@ define('tools.querytool', [
|
||||||
if (self.can_filter && res.data.filter_applied) {
|
if (self.can_filter && res.data.filter_applied) {
|
||||||
$('#btn-filter').removeClass('btn-default');
|
$('#btn-filter').removeClass('btn-default');
|
||||||
$('#btn-filter-dropdown').removeClass('btn-default');
|
$('#btn-filter-dropdown').removeClass('btn-default');
|
||||||
$('#btn-filter').addClass('btn-warning');
|
$('#btn-filter').addClass('btn-primary');
|
||||||
$('#btn-filter-dropdown').addClass('btn-warning');
|
$('#btn-filter-dropdown').addClass('btn-primary');
|
||||||
} else {
|
} else {
|
||||||
$('#btn-filter').removeClass('btn-warning');
|
$('#btn-filter').removeClass('btn-primary');
|
||||||
$('#btn-filter-dropdown').removeClass('btn-warning');
|
$('#btn-filter-dropdown').removeClass('btn-primary');
|
||||||
$('#btn-filter').addClass('btn-default');
|
$('#btn-filter').addClass('btn-default');
|
||||||
$('#btn-filter-dropdown').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.
|
// This function will show the filter in the text area.
|
||||||
_show_filter: function() {
|
_show_filter: function() {
|
||||||
var self = this;
|
let self = this;
|
||||||
|
FilterHandler.dialog(self);
|
||||||
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
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// This function will include the filter by selection.
|
// This function will include the filter by selection.
|
||||||
|
|
|
@ -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 %}
|
|
@ -3,7 +3,11 @@ SELECT {% if has_oids %}oid, {% endif %}* FROM {{ conn|qtIdent(nsp_name, object_
|
||||||
{% if sql_filter %}
|
{% if sql_filter %}
|
||||||
WHERE {{ sql_filter }}
|
WHERE {{ sql_filter }}
|
||||||
{% endif %}
|
{% 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 %}
|
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 %}
|
{% if not loop.last %}, {% else %} {% endif %}{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
##########################################################################
|
||||||
|
#
|
||||||
|
# pgAdmin 4 - PostgreSQL Tools
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 - 2018, The pgAdmin Development Team
|
||||||
|
# This software is released under the PostgreSQL Licence
|
||||||
|
#
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
"""Code to handle data sorting in view data mode."""
|
||||||
|
import pickle
|
||||||
|
import simplejson as json
|
||||||
|
from flask_babelex import gettext
|
||||||
|
from pgadmin.utils.ajax import make_json_response, internal_server_error
|
||||||
|
from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
|
||||||
|
update_session_grid_transaction
|
||||||
|
|
||||||
|
|
||||||
|
class FilterDialog(object):
|
||||||
|
@staticmethod
|
||||||
|
def get(*args):
|
||||||
|
"""To fetch the current sorted columns"""
|
||||||
|
status, error_msg, conn, trans_obj, session_obj = args
|
||||||
|
if error_msg == gettext('Transaction ID not found in the session.'):
|
||||||
|
return make_json_response(
|
||||||
|
success=0,
|
||||||
|
errormsg=error_msg,
|
||||||
|
info='DATAGRID_TRANSACTION_REQUIRED',
|
||||||
|
status=404
|
||||||
|
)
|
||||||
|
column_list = []
|
||||||
|
if status and conn is not None and \
|
||||||
|
trans_obj is not None and session_obj is not None:
|
||||||
|
msg = gettext('Success')
|
||||||
|
columns, column_list = trans_obj.get_all_columns_with_order(conn)
|
||||||
|
sql = trans_obj.get_filter()
|
||||||
|
else:
|
||||||
|
status = False
|
||||||
|
msg = error_msg
|
||||||
|
columns = None
|
||||||
|
sql = None
|
||||||
|
|
||||||
|
return make_json_response(
|
||||||
|
data={
|
||||||
|
'status': status,
|
||||||
|
'msg': msg,
|
||||||
|
'result': {
|
||||||
|
'data_sorting': columns,
|
||||||
|
'column_list': column_list,
|
||||||
|
'sql': sql
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save(*args, **kwargs):
|
||||||
|
"""To save the sorted columns"""
|
||||||
|
# Check the transaction and connection status
|
||||||
|
status, error_msg, conn, trans_obj, session_obj = args
|
||||||
|
trans_id = kwargs['trans_id']
|
||||||
|
request = kwargs['request']
|
||||||
|
|
||||||
|
if request.data:
|
||||||
|
data = json.loads(request.data, encoding='utf-8')
|
||||||
|
else:
|
||||||
|
data = request.args or request.form
|
||||||
|
|
||||||
|
if error_msg == gettext('Transaction ID not found in the session.'):
|
||||||
|
return make_json_response(
|
||||||
|
success=0,
|
||||||
|
errormsg=error_msg,
|
||||||
|
info='DATAGRID_TRANSACTION_REQUIRED',
|
||||||
|
status=404
|
||||||
|
)
|
||||||
|
|
||||||
|
if status and conn is not None and \
|
||||||
|
trans_obj is not None and session_obj is not None:
|
||||||
|
trans_obj.set_data_sorting(data)
|
||||||
|
trans_obj.set_filter(data.get('sql'))
|
||||||
|
# As we changed the transaction object we need to
|
||||||
|
# restore it and update the session variable.
|
||||||
|
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
|
||||||
|
update_session_grid_transaction(trans_id, session_obj)
|
||||||
|
res = gettext('Data sorting object updated successfully')
|
||||||
|
else:
|
||||||
|
return internal_server_error(
|
||||||
|
errormsg=gettext('Failed to update the data on server.')
|
||||||
|
)
|
||||||
|
|
||||||
|
return make_json_response(
|
||||||
|
data={
|
||||||
|
'status': status,
|
||||||
|
'result': res
|
||||||
|
}
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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({});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue