mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-09 23:15:58 -06:00
Added Macro support. Fixes #1402
This commit is contained in:
parent
952197f130
commit
4616a74029
@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #1402 <https://redmine.postgresql.org/issues/1402>`_ - Added Macro support.
|
||||
| `Issue #5200 <https://redmine.postgresql.org/issues/5200>`_ - Added support to ignore the owner while comparing objects in the Schema Diff tool
|
||||
|
||||
Housekeeping
|
||||
|
56
web/migrations/versions/398697dc9550_.py
Normal file
56
web/migrations/versions/398697dc9550_.py
Normal file
@ -0,0 +1,56 @@
|
||||
|
||||
"""empty message
|
||||
|
||||
Revision ID: 398697dc9550
|
||||
Revises: a091c9611d20
|
||||
Create Date: 2020-09-07 15:17:59.473879
|
||||
|
||||
"""
|
||||
from pgadmin.model import db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '398697dc9550'
|
||||
down_revision = 'a091c9611d20'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
db.engine.execute("""
|
||||
CREATE TABLE macros (
|
||||
id INTEGER NOT NULL,
|
||||
alt BOOLEAN NOT NULL,
|
||||
control BOOLEAN NOT NULL,
|
||||
key VARCHAR(128) NOT NULL,
|
||||
key_code INTEGER NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
""")
|
||||
|
||||
db.engine.execute("""
|
||||
CREATE TABLE user_macros (
|
||||
mid INTEGER NOT NULL,
|
||||
uid INTEGER NOT NULL,
|
||||
name VARCHAR(1024) NOT NULL,
|
||||
sql TEXT NOT NULL,
|
||||
PRIMARY KEY(mid, uid),
|
||||
FOREIGN KEY(mid) REFERENCES macros (id),
|
||||
FOREIGN KEY(uid) REFERENCES user (id)
|
||||
);
|
||||
""")
|
||||
|
||||
db.engine.execute("""
|
||||
INSERT INTO macros (id, alt, control, key, key_code) VALUES (1, false, true, '1', 49),
|
||||
(2, false, true, '2', 50), (3, false, true, '3', 51), (4, false, true, '4', 52),
|
||||
(5, false, true, '5', 53), (6, false, true, '6', 54), (7, false, true, '7', 55),
|
||||
(8, false, true, '8', 56), (9, false, true, '9', 57), (10, false, true, '0', 48),
|
||||
(11, true, false, 'F1', 112), (12, true, false, 'F2', 113), (13, true, false, 'F3', 114),
|
||||
(14, true, false, 'F4', 115), (15, true, false, 'F5', 116), (16, true, false, 'F6', 117),
|
||||
(17, true, false, 'F7', 118), (18, true, false, 'F8', 119), (19, true, false, 'F9', 120),
|
||||
(20, true, false, 'F10', 121), (21, true, false, 'F11', 122), (22, true, false, 'F12', 123);
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# pgAdmin only upgrades, downgrade not implemented.
|
||||
pass
|
@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
SCHEMA_VERSION = 26
|
||||
SCHEMA_VERSION = 27
|
||||
|
||||
##########################################################################
|
||||
#
|
||||
@ -391,3 +391,26 @@ class SharedServer(db.Model):
|
||||
tunnel_identity_file = db.Column(db.String(64), nullable=True)
|
||||
tunnel_password = db.Column(db.String(64), nullable=True)
|
||||
shared = db.Column(db.Boolean(), nullable=False)
|
||||
|
||||
|
||||
class Macros(db.Model):
|
||||
"""Define a particular macro."""
|
||||
__tablename__ = 'macros'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
alt = db.Column(db.Boolean(), nullable=False)
|
||||
control = db.Column(db.Boolean(), nullable=False)
|
||||
key = db.Column(db.String(32), nullable=False)
|
||||
key_code = db.Column(db.Integer, nullable=False)
|
||||
|
||||
|
||||
class UserMacros(db.Model):
|
||||
"""Define the macro for a particular user."""
|
||||
__tablename__ = 'user_macros'
|
||||
mid = db.Column(
|
||||
db.Integer, db.ForeignKey('macros.id'), primary_key=True
|
||||
)
|
||||
uid = db.Column(
|
||||
db.Integer, db.ForeignKey('user.id'), primary_key=True
|
||||
)
|
||||
name = db.Column(db.String(1024), nullable=False)
|
||||
sql = db.Column(db.Text(), nullable=False)
|
||||
|
@ -9,10 +9,10 @@
|
||||
|
||||
define([
|
||||
'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
|
||||
'moment', 'bignumber', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll',
|
||||
'moment', 'bignumber', 'codemirror', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll',
|
||||
'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
|
||||
], function(
|
||||
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber,
|
||||
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror,
|
||||
commonUtils, keyboardShortcuts, configure_show_on_scroll
|
||||
) {
|
||||
/*
|
||||
@ -44,7 +44,7 @@ define([
|
||||
_.extend(Backgrid.InputCellEditor.prototype.events, {
|
||||
'keydown': function(e) {
|
||||
let preferences = pgBrowser.get_preferences_for_module('browser');
|
||||
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
|
||||
if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
|
||||
pgBrowser.keyboardNavigation.bindAddGridRow();
|
||||
} else {
|
||||
Backgrid.InputCellEditor.prototype.saveOrCancel.apply(this, arguments);
|
||||
@ -324,7 +324,7 @@ define([
|
||||
events: {
|
||||
'keydown': function (event) {
|
||||
let preferences = pgBrowser.get_preferences_for_module('browser');
|
||||
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) {
|
||||
if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) {
|
||||
pgBrowser.keyboardNavigation.bindAddGridRow();
|
||||
}
|
||||
},
|
||||
@ -606,6 +606,96 @@ define([
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Backgrid.Extension.ClearCell = Backgrid.Cell.extend({
|
||||
defaults: _.defaults({
|
||||
defaultClearMsg: gettext('Are you sure you wish to clear this row?'),
|
||||
defaultClearTitle: gettext('Clear Row'),
|
||||
}, Backgrid.Cell.prototype.defaults),
|
||||
|
||||
/** @property */
|
||||
className: 'clear-cell',
|
||||
events: {
|
||||
'click': 'clearRow',
|
||||
},
|
||||
clearRow: function(e) {
|
||||
e.preventDefault();
|
||||
if (_.isEmpty(e.currentTarget.innerHTML)) return false;
|
||||
var that = this;
|
||||
// We will check if row is deletable or not
|
||||
|
||||
var clear_msg = !_.isUndefined(this.column.get('customClearMsg')) ?
|
||||
this.column.get('customClearMsg') : that.defaults.defaultClearMsg;
|
||||
var clear_title = !_.isUndefined(this.column.get('customClearTitle')) ?
|
||||
this.column.get('customClearTitle') : that.defaults.defaultClearTitle;
|
||||
Alertify.confirm(
|
||||
clear_title,
|
||||
clear_msg,
|
||||
function() {
|
||||
that.model.set('name', null);
|
||||
that.model.set('sql', null);
|
||||
},
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
exitEditMode: function() {
|
||||
this.$el.removeClass('editor');
|
||||
},
|
||||
initialize: function() {
|
||||
Backgrid.Cell.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
this.$el.empty();
|
||||
$(this.$el).attr('tabindex', 0);
|
||||
if (this.model.get('name') !== null && this.model.get('sql') !== null)
|
||||
this.$el.html('<i aria-label="' + gettext('Clear row') + '" class=\'fa fa-eraser\' title=\'' + gettext('Clear row') + '\'></i>');
|
||||
// Listen for Tab/Shift-Tab key
|
||||
this.$el.on('keydown', function(e) {
|
||||
// with keyboard navigation on space key, mark row for deletion
|
||||
if (e.keyCode == 32) {
|
||||
self.$el.click();
|
||||
}
|
||||
var gotoCell;
|
||||
if (e.keyCode == 9 || e.keyCode == 16) {
|
||||
// go to Next Cell & if Shift is also pressed go to Previous Cell
|
||||
gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next();
|
||||
}
|
||||
|
||||
if (gotoCell) {
|
||||
let command = new Backgrid.Command({
|
||||
key: 'Tab',
|
||||
keyCode: 9,
|
||||
which: 9,
|
||||
shiftKey: e.shiftKey,
|
||||
});
|
||||
setTimeout(function() {
|
||||
// When we have Editable Cell
|
||||
if (gotoCell.hasClass('editable')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.model.trigger('backgrid:edited', self.model,
|
||||
self.column, command);
|
||||
}
|
||||
else {
|
||||
// When we have Non-Editable Cell
|
||||
self.model.trigger('backgrid:edited', self.model,
|
||||
self.column, command);
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Backgrid.Extension.CustomHeaderCell = Backgrid.HeaderCell.extend({
|
||||
initialize: function() {
|
||||
// Here, we will add custom classes to header cell
|
||||
@ -2081,6 +2171,84 @@ define([
|
||||
},
|
||||
});
|
||||
|
||||
Backgrid.Extension.SqlCell = Backgrid.Extension.TextareaCell.extend({
|
||||
className: 'sql-cell',
|
||||
defaults: {
|
||||
lineWrapping: true,
|
||||
},
|
||||
template: _.template([
|
||||
'<div data-toggle="tooltip" data-placement="top" data-html="true" title="<%- val %>"><textarea aria-label="' + gettext('SQL') +'" + style="display: none;"><%- val %></textarea><div>',
|
||||
].join('\n')),
|
||||
|
||||
render: function() {
|
||||
let self = this,
|
||||
col = _.defaults(this.column.toJSON(), this.defaults),
|
||||
model = this.model,
|
||||
column = this.column,
|
||||
columnName = this.column.get('name'),
|
||||
editable = Backgrid.callByNeed(col.editable, column, model);
|
||||
|
||||
if (this.sqlCell) {
|
||||
this.sqlCell.toTextArea();
|
||||
delete this.sqlCell;
|
||||
this.sqlCell = null;
|
||||
}
|
||||
|
||||
this.$el.empty();
|
||||
this.$el.append(this.template({
|
||||
val:this.formatter.fromRaw(model.get(columnName), model),
|
||||
})
|
||||
);
|
||||
this.$el.addClass(columnName);
|
||||
this.updateStateClassesMaybe();
|
||||
this.delegateEvents();
|
||||
|
||||
setTimeout(function() {
|
||||
self.sqlCell = CodeMirror.fromTextArea(
|
||||
(self.$el.find('textarea')[0]), {
|
||||
mode: 'text/x-pgsql',
|
||||
readOnly: !editable,
|
||||
singleCursorHeightPerLine: true,
|
||||
screenReaderLabel: columnName,
|
||||
});
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
enterEditMode: function () {
|
||||
if (!this.$el.hasClass('editor')) this.$el.addClass('editor');
|
||||
this.sqlCell.focus();
|
||||
this.sqlCell.on('blur', this.exitEditMode.bind(this));
|
||||
},
|
||||
exitEditMode: function () {
|
||||
this.$el.removeClass('editor');
|
||||
this.saveOrCancel.apply(this, arguments);
|
||||
},
|
||||
saveOrCancel: function() {
|
||||
var model = this.model;
|
||||
var column = this.column;
|
||||
if (this.sqlCell) {
|
||||
var val = this.sqlCell.getTextArea().value;
|
||||
var newValue = this.sqlCell.getValue();
|
||||
if (_.isUndefined(newValue)) {
|
||||
model.trigger('backgrid:error', model, column, val);
|
||||
}
|
||||
else {
|
||||
model.set(column.get('name'), newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
remove: function() {
|
||||
if (this.sqlCell) {
|
||||
$(this.$el.find('[data-toggle="tooltip"]')).tooltip('dispose');
|
||||
this.sqlCell.toTextArea();
|
||||
delete this.sqlCell;
|
||||
this.sqlCell = null;
|
||||
}
|
||||
return Backgrid.Extension.TextareaCell.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
return Backgrid;
|
||||
|
||||
});
|
||||
|
@ -324,15 +324,38 @@ function keyboardShortcutsQueryTool(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Macros
|
||||
let macroId = this.validateMacros(sqlEditorController, event);
|
||||
|
||||
if (macroId !== false) {
|
||||
this._stopEventPropagation(event);
|
||||
queryToolActions.executeMacro(sqlEditorController, macroId);
|
||||
}
|
||||
}
|
||||
|
||||
return panel_type;
|
||||
}
|
||||
|
||||
function validateMacros(sqlEditorController, event) {
|
||||
let keyCode = event.which || event.keyCode;
|
||||
|
||||
let macro = sqlEditorController.macros.filter(mc =>
|
||||
mc.alt == event.altKey &&
|
||||
mc.control == event.ctrlKey &&
|
||||
mc.key_code == keyCode);
|
||||
|
||||
if (macro.length == 1) {
|
||||
return macro[0].id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export {
|
||||
keyboardShortcutsDebugger as processEventDebugger,
|
||||
keyboardShortcutsQueryTool as processEventQueryTool,
|
||||
focusDockerPanel, validateShortcutKeys,
|
||||
focusDockerPanel, validateShortcutKeys, validateMacros,
|
||||
_stopEventPropagation, isMac, isKeyCtrlAlt, isKeyAltShift, isKeyCtrlShift,
|
||||
isKeyCtrlAltShift, isAltShiftBoth, isCtrlShiftBoth, isCtrlAltBoth,
|
||||
shortcut_key, shortcut_title, shortcut_accesskey_title,
|
||||
|
325
web/pgadmin/static/js/sqleditor/macro.js
Normal file
325
web/pgadmin/static/js/sqleditor/macro.js
Normal file
@ -0,0 +1,325 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
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 macroModel from 'sources/sqleditor/macro_model';
|
||||
import axios from 'axios';
|
||||
|
||||
let MacroDialog = {
|
||||
'dialog': function(handler) {
|
||||
let title = gettext('Manage Macros');
|
||||
|
||||
// Check the alertify dialog already loaded then delete it to clear
|
||||
// the cache
|
||||
if (Alertify.macroDialog) {
|
||||
delete Alertify.macroDialog;
|
||||
}
|
||||
|
||||
// Create Dialog
|
||||
Alertify.dialog('macroDialog', function factory() {
|
||||
let $container = $('<div class=\'macro_dialog\'></div>');
|
||||
return {
|
||||
main: function() {
|
||||
this.set('title', '<i class="fa fa-scroll sql-icon-lg" aria-hidden="true" role="img"></i> ' + gettext('Manage Macros'));
|
||||
},
|
||||
build: function() {
|
||||
this.elements.content.appendChild($container.get(0));
|
||||
Alertify.pgDialogBuild.apply(this);
|
||||
},
|
||||
setup: function() {
|
||||
return {
|
||||
buttons: [{
|
||||
text: '',
|
||||
key: 112,
|
||||
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
|
||||
attrs: {
|
||||
name: 'dialog_help',
|
||||
type: 'button',
|
||||
label: gettext('Help'),
|
||||
'aria-label': gettext('Help'),
|
||||
url: url_for('help.static', {
|
||||
'filename': 'querytool.html',
|
||||
}),
|
||||
},
|
||||
}, {
|
||||
text: gettext('Cancel'),
|
||||
key: 27,
|
||||
className: 'btn btn-secondary fa fa-times pg-alertify-button',
|
||||
'data-btn-name': 'cancel',
|
||||
}, {
|
||||
text: gettext('Save'),
|
||||
className: 'btn btn-primary fa fa-save pg-alertify-button',
|
||||
'data-btn-name': 'ok',
|
||||
}],
|
||||
// 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.macroCollectionModel.stopSession();
|
||||
this.view.model.stopSession();
|
||||
this.view.remove({
|
||||
data: true,
|
||||
internal: true,
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
prepare: function() {
|
||||
let self = this;
|
||||
$container.html('');
|
||||
// Status bar
|
||||
this.statusBar = $(
|
||||
'<div class=\'pg-prop-status-bar pg-el-xs-12 d-none\'>' +
|
||||
' <div class="error-in-footer"> ' +
|
||||
' <div class="d-flex px-2 py-1"> ' +
|
||||
' <div class="pr-2"> ' +
|
||||
' <i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i> ' +
|
||||
' </div> ' +
|
||||
' <div class="alert-text" role="alert"></div> ' +
|
||||
' </div> ' +
|
||||
' </div> ' +
|
||||
'</div>').appendTo($container);
|
||||
|
||||
// To show progress on filter Saving/Updating on AJAX
|
||||
this.showFilterProgress = $(
|
||||
`<div id="show_filter_progress" class="pg-sp-container sql-editor-busy-fetching d-none">
|
||||
<div class="pg-sp-content">
|
||||
<div class="row"><div class="col-12 pg-sp-icon sql-editor-busy-icon"></div></div>
|
||||
<div class="row"><div class="col-12 pg-sp-text sql-editor-busy-text">` + gettext('Loading data...') + `</div></div>
|
||||
</div>
|
||||
</div>`
|
||||
).appendTo($container);
|
||||
|
||||
self.macroCollectionModel = macroModel(handler.transId);
|
||||
|
||||
let fields = Backform.generateViewSchema(
|
||||
null, self.macroCollectionModel, 'edit', null, null, true
|
||||
);
|
||||
|
||||
let ManageMacroDialog = Backform.Dialog.extend({
|
||||
template: {
|
||||
'panel': _.template(
|
||||
'<div role="tabpanel" tabindex="-1" class="tab-pane <%=label%> <%=tabPanelCodeClass%> pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-12 fade" id="<%=cId%>"></div>'
|
||||
),
|
||||
},
|
||||
render: function() {
|
||||
this.cleanup();
|
||||
|
||||
var m = this.model,
|
||||
controls = this.controls,
|
||||
tmpls = this.template,
|
||||
dialog_obj = this,
|
||||
idx = (this.tabIndex * 100),
|
||||
evalF = function(f, d, model) {
|
||||
return (_.isFunction(f) ? !!f.apply(d, [model]) : !!f);
|
||||
};
|
||||
|
||||
this.$el
|
||||
.empty()
|
||||
.attr('role', 'tabpanel')
|
||||
.attr('class', _.result(this, 'tabPanelClassName'));
|
||||
m.panelEl = this.$el;
|
||||
|
||||
var tabContent = $('<div class="tab-content pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-12 macro-tab"></div>')
|
||||
.appendTo(this.$el);
|
||||
|
||||
_.each(this.schema, function(o) {
|
||||
idx++;
|
||||
if (!o.version_compatible || !evalF(o.visible, o, m)) {
|
||||
return;
|
||||
}
|
||||
var el = $((tmpls['panel'])(_.extend(o, {
|
||||
'tabIndex': idx,
|
||||
'tabPanelCodeClass': o.tabPanelCodeClass ? o.tabPanelCodeClass : '',
|
||||
})))
|
||||
.appendTo(tabContent)
|
||||
.removeClass('collapse').addClass('collapse');
|
||||
|
||||
o.fields.each(function(f) {
|
||||
var cntr = new(f.get('control'))({
|
||||
field: f,
|
||||
model: m,
|
||||
dialog: dialog_obj,
|
||||
tabIndex: idx,
|
||||
});
|
||||
el.append(cntr.render().$el);
|
||||
controls.push(cntr);
|
||||
});
|
||||
});
|
||||
|
||||
tabContent.find('.tab-pane').first().addClass('active show');
|
||||
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
let view = self.view = new ManageMacroDialog({
|
||||
el: '<div></div>',
|
||||
model: self.macroCollectionModel,
|
||||
schema: fields,
|
||||
});
|
||||
|
||||
self.macroCollectionModel.fetch({
|
||||
success: function() {
|
||||
|
||||
// We got the latest attributes of the object. Render the view
|
||||
// now.
|
||||
$container.append(self.view.render().$el);
|
||||
|
||||
// Enable/disable save button and show/hide statusbar based on session
|
||||
self.view.listenTo(self.view.model, 'pgadmin-session:start', function() {
|
||||
self.view.listenTo(self.view.model, 'pgadmin-session:invalid', function(msg) {
|
||||
self.statusBar.removeClass('d-none');
|
||||
$(self.statusBar.find('.alert-text')).html(msg);
|
||||
// Disable Okay button
|
||||
self.__internal.buttons[2].element.disabled = true;
|
||||
});
|
||||
|
||||
view.listenTo(self.view.model, 'pgadmin-session:valid', function() {
|
||||
self.statusBar.addClass('d-none');
|
||||
$(self.statusBar.find('.alert-text')).html('');
|
||||
// Enable Okay button
|
||||
self.__internal.buttons[2].element.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
view.listenTo(self.view.model, 'pgadmin-session:stop', function() {
|
||||
view.stopListening(self.view.model, 'pgadmin-session:invalid');
|
||||
view.stopListening(self.view.model, 'pgadmin-session:valid');
|
||||
});
|
||||
|
||||
// Starts monitoring changes to model
|
||||
self.view.model.startNewSession();
|
||||
|
||||
}});
|
||||
|
||||
$(this.elements.body.childNodes[0]).addClass(
|
||||
'alertify_tools_dialog_properties obj_properties'
|
||||
);
|
||||
|
||||
|
||||
|
||||
},
|
||||
// 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);
|
||||
return;
|
||||
} else if (e.button['data-btn-name'] === 'ok') {
|
||||
e.cancel = true; // Do not close dialog
|
||||
|
||||
let data = self.view.model.get('macro').toJSON(true);
|
||||
|
||||
if (data == undefined || data == null) {
|
||||
self.close();
|
||||
return;
|
||||
}
|
||||
|
||||
axios.put(
|
||||
url_for('sqleditor.set_macros', {
|
||||
'trans_id': handler.transId,
|
||||
}),
|
||||
data
|
||||
).then(function (result) {
|
||||
// Hide Progress ...
|
||||
$(
|
||||
self.showFilterProgress[0]
|
||||
).addClass('d-none');
|
||||
|
||||
let macroResponse = result;
|
||||
|
||||
if (macroResponse.status) {
|
||||
setTimeout(
|
||||
function() {
|
||||
// Update Macro Menu
|
||||
let macros = self.view.model.get('macro').toJSON().filter(m => m.name !== undefined || m.name !== null);
|
||||
handler.macros = macros;
|
||||
var str = `
|
||||
<li>
|
||||
<a class="dropdown-item" id="btn-manage-macros" href="#" tabindex="0">
|
||||
<span> Manage Macros... </span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>`;
|
||||
_.each(macros, function(m) {
|
||||
if (m.name) {
|
||||
str += `<li>
|
||||
<a class="dropdown-item btn-macro" data-macro-id="`+ m.id +`" href="#" tabindex="0">
|
||||
<span>` + m.name + `</span>
|
||||
<span> (` + m.key_label + `) </span>
|
||||
</a>
|
||||
</li>`;
|
||||
}
|
||||
});
|
||||
|
||||
$($.find('div.btn-group.mr-1.user_macros ul.dropdown-menu')).html($(str));
|
||||
|
||||
self.close(); // Close the dialog now
|
||||
Alertify.success(gettext('Macro updated successfully'));
|
||||
}, 10
|
||||
);
|
||||
} else {
|
||||
Alertify.alert(
|
||||
gettext('Validation Error'),
|
||||
macroResponse.result
|
||||
);
|
||||
}
|
||||
|
||||
}).catch(function (error) {
|
||||
// Hide Progress ...
|
||||
$(
|
||||
self.showFilterProgress[0]
|
||||
).addClass('d-none');
|
||||
|
||||
setTimeout(
|
||||
function() {
|
||||
Alertify.error(error.response.data.errormsg);
|
||||
}, 10
|
||||
);
|
||||
});
|
||||
|
||||
} else {
|
||||
self.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Alertify.macroDialog(title).resizeTo(pgAdmin.Browser.stdW.calc(pgAdmin.Browser.stdW.lg),
|
||||
pgAdmin.Browser.stdH.calc(pgAdmin.Browser.stdH.lg));
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = MacroDialog;
|
224
web/pgadmin/static/js/sqleditor/macro_model.js
Normal file
224
web/pgadmin/static/js/sqleditor/macro_model.js
Normal file
@ -0,0 +1,224 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import Backform from 'pgadmin.backform';
|
||||
import Backgrid from 'pgadmin.backgrid';
|
||||
import url_for from 'sources/url_for';
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
|
||||
export default function macroModel(transId) {
|
||||
|
||||
let MacroModel = pgAdmin.Browser.DataModel.extend({
|
||||
idAttribute: 'id',
|
||||
defaults: {
|
||||
id: undefined,
|
||||
key: undefined,
|
||||
name: undefined,
|
||||
sql: undefined,
|
||||
key_label: undefined,
|
||||
},
|
||||
schema: [{
|
||||
id: 'key_label',
|
||||
name: 'key_label',
|
||||
label: gettext('Key'),
|
||||
type: 'text',
|
||||
cell: 'string',
|
||||
editable: false,
|
||||
cellHeaderClasses: 'width_percent_10',
|
||||
headerCell: Backgrid.Extension.CustomHeaderCell,
|
||||
disabled: false,
|
||||
}, {
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
label: gettext('Name'),
|
||||
cell: 'string',
|
||||
type: 'text',
|
||||
editable: true,
|
||||
cellHeaderClasses: 'width_percent_20',
|
||||
headerCell: Backgrid.Extension.CustomHeaderCell,
|
||||
disabled: false,
|
||||
}, {
|
||||
id: 'sql',
|
||||
name: 'sql',
|
||||
label: gettext('SQL'),
|
||||
cell: Backgrid.Extension.SqlCell,
|
||||
type: 'multiline',
|
||||
control: Backform.SqlCodeControl,
|
||||
editable: true,
|
||||
cellHeaderClasses: 'width_percent_70',
|
||||
headerCell: Backgrid.Extension.CustomHeaderCell,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
validate: function() {
|
||||
let msg = null;
|
||||
this.errorModel.clear();
|
||||
if (_.isEmpty(this.get('name')) && !(_.isEmpty(this.get('sql')))) {
|
||||
msg = gettext('Please enter macro name.');
|
||||
this.errorModel.set('name', msg);
|
||||
return msg;
|
||||
} else if (_.isEmpty(this.get('sql')) && !(_.isEmpty(this.get('name')))) {
|
||||
msg = gettext('Please enter macro sql.');
|
||||
this.errorModel.set('sql', msg);
|
||||
return msg;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
let MacroCollectionModel = pgAdmin.Browser.DataModel.extend({
|
||||
defaults: {
|
||||
macro: undefined,
|
||||
},
|
||||
urlRoot: url_for('sqleditor.get_macros', {'trans_id': transId}),
|
||||
schema: [{
|
||||
id: 'macro',
|
||||
name: 'macro',
|
||||
label: gettext('Macros'),
|
||||
model: MacroModel,
|
||||
editable: true,
|
||||
type: 'collection',
|
||||
control: Backform.SubNodeCollectionControl.extend({
|
||||
showGridControl: function(data) {
|
||||
var self = this,
|
||||
gridBody = $('<div class=\'pgadmin-control-group backgrid form-group pg-el-12 object subnode\'></div>');
|
||||
|
||||
var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype,
|
||||
gridSchema = Backform.generateGridColumnsFromModel(
|
||||
data.node_info, subnode, this.field.get('mode'), data.columns, data.schema_node
|
||||
);
|
||||
|
||||
// Clean up existing grid if any (in case of re-render)
|
||||
if (self.grid) {
|
||||
self.grid.remove();
|
||||
}
|
||||
|
||||
// Set visibility of Add button
|
||||
if (data.disabled || data.canAdd == false) {
|
||||
$(gridBody).find('button.add').remove();
|
||||
}
|
||||
|
||||
// Insert Clear Cell into Grid
|
||||
gridSchema.columns.unshift({
|
||||
name: 'pg-backform-clear',
|
||||
label: '<i aria-label="' + gettext('Clear row') + '" class="fa fa-eraser" title="' + gettext('Clear row') + '"></i>',
|
||||
cell: Backgrid.Extension.ClearCell,
|
||||
editable: false,
|
||||
cell_priority: -1,
|
||||
sortable: false,
|
||||
headerCell: Backgrid.Extension.CustomHeaderCell.extend({
|
||||
className: 'header-icon-cell',
|
||||
events: {
|
||||
'click': 'clearrAll',
|
||||
},
|
||||
clearrAll: function(e) {
|
||||
e.preventDefault();
|
||||
var that = this;
|
||||
// We will check if row is deletable or not
|
||||
|
||||
Alertify.confirm(
|
||||
gettext('Clear All Rows'),
|
||||
gettext('Are you sure you wish to clear all rows?'),
|
||||
function() {
|
||||
_.each(that.collection.toJSON(), function(m) {
|
||||
that.collection.get(m.id).set({'name': null, 'sql': null});
|
||||
});
|
||||
},
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
this.$el.empty();
|
||||
var column = this.column;
|
||||
var label = $('<button type="button" title="' + gettext('Clear row') + '" aria-label="Clear row" aria-expanded="false" tabindex="0">').html(column.get('label')).append('<span class=\'sort-caret\' aria-hidden=\'true\'></span>');
|
||||
|
||||
this.$el.append(label);
|
||||
this.$el.addClass(column.get('name'));
|
||||
this.$el.addClass(column.get('direction'));
|
||||
this.$el.attr('role', 'columnheader');
|
||||
this.$el.attr('aria-label', 'columnheader');
|
||||
this.$el.attr('alt', 'columnheader');
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
var collection = self.model.get(data.name);
|
||||
|
||||
if (!collection) {
|
||||
collection = new(pgAdmin.Browser.Node.Collection)(null, {
|
||||
handler: self.model.handler || self.model,
|
||||
model: data.model,
|
||||
top: self.model.top || self.model,
|
||||
silent: true,
|
||||
});
|
||||
self.model.set(data.name, collection, {
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
|
||||
var cellEditing = function(args) {
|
||||
var ctx = this,
|
||||
cell = args[0];
|
||||
// Search for any other rows which are open.
|
||||
this.each(function(m) {
|
||||
// Check if row which we are about to close is not current row.
|
||||
if (cell.model != m) {
|
||||
var idx = ctx.indexOf(m);
|
||||
if (idx > -1) {
|
||||
var row = grid.body.rows[idx],
|
||||
rowEditCell = row.$el.find('.subnode-edit-in-process').parent();
|
||||
// Only close row if it's open.
|
||||
if (rowEditCell.length > 0) {
|
||||
var event = new Event('click');
|
||||
rowEditCell[0].dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
// Listen for any row which is about to enter in edit mode.
|
||||
collection.on('enteringEditMode', cellEditing, collection);
|
||||
|
||||
// Initialize a new Grid instance
|
||||
var grid = self.grid = new Backgrid.Grid({
|
||||
columns: gridSchema.columns,
|
||||
collection: collection,
|
||||
row: this.row,
|
||||
className: 'backgrid table presentation table-bordered table-noouter-border table-hover',
|
||||
});
|
||||
|
||||
// Render subNode grid
|
||||
var subNodeGrid = grid.render().$el;
|
||||
|
||||
var $dialog = gridBody.append(subNodeGrid);
|
||||
|
||||
return $dialog;
|
||||
},
|
||||
}),
|
||||
columns: ['key_label', 'name', 'sql'],
|
||||
visible: true,
|
||||
}],
|
||||
validate: function() {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
let model = new MacroCollectionModel();
|
||||
return model;
|
||||
}
|
@ -39,6 +39,11 @@ let queryToolActions = {
|
||||
$('.sql-editor-message').html('');
|
||||
},
|
||||
|
||||
executeMacro: function (sqlEditorController, MacroId) {
|
||||
this._clearMessageTab();
|
||||
sqlEditorController.check_data_changes_to_execute_query(null, false, MacroId);
|
||||
},
|
||||
|
||||
executeQuery: function (sqlEditorController) {
|
||||
this._clearMessageTab();
|
||||
sqlEditorController.check_data_changes_to_execute_query();
|
||||
|
@ -359,3 +359,31 @@ table tr th button {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.backgrid .sql-cell .CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.backgrid .sql-cell .cm-s-default {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
.backgrid .sql-cell .CodeMirror-hscrollbar > div {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.backgrid .sql-cell .CodeMirror-hscrollbar {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.backgrid .sql-cell .CodeMirror-vscrollbar {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.backgrid .sql-cell .CodeMirror-sizer {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.backgrid .sql-cell .CodeMirror-scrollbar-filler {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ from pgadmin.settings import get_setting
|
||||
from pgadmin.browser.utils import underscore_unescape
|
||||
from pgadmin.utils.exception import ObjectGone
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS
|
||||
from pgadmin.tools.sqleditor.utils.macros import get_user_macros
|
||||
|
||||
MODULE_NAME = 'datagrid'
|
||||
|
||||
@ -274,6 +275,8 @@ def panel(trans_id):
|
||||
|
||||
layout = get_setting('SQLEditor/Layout')
|
||||
|
||||
macros = get_user_macros()
|
||||
|
||||
return render_template(
|
||||
"datagrid/index.html",
|
||||
_=gettext,
|
||||
@ -286,6 +289,7 @@ def panel(trans_id):
|
||||
bgcolor=bgcolor,
|
||||
fgcolor=fgcolor,
|
||||
layout=layout,
|
||||
macros=macros
|
||||
)
|
||||
|
||||
|
||||
|
@ -377,6 +377,31 @@
|
||||
<i class="fa fa-download sql-icon-lg" aria-hidden="true" role="img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group mr-1 user_macros" role="group" aria-label="">
|
||||
<button id="btn-macro-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
aria-label="{{ _('Macros') }}" title="{{ _('Macros') }}" tabindex="0">
|
||||
<i class="fa fa-scroll sql-icon-lg" aria-hidden="true" role="img"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" id="btn-manage-macros" href="#" tabindex="0">
|
||||
<span> {{ _('Manage Macros...') }} </span>
|
||||
</a>
|
||||
</li>
|
||||
{% if macros|length > 0 %}
|
||||
<li class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
{% for i in macros %}
|
||||
<li>
|
||||
<a class="dropdown-item btn-macro" data-macro-id="{{ i.id }}" href="#" tabindex="0">
|
||||
<span> {{ _(i.name) }} </span>
|
||||
<span> ({{ i.key_label }}) </span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection_status_wrapper d-flex">
|
||||
@ -459,7 +484,8 @@ require(['sources/generated/browser_nodes', 'sources/generated/codemirror', 'sou
|
||||
sqlEditorController.start(
|
||||
{{ uniqueId }},
|
||||
{{ url_params|safe}},
|
||||
'{{ layout|safe }}'
|
||||
'{{ layout|safe }}',
|
||||
{{ macros|safe }}
|
||||
);
|
||||
|
||||
// If opening from schema diff, set the generated script to the SQL Editor
|
||||
|
@ -32,7 +32,7 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
|
||||
from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils import get_storage_directory
|
||||
from pgadmin.utils.ajax import make_json_response, bad_request, \
|
||||
success_return, internal_server_error
|
||||
success_return, internal_server_error, make_response as ajax_response
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from pgadmin.utils.menu import MenuItem
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
||||
@ -46,6 +46,8 @@ from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
|
||||
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_CONNECTION_CLOSED,\
|
||||
ERROR_MSG_TRANS_ID_NOT_FOUND
|
||||
from pgadmin.tools.sqleditor.utils.macros import get_macros,\
|
||||
get_user_macros, set_macros
|
||||
|
||||
MODULE_NAME = 'sqleditor'
|
||||
|
||||
@ -109,6 +111,9 @@ class SqlEditorModule(PgAdminModule):
|
||||
'sqleditor.get_query_history',
|
||||
'sqleditor.add_query_history',
|
||||
'sqleditor.clear_query_history',
|
||||
'sqleditor.get_macro',
|
||||
'sqleditor.get_macros',
|
||||
'sqleditor.set_macros'
|
||||
]
|
||||
|
||||
def register_preferences(self):
|
||||
@ -1547,3 +1552,46 @@ def get_query_history(trans_id):
|
||||
check_transaction_status(trans_id)
|
||||
|
||||
return QueryHistory.get(current_user.id, trans_obj.sid, conn.db)
|
||||
|
||||
|
||||
@blueprint.route(
|
||||
'/get_macros/<int:trans_id>',
|
||||
methods=["GET"], endpoint='get_macros'
|
||||
)
|
||||
@blueprint.route(
|
||||
'/get_macros/<int:macro_id>/<int:trans_id>',
|
||||
methods=["GET"], endpoint='get_macro'
|
||||
)
|
||||
@login_required
|
||||
def macros(trans_id, macro_id=None, json_resp=True):
|
||||
"""
|
||||
This method is used to get all the columns for data sorting dialog.
|
||||
|
||||
Args:
|
||||
trans_id: unique transaction id
|
||||
macro_id: Macro id
|
||||
"""
|
||||
|
||||
status, error_msg, conn, trans_obj, session_ob = \
|
||||
check_transaction_status(trans_id)
|
||||
|
||||
return get_macros(macro_id, json_resp)
|
||||
|
||||
|
||||
@blueprint.route(
|
||||
'/set_macros/<int:trans_id>',
|
||||
methods=["PUT"], endpoint='set_macros'
|
||||
)
|
||||
@login_required
|
||||
def update_macros(trans_id):
|
||||
"""
|
||||
This method is used to get all the columns for data sorting dialog.
|
||||
|
||||
Args:
|
||||
trans_id: unique transaction id
|
||||
"""
|
||||
|
||||
status, error_msg, conn, trans_obj, session_ob = \
|
||||
check_transaction_status(trans_id)
|
||||
|
||||
return set_macros()
|
||||
|
@ -395,3 +395,32 @@ input.editor-checkbox:focus {
|
||||
.hide-vertical-scrollbar {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* Macros */
|
||||
|
||||
.macro-tab {
|
||||
top: 0px !important;
|
||||
}
|
||||
|
||||
.macro-tab .tab-pane {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.macro_dialog .CodeMirror {
|
||||
overflow-y: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
.macro_dialog .sql-cell > div {
|
||||
overflow-y: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.macro_dialog .CodeMirror-cursor {
|
||||
width: 1px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
.macro_dialog .pg-prop-status-bar {
|
||||
z-index: 1;
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ define('tools.querytool', [
|
||||
'tools/datagrid/static/js/datagrid_panel_title',
|
||||
'sources/window',
|
||||
'sources/is_native',
|
||||
'sources/sqleditor/macro',
|
||||
'sources/../bundle/slickgrid',
|
||||
'pgadmin.file_manager',
|
||||
'slick.pgadmin.formatters',
|
||||
@ -57,7 +58,7 @@ define('tools.querytool', [
|
||||
GeometryViewer, historyColl, queryHist, querySources,
|
||||
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
|
||||
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc,
|
||||
pgWindow, isNative) {
|
||||
pgWindow, isNative, MacroHandler) {
|
||||
/* Return back, this has been called more than once */
|
||||
if (pgAdmin.SqlEditor)
|
||||
return pgAdmin.SqlEditor;
|
||||
@ -149,6 +150,9 @@ define('tools.querytool', [
|
||||
// Transaction control
|
||||
'click #btn-commit': 'on_commit_transaction',
|
||||
'click #btn-rollback': 'on_rollback_transaction',
|
||||
// Manage Macros
|
||||
'click #btn-manage-macros': 'on_manage_macros',
|
||||
'click .btn-macro': 'on_execute_macro',
|
||||
},
|
||||
|
||||
reflectPreferences: function() {
|
||||
@ -2038,8 +2042,30 @@ define('tools.querytool', [
|
||||
|
||||
queryToolActions.executeRollback(this.handler);
|
||||
},
|
||||
|
||||
// Callback function for manage macros button click.
|
||||
on_manage_macros: function() {
|
||||
var self = this;
|
||||
|
||||
// Trigger the show_filter signal to the SqlEditorController class
|
||||
self.handler.trigger(
|
||||
'pgadmin-sqleditor:button:manage_macros',
|
||||
self,
|
||||
self.handler
|
||||
);
|
||||
},
|
||||
|
||||
// Callback function for manage macros button click.
|
||||
on_execute_macro: function(e) {
|
||||
let macroId = $(e.currentTarget).data('macro-id');
|
||||
this.handler.history_query_source = QuerySources.EXECUTE;
|
||||
queryToolActions.executeMacro(this.handler, macroId);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
/* Defining controller class for data grid, which actually
|
||||
* perform the operations like executing the sql query, poll the result,
|
||||
* render the data in the grid, Save/Refresh the data etc...
|
||||
@ -2308,7 +2334,7 @@ define('tools.querytool', [
|
||||
* call the render method of the grid view to render the slickgrid
|
||||
* header and loading icon and start execution of the sql query.
|
||||
*/
|
||||
start: function(transId, url_params, layout) {
|
||||
start: function(transId, url_params, layout, macros) {
|
||||
var self = this;
|
||||
|
||||
self.is_query_tool = url_params.is_query_tool==='true'?true:false;
|
||||
@ -2333,6 +2359,7 @@ define('tools.querytool', [
|
||||
layout: layout,
|
||||
});
|
||||
self.transId = self.gridView.transId = transId;
|
||||
self.macros = self.gridView.macros = macros;
|
||||
|
||||
self.gridView.current_file = undefined;
|
||||
|
||||
@ -2474,12 +2501,14 @@ define('tools.querytool', [
|
||||
self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self);
|
||||
// Format
|
||||
self.on('pgadmin-sqleditor:format_sql', self._format_sql, self);
|
||||
self.on('pgadmin-sqleditor:button:manage_macros', self._manage_macros, self);
|
||||
self.on('pgadmin-sqleditor:button:execute_macro', self._execute_macro, self);
|
||||
|
||||
window.parent.$(window.parent.document).on('pgadmin-sqleditor:rows-copied', self._copied_in_other_session);
|
||||
},
|
||||
|
||||
// Checks if there is any dirty data in the grid before executing a query
|
||||
check_data_changes_to_execute_query: function(explain_prefix=null, shouldReconnect=false) {
|
||||
check_data_changes_to_execute_query: function(explain_prefix=null, shouldReconnect=false, macroId=undefined) {
|
||||
var self = this;
|
||||
|
||||
// Check if the data grid has any changes before running query
|
||||
@ -2492,7 +2521,10 @@ define('tools.querytool', [
|
||||
gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'),
|
||||
function() {
|
||||
// The user does not want to save, just continue
|
||||
if(self.is_query_tool) {
|
||||
if (macroId !== undefined) {
|
||||
self._execute_macro_query(explain_prefix, shouldReconnect, macroId);
|
||||
}
|
||||
else if(self.is_query_tool) {
|
||||
self._execute_sql_query(explain_prefix, shouldReconnect);
|
||||
}
|
||||
else {
|
||||
@ -2508,7 +2540,10 @@ define('tools.querytool', [
|
||||
cancel: gettext('No'),
|
||||
});
|
||||
} else {
|
||||
if(self.is_query_tool) {
|
||||
if (macroId !== undefined) {
|
||||
self._execute_macro_query(explain_prefix, shouldReconnect, macroId);
|
||||
}
|
||||
else if(self.is_query_tool) {
|
||||
self._execute_sql_query(explain_prefix, shouldReconnect);
|
||||
}
|
||||
else {
|
||||
@ -2602,6 +2637,37 @@ define('tools.querytool', [
|
||||
});
|
||||
},
|
||||
|
||||
// Executes sql query for macroin the editor in Query Tool mode
|
||||
_execute_macro_query: function(explain_prefix, shouldReconnect, macroId) {
|
||||
var self = this;
|
||||
|
||||
self.has_more_rows = false;
|
||||
self.fetching_rows = false;
|
||||
|
||||
$.ajax({
|
||||
url: url_for('sqleditor.get_macro', {'macro_id': macroId, 'trans_id': self.transId}),
|
||||
method: 'GET',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
})
|
||||
.done(function(res) {
|
||||
if (res) {
|
||||
// Replace the place holder
|
||||
let query = res.sql.replaceAll('$SELECTION$', self.gridView.query_tool_obj.getSelection());
|
||||
|
||||
const executeQuery = new ExecuteQuery.ExecuteQuery(self, pgAdmin.Browser.UserManagement);
|
||||
executeQuery.poll = pgBrowser.override_activity_event_decorator(executeQuery.poll).bind(executeQuery);
|
||||
executeQuery.execute(query, explain_prefix, shouldReconnect);
|
||||
} else {
|
||||
// Let it be for now
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
/* failure should not be ignored */
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// Executes sql query in the editor in Query Tool mode
|
||||
_execute_sql_query: function(explain_prefix, shouldReconnect) {
|
||||
var self = this, sql = '';
|
||||
@ -3968,6 +4034,7 @@ define('tools.querytool', [
|
||||
$('#btn-file-menu-dropdown').prop('disabled', mode_disabled);
|
||||
$('#btn-find').prop('disabled', mode_disabled);
|
||||
$('#btn-find-menu-dropdown').prop('disabled', mode_disabled);
|
||||
$('#btn-macro-dropdown').prop('disabled', mode_disabled);
|
||||
|
||||
if (this.is_query_tool) {
|
||||
|
||||
@ -4375,6 +4442,24 @@ define('tools.querytool', [
|
||||
});
|
||||
},
|
||||
|
||||
// This function will open the manage macro dialog
|
||||
_manage_macros: function() {
|
||||
let self = this;
|
||||
|
||||
/* When server is disconnected and connected, connection is lost,
|
||||
* To reconnect pass true
|
||||
*/
|
||||
MacroHandler.dialog(self);
|
||||
},
|
||||
|
||||
// This function will open the manage macro dialog
|
||||
_execute_macro: function() {
|
||||
|
||||
queryToolActions.executeMacro(this.handler);
|
||||
|
||||
},
|
||||
|
||||
|
||||
isQueryRunning: function() {
|
||||
return is_query_running;
|
||||
},
|
||||
|
125
web/pgadmin/tools/sqleditor/tests/test_macros.py
Normal file
125
web/pgadmin/tools/sqleditor/tests/test_macros.py
Normal file
@ -0,0 +1,125 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
|
||||
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
|
||||
database_utils
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression import parent_node_dict
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
import random
|
||||
|
||||
|
||||
class TestMacros(BaseTestGenerator):
|
||||
""" This class will test the query tool polling. """
|
||||
scenarios = [
|
||||
('Get all macros',
|
||||
dict(
|
||||
url='get_macros',
|
||||
method='get'
|
||||
)),
|
||||
('Set Macros',
|
||||
dict(
|
||||
url='set_macros',
|
||||
method='put',
|
||||
operation='update',
|
||||
data={
|
||||
'changed': [
|
||||
{'id': 1,
|
||||
'name': 'Test Macro 1',
|
||||
'sql': 'SELECT 1;'
|
||||
},
|
||||
{'id': 2,
|
||||
'name': 'Test Macro 2',
|
||||
'sql': 'SELECT 2;'
|
||||
},
|
||||
{'id': 3,
|
||||
'name': 'Test Macro 3',
|
||||
'sql': 'SELECT 3;'
|
||||
},
|
||||
]
|
||||
}
|
||||
)),
|
||||
('Clear Macros',
|
||||
dict(
|
||||
url='set_macros',
|
||||
method='put',
|
||||
operation='clear',
|
||||
data={
|
||||
'changed': [
|
||||
{'id': 1,
|
||||
'name': '',
|
||||
'sql': ''
|
||||
},
|
||||
{'id': 2,
|
||||
'name': '',
|
||||
'sql': ''
|
||||
},
|
||||
{'id': 3,
|
||||
'name': '',
|
||||
'sql': ''
|
||||
},
|
||||
]
|
||||
}
|
||||
))
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
""" This function will check messages return by query tool polling. """
|
||||
database_info = parent_node_dict["database"][-1]
|
||||
self.server_id = database_info["server_id"]
|
||||
|
||||
self.db_id = database_info["db_id"]
|
||||
db_con = database_utils.connect_database(self,
|
||||
utils.SERVER_GROUP,
|
||||
self.server_id,
|
||||
self.db_id)
|
||||
if not db_con["info"] == "Database connected.":
|
||||
raise Exception("Could not connect to the database.")
|
||||
|
||||
# Initialize query tool
|
||||
self.trans_id = str(random.randint(1, 9999999))
|
||||
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
|
||||
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
|
||||
response = self.tester.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def runTest(self):
|
||||
url = '/sqleditor/{0}/{1}'.format(self.url, self.trans_id)
|
||||
|
||||
if self.method == 'get':
|
||||
response = self.tester.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response_data = json.loads(response.data.decode('utf-8'))
|
||||
self.assertEqual(len(response_data['macro']), 22)
|
||||
else:
|
||||
response = self.tester.put(url,
|
||||
data=json.dumps(self.data),
|
||||
follow_redirects=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
for m in self.data['changed']:
|
||||
url = '/sqleditor/get_macros/{0}/{1}'.format(m['id'],
|
||||
self.trans_id)
|
||||
response = self.tester.get(url)
|
||||
|
||||
if self.operation == 'clear':
|
||||
self.assertEqual(response.status_code, 410)
|
||||
else:
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response_data = json.loads(response.data.decode('utf-8'))
|
||||
self.assertEqual(response_data['name'], m['name'])
|
||||
self.assertEqual(response_data['sql'], m['sql'])
|
||||
|
||||
def tearDown(self):
|
||||
# Disconnect the database
|
||||
database_utils.disconnect_database(self, self.server_id, self.db_id)
|
189
web/pgadmin/tools/sqleditor/utils/macros.py
Normal file
189
web/pgadmin/tools/sqleditor/utils/macros.py
Normal file
@ -0,0 +1,189 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Handle Macros for SQL Editor."""
|
||||
|
||||
import simplejson as json
|
||||
from flask_babelex import gettext
|
||||
from flask import current_app, request
|
||||
from flask_security import login_required, current_user
|
||||
from pgadmin.utils.ajax import make_response as ajax_response,\
|
||||
make_json_response
|
||||
from pgadmin.model import db, Macros, UserMacros
|
||||
from sqlalchemy import and_
|
||||
|
||||
|
||||
def get_macros(macro_id, json_resp):
|
||||
"""
|
||||
This method is used to get all the macros/specific macro.
|
||||
:param macro_id: Macro ID
|
||||
:param json_resp: Set True to return json response
|
||||
"""
|
||||
if macro_id:
|
||||
macro = UserMacros.query.filter_by(mid=macro_id,
|
||||
uid=current_user.id).first()
|
||||
if macro is None:
|
||||
return make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=gettext("Macro not found.")
|
||||
)
|
||||
else:
|
||||
return ajax_response(
|
||||
response={'id': macro.mid,
|
||||
'name': macro.name,
|
||||
'sql': macro.sql},
|
||||
status=200
|
||||
)
|
||||
else:
|
||||
macros = db.session.query(Macros.id, Macros.alt, Macros.control,
|
||||
Macros.key, Macros.key_code,
|
||||
UserMacros.name, UserMacros.sql
|
||||
).outerjoin(
|
||||
UserMacros, and_(Macros.id == UserMacros.mid,
|
||||
UserMacros.uid == current_user.id)).all()
|
||||
|
||||
data = []
|
||||
|
||||
for m in macros:
|
||||
key_label = 'Ctrl + ' + m[3] if m[2] is True else 'Alt + ' + m[3]
|
||||
data.append({'id': m[0], 'alt': m[1],
|
||||
'control': m[2], 'key': m[3],
|
||||
'key_code': m[4], 'name': m[5],
|
||||
'sql': m[6],
|
||||
'key_label': key_label})
|
||||
|
||||
if not json_resp:
|
||||
return data
|
||||
|
||||
return ajax_response(
|
||||
response={'macro': data},
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
def get_user_macros():
|
||||
"""
|
||||
This method is used to get all the user macros.
|
||||
"""
|
||||
|
||||
macros = db.session.query(UserMacros.name,
|
||||
Macros.id,
|
||||
Macros.alt, Macros.control,
|
||||
Macros.key, Macros.key_code
|
||||
).outerjoin(
|
||||
Macros, UserMacros.mid == Macros.id).filter(
|
||||
UserMacros.uid == current_user.id).order_by(UserMacros.name).all()
|
||||
|
||||
data = []
|
||||
|
||||
for m in macros:
|
||||
key_label = 'Ctrl + ' + m[4] if m[3] is True else 'Alt + ' + m[4]
|
||||
data.append({'name': m[0], 'id': m[1], 'key': m[4],
|
||||
'key_label': key_label, 'alt': 1 if m[2] else 0,
|
||||
'control': 1 if m[3] else 0, 'key_code': m[5]})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def set_macros():
|
||||
"""
|
||||
This method is used to update the user defined macros.
|
||||
"""
|
||||
|
||||
data = request.form if request.form else json.loads(
|
||||
request.data, encoding='utf-8'
|
||||
)
|
||||
|
||||
if 'changed' not in data:
|
||||
return make_json_response(
|
||||
success=1,
|
||||
info=gettext('Nothing to update.')
|
||||
)
|
||||
|
||||
for m in data['changed']:
|
||||
if m['id']:
|
||||
macro = UserMacros.query.filter_by(
|
||||
uid=current_user.id,
|
||||
mid=m['id']).first()
|
||||
if macro:
|
||||
status, msg = update_macro(m, macro)
|
||||
else:
|
||||
status, msg = create_macro(m)
|
||||
|
||||
if not status:
|
||||
return make_json_response(
|
||||
status=410, success=0, errormsg=msg
|
||||
)
|
||||
return ajax_response(status=200)
|
||||
|
||||
|
||||
def create_macro(macro):
|
||||
"""
|
||||
This method is used to create the user defined macros.
|
||||
:param macro: macro
|
||||
"""
|
||||
|
||||
required_args = [
|
||||
'name',
|
||||
'sql'
|
||||
]
|
||||
for arg in required_args:
|
||||
if arg not in macro:
|
||||
return False, gettext(
|
||||
"Could not find the required parameter ({}).").format(arg)
|
||||
|
||||
try:
|
||||
new_macro = UserMacros(
|
||||
uid=current_user.id,
|
||||
mid=macro['id'],
|
||||
name=macro['name'],
|
||||
sql=macro['sql']
|
||||
)
|
||||
db.session.add(new_macro)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
def update_macro(data, macro):
|
||||
"""
|
||||
This method is used to clear/update the user defined macros.
|
||||
:param data: updated macro data
|
||||
:param macro: macro
|
||||
"""
|
||||
|
||||
name = getattr(data, 'name', None)
|
||||
sql = getattr(data, 'sql', None)
|
||||
|
||||
if name or sql and macro.sql and name is None:
|
||||
return False, gettext(
|
||||
"Could not find the required parameter (name).")
|
||||
elif name or sql and macro.name and sql is None:
|
||||
return False, gettext(
|
||||
"Could not find the required parameter (sql).")
|
||||
|
||||
try:
|
||||
if name or sql:
|
||||
if name:
|
||||
macro.name = name
|
||||
if sql:
|
||||
macro.sql = sql
|
||||
else:
|
||||
db.session.delete(macro)
|
||||
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
return True, None
|
@ -18,7 +18,8 @@ describe('the keyboard shortcuts', () => {
|
||||
F7_KEY = 118,
|
||||
F8_KEY = 119,
|
||||
PERIOD_KEY = 190,
|
||||
FWD_SLASH_KEY = 191;
|
||||
FWD_SLASH_KEY = 191,
|
||||
C1_KEY = 49;
|
||||
|
||||
let sqlEditorControllerSpy, event, queryToolActionsSpy;
|
||||
beforeEach(() => {
|
||||
@ -120,6 +121,19 @@ describe('the keyboard shortcuts', () => {
|
||||
},
|
||||
};
|
||||
|
||||
sqlEditorControllerSpy.macros = [
|
||||
{
|
||||
alt: false,
|
||||
control: true,
|
||||
id: 1,
|
||||
key: '1',
|
||||
key_code: C1_KEY,
|
||||
key_label: 'Ctrl + 1',
|
||||
name: 'C1',
|
||||
sql: 'Select 1;',
|
||||
},
|
||||
];
|
||||
|
||||
queryToolActionsSpy = jasmine.createSpyObj(queryToolActions, [
|
||||
'explainAnalyze',
|
||||
'explain',
|
||||
@ -131,6 +145,7 @@ describe('the keyboard shortcuts', () => {
|
||||
'executeCommit',
|
||||
'executeRollback',
|
||||
'saveDataChanges',
|
||||
'executeMacro',
|
||||
]);
|
||||
});
|
||||
|
||||
@ -667,6 +682,47 @@ describe('the keyboard shortcuts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Macro Ctrl + 1', () => {
|
||||
describe('when there is not a query already running', () => {
|
||||
beforeEach(() => {
|
||||
event.which = C1_KEY;
|
||||
event.altKey = false;
|
||||
event.shiftKey = false;
|
||||
event.ctrlKey = true;
|
||||
|
||||
keyboardShortcuts.processEventQueryTool(
|
||||
sqlEditorControllerSpy, queryToolActionsSpy, event
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute the macro', () => {
|
||||
expect(queryToolActionsSpy.executeMacro).toHaveBeenCalledWith(sqlEditorControllerSpy,
|
||||
sqlEditorControllerSpy.macros[0].id);
|
||||
});
|
||||
|
||||
it('should stop event propagation', () => {
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the query is already running', () => {
|
||||
it('does nothing', () => {
|
||||
event.keyCode = C1_KEY;
|
||||
event.altKey = false;
|
||||
event.shiftKey = false;
|
||||
event.ctrlKey = true;
|
||||
|
||||
sqlEditorControllerSpy.isQueryRunning.and.returnValue(true);
|
||||
|
||||
keyboardShortcuts.processEventQueryTool(
|
||||
sqlEditorControllerSpy, queryToolActionsSpy, event
|
||||
);
|
||||
|
||||
expect(queryToolActionsSpy.executeMacro).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectEventPropagationToStop() {
|
||||
describe('stops all event propogation', () => {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user