mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Added Macro support. Fixes #1402
This commit is contained in:
committed by
Akshay Joshi
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
|
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
|
| `Issue #5200 <https://redmine.postgresql.org/issues/5200>`_ - Added support to ignore the owner while comparing objects in the Schema Diff tool
|
||||||
|
|
||||||
Housekeeping
|
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_identity_file = db.Column(db.String(64), nullable=True)
|
||||||
tunnel_password = db.Column(db.String(64), nullable=True)
|
tunnel_password = db.Column(db.String(64), nullable=True)
|
||||||
shared = db.Column(db.Boolean(), nullable=False)
|
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([
|
define([
|
||||||
'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
|
'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',
|
'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
|
||||||
], function(
|
], function(
|
||||||
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber,
|
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror,
|
||||||
commonUtils, keyboardShortcuts, configure_show_on_scroll
|
commonUtils, keyboardShortcuts, configure_show_on_scroll
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
@@ -44,7 +44,7 @@ define([
|
|||||||
_.extend(Backgrid.InputCellEditor.prototype.events, {
|
_.extend(Backgrid.InputCellEditor.prototype.events, {
|
||||||
'keydown': function(e) {
|
'keydown': function(e) {
|
||||||
let preferences = pgBrowser.get_preferences_for_module('browser');
|
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();
|
pgBrowser.keyboardNavigation.bindAddGridRow();
|
||||||
} else {
|
} else {
|
||||||
Backgrid.InputCellEditor.prototype.saveOrCancel.apply(this, arguments);
|
Backgrid.InputCellEditor.prototype.saveOrCancel.apply(this, arguments);
|
||||||
@@ -324,7 +324,7 @@ define([
|
|||||||
events: {
|
events: {
|
||||||
'keydown': function (event) {
|
'keydown': function (event) {
|
||||||
let preferences = pgBrowser.get_preferences_for_module('browser');
|
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();
|
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({
|
Backgrid.Extension.CustomHeaderCell = Backgrid.HeaderCell.extend({
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
// Here, we will add custom classes to header cell
|
// 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;
|
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;
|
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 {
|
export {
|
||||||
keyboardShortcutsDebugger as processEventDebugger,
|
keyboardShortcutsDebugger as processEventDebugger,
|
||||||
keyboardShortcutsQueryTool as processEventQueryTool,
|
keyboardShortcutsQueryTool as processEventQueryTool,
|
||||||
focusDockerPanel, validateShortcutKeys,
|
focusDockerPanel, validateShortcutKeys, validateMacros,
|
||||||
_stopEventPropagation, isMac, isKeyCtrlAlt, isKeyAltShift, isKeyCtrlShift,
|
_stopEventPropagation, isMac, isKeyCtrlAlt, isKeyAltShift, isKeyCtrlShift,
|
||||||
isKeyCtrlAltShift, isAltShiftBoth, isCtrlShiftBoth, isCtrlAltBoth,
|
isKeyCtrlAltShift, isAltShiftBoth, isCtrlShiftBoth, isCtrlAltBoth,
|
||||||
shortcut_key, shortcut_title, shortcut_accesskey_title,
|
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('');
|
$('.sql-editor-message').html('');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
executeMacro: function (sqlEditorController, MacroId) {
|
||||||
|
this._clearMessageTab();
|
||||||
|
sqlEditorController.check_data_changes_to_execute_query(null, false, MacroId);
|
||||||
|
},
|
||||||
|
|
||||||
executeQuery: function (sqlEditorController) {
|
executeQuery: function (sqlEditorController) {
|
||||||
this._clearMessageTab();
|
this._clearMessageTab();
|
||||||
sqlEditorController.check_data_changes_to_execute_query();
|
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.browser.utils import underscore_unescape
|
||||||
from pgadmin.utils.exception import ObjectGone
|
from pgadmin.utils.exception import ObjectGone
|
||||||
from pgadmin.utils.constants import MIMETYPE_APP_JS
|
from pgadmin.utils.constants import MIMETYPE_APP_JS
|
||||||
|
from pgadmin.tools.sqleditor.utils.macros import get_user_macros
|
||||||
|
|
||||||
MODULE_NAME = 'datagrid'
|
MODULE_NAME = 'datagrid'
|
||||||
|
|
||||||
@@ -274,6 +275,8 @@ def panel(trans_id):
|
|||||||
|
|
||||||
layout = get_setting('SQLEditor/Layout')
|
layout = get_setting('SQLEditor/Layout')
|
||||||
|
|
||||||
|
macros = get_user_macros()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"datagrid/index.html",
|
"datagrid/index.html",
|
||||||
_=gettext,
|
_=gettext,
|
||||||
@@ -286,6 +289,7 @@ def panel(trans_id):
|
|||||||
bgcolor=bgcolor,
|
bgcolor=bgcolor,
|
||||||
fgcolor=fgcolor,
|
fgcolor=fgcolor,
|
||||||
layout=layout,
|
layout=layout,
|
||||||
|
macros=macros
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -377,6 +377,31 @@
|
|||||||
<i class="fa fa-download sql-icon-lg" aria-hidden="true" role="img"></i>
|
<i class="fa fa-download sql-icon-lg" aria-hidden="true" role="img"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="connection_status_wrapper d-flex">
|
<div class="connection_status_wrapper d-flex">
|
||||||
@@ -459,7 +484,8 @@ require(['sources/generated/browser_nodes', 'sources/generated/codemirror', 'sou
|
|||||||
sqlEditorController.start(
|
sqlEditorController.start(
|
||||||
{{ uniqueId }},
|
{{ uniqueId }},
|
||||||
{{ url_params|safe}},
|
{{ url_params|safe}},
|
||||||
'{{ layout|safe }}'
|
'{{ layout|safe }}',
|
||||||
|
{{ macros|safe }}
|
||||||
);
|
);
|
||||||
|
|
||||||
// If opening from schema diff, set the generated script to the SQL Editor
|
// 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 PgAdminModule
|
||||||
from pgadmin.utils import get_storage_directory
|
from pgadmin.utils import get_storage_directory
|
||||||
from pgadmin.utils.ajax import make_json_response, bad_request, \
|
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.driver import get_driver
|
||||||
from pgadmin.utils.menu import MenuItem
|
from pgadmin.utils.menu import MenuItem
|
||||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
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.tools.sqleditor.utils.query_history import QueryHistory
|
||||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_CONNECTION_CLOSED,\
|
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_CONNECTION_CLOSED,\
|
||||||
ERROR_MSG_TRANS_ID_NOT_FOUND
|
ERROR_MSG_TRANS_ID_NOT_FOUND
|
||||||
|
from pgadmin.tools.sqleditor.utils.macros import get_macros,\
|
||||||
|
get_user_macros, set_macros
|
||||||
|
|
||||||
MODULE_NAME = 'sqleditor'
|
MODULE_NAME = 'sqleditor'
|
||||||
|
|
||||||
@@ -109,6 +111,9 @@ class SqlEditorModule(PgAdminModule):
|
|||||||
'sqleditor.get_query_history',
|
'sqleditor.get_query_history',
|
||||||
'sqleditor.add_query_history',
|
'sqleditor.add_query_history',
|
||||||
'sqleditor.clear_query_history',
|
'sqleditor.clear_query_history',
|
||||||
|
'sqleditor.get_macro',
|
||||||
|
'sqleditor.get_macros',
|
||||||
|
'sqleditor.set_macros'
|
||||||
]
|
]
|
||||||
|
|
||||||
def register_preferences(self):
|
def register_preferences(self):
|
||||||
@@ -1547,3 +1552,46 @@ def get_query_history(trans_id):
|
|||||||
check_transaction_status(trans_id)
|
check_transaction_status(trans_id)
|
||||||
|
|
||||||
return QueryHistory.get(current_user.id, trans_obj.sid, conn.db)
|
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 {
|
.hide-vertical-scrollbar {
|
||||||
overflow-y: hidden;
|
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',
|
'tools/datagrid/static/js/datagrid_panel_title',
|
||||||
'sources/window',
|
'sources/window',
|
||||||
'sources/is_native',
|
'sources/is_native',
|
||||||
|
'sources/sqleditor/macro',
|
||||||
'sources/../bundle/slickgrid',
|
'sources/../bundle/slickgrid',
|
||||||
'pgadmin.file_manager',
|
'pgadmin.file_manager',
|
||||||
'slick.pgadmin.formatters',
|
'slick.pgadmin.formatters',
|
||||||
@@ -57,7 +58,7 @@ define('tools.querytool', [
|
|||||||
GeometryViewer, historyColl, queryHist, querySources,
|
GeometryViewer, historyColl, queryHist, querySources,
|
||||||
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
|
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
|
||||||
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc,
|
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc,
|
||||||
pgWindow, isNative) {
|
pgWindow, isNative, MacroHandler) {
|
||||||
/* Return back, this has been called more than once */
|
/* Return back, this has been called more than once */
|
||||||
if (pgAdmin.SqlEditor)
|
if (pgAdmin.SqlEditor)
|
||||||
return pgAdmin.SqlEditor;
|
return pgAdmin.SqlEditor;
|
||||||
@@ -149,6 +150,9 @@ define('tools.querytool', [
|
|||||||
// Transaction control
|
// Transaction control
|
||||||
'click #btn-commit': 'on_commit_transaction',
|
'click #btn-commit': 'on_commit_transaction',
|
||||||
'click #btn-rollback': 'on_rollback_transaction',
|
'click #btn-rollback': 'on_rollback_transaction',
|
||||||
|
// Manage Macros
|
||||||
|
'click #btn-manage-macros': 'on_manage_macros',
|
||||||
|
'click .btn-macro': 'on_execute_macro',
|
||||||
},
|
},
|
||||||
|
|
||||||
reflectPreferences: function() {
|
reflectPreferences: function() {
|
||||||
@@ -2038,8 +2042,30 @@ define('tools.querytool', [
|
|||||||
|
|
||||||
queryToolActions.executeRollback(this.handler);
|
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
|
/* Defining controller class for data grid, which actually
|
||||||
* perform the operations like executing the sql query, poll the result,
|
* perform the operations like executing the sql query, poll the result,
|
||||||
* render the data in the grid, Save/Refresh the data etc...
|
* 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
|
* call the render method of the grid view to render the slickgrid
|
||||||
* header and loading icon and start execution of the sql query.
|
* 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;
|
var self = this;
|
||||||
|
|
||||||
self.is_query_tool = url_params.is_query_tool==='true'?true:false;
|
self.is_query_tool = url_params.is_query_tool==='true'?true:false;
|
||||||
@@ -2333,6 +2359,7 @@ define('tools.querytool', [
|
|||||||
layout: layout,
|
layout: layout,
|
||||||
});
|
});
|
||||||
self.transId = self.gridView.transId = transId;
|
self.transId = self.gridView.transId = transId;
|
||||||
|
self.macros = self.gridView.macros = macros;
|
||||||
|
|
||||||
self.gridView.current_file = undefined;
|
self.gridView.current_file = undefined;
|
||||||
|
|
||||||
@@ -2474,12 +2501,14 @@ define('tools.querytool', [
|
|||||||
self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self);
|
self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self);
|
||||||
// Format
|
// Format
|
||||||
self.on('pgadmin-sqleditor:format_sql', self._format_sql, self);
|
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);
|
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
|
// 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;
|
var self = this;
|
||||||
|
|
||||||
// Check if the data grid has any changes before running query
|
// 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?'),
|
gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'),
|
||||||
function() {
|
function() {
|
||||||
// The user does not want to save, just continue
|
// 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);
|
self._execute_sql_query(explain_prefix, shouldReconnect);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -2508,7 +2540,10 @@ define('tools.querytool', [
|
|||||||
cancel: gettext('No'),
|
cancel: gettext('No'),
|
||||||
});
|
});
|
||||||
} else {
|
} 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);
|
self._execute_sql_query(explain_prefix, shouldReconnect);
|
||||||
}
|
}
|
||||||
else {
|
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
|
// Executes sql query in the editor in Query Tool mode
|
||||||
_execute_sql_query: function(explain_prefix, shouldReconnect) {
|
_execute_sql_query: function(explain_prefix, shouldReconnect) {
|
||||||
var self = this, sql = '';
|
var self = this, sql = '';
|
||||||
@@ -3968,6 +4034,7 @@ define('tools.querytool', [
|
|||||||
$('#btn-file-menu-dropdown').prop('disabled', mode_disabled);
|
$('#btn-file-menu-dropdown').prop('disabled', mode_disabled);
|
||||||
$('#btn-find').prop('disabled', mode_disabled);
|
$('#btn-find').prop('disabled', mode_disabled);
|
||||||
$('#btn-find-menu-dropdown').prop('disabled', mode_disabled);
|
$('#btn-find-menu-dropdown').prop('disabled', mode_disabled);
|
||||||
|
$('#btn-macro-dropdown').prop('disabled', mode_disabled);
|
||||||
|
|
||||||
if (this.is_query_tool) {
|
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() {
|
isQueryRunning: function() {
|
||||||
return is_query_running;
|
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,
|
F7_KEY = 118,
|
||||||
F8_KEY = 119,
|
F8_KEY = 119,
|
||||||
PERIOD_KEY = 190,
|
PERIOD_KEY = 190,
|
||||||
FWD_SLASH_KEY = 191;
|
FWD_SLASH_KEY = 191,
|
||||||
|
C1_KEY = 49;
|
||||||
|
|
||||||
let sqlEditorControllerSpy, event, queryToolActionsSpy;
|
let sqlEditorControllerSpy, event, queryToolActionsSpy;
|
||||||
beforeEach(() => {
|
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, [
|
queryToolActionsSpy = jasmine.createSpyObj(queryToolActions, [
|
||||||
'explainAnalyze',
|
'explainAnalyze',
|
||||||
'explain',
|
'explain',
|
||||||
@@ -131,6 +145,7 @@ describe('the keyboard shortcuts', () => {
|
|||||||
'executeCommit',
|
'executeCommit',
|
||||||
'executeRollback',
|
'executeRollback',
|
||||||
'saveDataChanges',
|
'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() {
|
function expectEventPropagationToStop() {
|
||||||
describe('stops all event propogation', () => {
|
describe('stops all event propogation', () => {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user