mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Make the Query Tool history persistent across sessions. Fixes #4017
This commit is contained in:
parent
a502019e20
commit
ab9a3a57ad
@ -179,6 +179,12 @@ The Query History tab displays information about recent commands:
|
|||||||
To erase the content of the *Query History* tab, select *Clear history* from
|
To erase the content of the *Query History* tab, select *Clear history* from
|
||||||
the *Clear* drop-down menu.
|
the *Clear* drop-down menu.
|
||||||
|
|
||||||
|
Query History is maintained across sessions for each database on a per-user
|
||||||
|
basis when running in Query Tool mode. In View/Edit Data mode, history is not
|
||||||
|
retained. By default, the last 20 queries are stored for each database. This
|
||||||
|
can be adjusted in `config_local.py` by overriding the `MAX_QUERY_HIST_STORED`
|
||||||
|
value. See the :ref:`Deployment <deployment>` section for more information.
|
||||||
|
|
||||||
Use the *Connection status* feature to view the current connection and
|
Use the *Connection status* feature to view the current connection and
|
||||||
transaction status by clicking on the status icon in the Query Tool:
|
transaction status by clicking on the status icon in the Query Tool:
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ Features
|
|||||||
********
|
********
|
||||||
|
|
||||||
| `Feature #2001 <https://redmine.postgresql.org/issues/2001>`_ - Add support for reverse proxied setups with Gunicorn, and document Gunicorn, uWSGI & NGINX configurations.
|
| `Feature #2001 <https://redmine.postgresql.org/issues/2001>`_ - Add support for reverse proxied setups with Gunicorn, and document Gunicorn, uWSGI & NGINX configurations.
|
||||||
|
| `Feature #4017 <https://redmine.postgresql.org/issues/4018>`_ - Make the Query Tool history persistent across sessions.
|
||||||
| `Feature #4018 <https://redmine.postgresql.org/issues/4018>`_ - Remove the large and unnecessary dependency on React and 87 other related libraries.
|
| `Feature #4018 <https://redmine.postgresql.org/issues/4018>`_ - Remove the large and unnecessary dependency on React and 87 other related libraries.
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
|
@ -245,6 +245,9 @@ SQLITE_TIMEOUT = 500
|
|||||||
# Set to False to disable password saving.
|
# Set to False to disable password saving.
|
||||||
ALLOW_SAVE_PASSWORD = True
|
ALLOW_SAVE_PASSWORD = True
|
||||||
|
|
||||||
|
# Maximum number of history queries stored per user/server/database
|
||||||
|
MAX_QUERY_HIST_STORED = 20
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# Server-side session storage path
|
# Server-side session storage path
|
||||||
#
|
#
|
||||||
|
42
web/migrations/versions/ec1cac3399c9_.py
Normal file
42
web/migrations/versions/ec1cac3399c9_.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: ec1cac3399c9
|
||||||
|
Revises: b5b87fdfcb30
|
||||||
|
Create Date: 2019-03-07 16:05:28.874203
|
||||||
|
|
||||||
|
"""
|
||||||
|
from pgadmin.model import db
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ec1cac3399c9'
|
||||||
|
down_revision = 'b5b87fdfcb30'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
srno = db.Column(db.Integer(), nullable=False, primary_key=True)
|
||||||
|
uid = db.Column(
|
||||||
|
db.Integer, db.ForeignKey('user.id'), nullable=False, primary_key=True
|
||||||
|
)
|
||||||
|
sid = db.Column(db.Integer(), nullable=False, primary_key=True)
|
||||||
|
did = db.Column(db.Integer(), nullable=False, primary_key=True)
|
||||||
|
query = db.Column(db.String(), nullable=False)
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
db.engine.execute("""
|
||||||
|
CREATE TABLE query_history (
|
||||||
|
srno INTEGER NOT NULL,
|
||||||
|
uid INTEGER NOT NULL,
|
||||||
|
sid INTEGER NOT NULL,
|
||||||
|
dbname TEXT NOT NULL,
|
||||||
|
query_info TEXT NOT NULL,
|
||||||
|
last_updated_flag TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (srno, uid, sid, dbname),
|
||||||
|
FOREIGN KEY(uid) REFERENCES user (id),
|
||||||
|
FOREIGN KEY(sid) REFERENCES server (id)
|
||||||
|
)""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
@ -20,6 +20,7 @@ from pgadmin.utils.ajax import make_json_response, bad_request, forbidden, \
|
|||||||
make_response as ajax_response, internal_server_error, unauthorized, gone
|
make_response as ajax_response, internal_server_error, unauthorized, gone
|
||||||
from pgadmin.utils.crypto import encrypt, decrypt, pqencryptpassword
|
from pgadmin.utils.crypto import encrypt, decrypt, pqencryptpassword
|
||||||
from pgadmin.utils.menu import MenuItem
|
from pgadmin.utils.menu import MenuItem
|
||||||
|
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from config import PG_DEFAULT_DRIVER
|
from config import PG_DEFAULT_DRIVER
|
||||||
@ -450,6 +451,9 @@ class ServerNode(PGChildNodeView):
|
|||||||
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
|
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
|
||||||
db.session.delete(s)
|
db.session.delete(s)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
QueryHistory.clear_history(current_user.id, sid)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
|
@ -15,6 +15,7 @@ from functools import wraps
|
|||||||
import simplejson as json
|
import simplejson as json
|
||||||
from flask import render_template, current_app, request, jsonify
|
from flask import render_template, current_app, request, jsonify
|
||||||
from flask_babelex import gettext as _
|
from flask_babelex import gettext as _
|
||||||
|
from flask_security import current_user
|
||||||
|
|
||||||
import pgadmin.browser.server_groups.servers as servers
|
import pgadmin.browser.server_groups.servers as servers
|
||||||
from config import PG_DEFAULT_DRIVER
|
from config import PG_DEFAULT_DRIVER
|
||||||
@ -28,6 +29,7 @@ from pgadmin.utils.ajax import gone
|
|||||||
from pgadmin.utils.ajax import make_json_response, \
|
from pgadmin.utils.ajax import make_json_response, \
|
||||||
make_response as ajax_response, internal_server_error, unauthorized
|
make_response as ajax_response, internal_server_error, unauthorized
|
||||||
from pgadmin.utils.driver import get_driver
|
from pgadmin.utils.driver import get_driver
|
||||||
|
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
||||||
|
|
||||||
|
|
||||||
class DatabaseModule(CollectionNodeModule):
|
class DatabaseModule(CollectionNodeModule):
|
||||||
@ -675,6 +677,8 @@ class DatabaseView(PGChildNodeView):
|
|||||||
)
|
)
|
||||||
return internal_server_error(errormsg=msg)
|
return internal_server_error(errormsg=msg)
|
||||||
|
|
||||||
|
QueryHistory.update_history_dbname(
|
||||||
|
current_user.id, sid, data['old_name'], data['name'])
|
||||||
# Make connection for database again
|
# Make connection for database again
|
||||||
if self._db['datallowconn']:
|
if self._db['datallowconn']:
|
||||||
self.conn = self.manager.connection(
|
self.conn = self.manager.connection(
|
||||||
|
@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
#
|
#
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
SCHEMA_VERSION = 21
|
SCHEMA_VERSION = 22
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
#
|
#
|
||||||
@ -267,3 +267,18 @@ class Keys(db.Model):
|
|||||||
__tablename__ = 'keys'
|
__tablename__ = 'keys'
|
||||||
name = db.Column(db.String(), nullable=False, primary_key=True)
|
name = db.Column(db.String(), nullable=False, primary_key=True)
|
||||||
value = db.Column(db.String(), nullable=False)
|
value = db.Column(db.String(), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class QueryHistoryModel(db.Model):
|
||||||
|
"""Define the history SQL table."""
|
||||||
|
__tablename__ = 'query_history'
|
||||||
|
srno = db.Column(db.Integer(), nullable=False, primary_key=True)
|
||||||
|
uid = db.Column(
|
||||||
|
db.Integer, db.ForeignKey('user.id'), nullable=False, primary_key=True
|
||||||
|
)
|
||||||
|
sid = db.Column(
|
||||||
|
db.Integer(), db.ForeignKey('server.id'), nullable=False,
|
||||||
|
primary_key=True)
|
||||||
|
dbname = db.Column(db.String(), nullable=False, primary_key=True)
|
||||||
|
query_info = db.Column(db.String(), nullable=False)
|
||||||
|
last_updated_flag = db.Column(db.String(), nullable=False)
|
||||||
|
@ -10,6 +10,7 @@ export default class QueryHistory {
|
|||||||
this.histCollection = histModel;
|
this.histCollection = histModel;
|
||||||
this.editorPref = {};
|
this.editorPref = {};
|
||||||
|
|
||||||
|
this.onCopyToEditorHandler = ()=>{};
|
||||||
this.histCollection.onAdd(this.onAddEntry.bind(this));
|
this.histCollection.onAdd(this.onAddEntry.bind(this));
|
||||||
this.histCollection.onReset(this.onResetEntries.bind(this));
|
this.histCollection.onReset(this.onResetEntries.bind(this));
|
||||||
}
|
}
|
||||||
@ -35,8 +36,19 @@ export default class QueryHistory {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCopyToEditorClick(onCopyToEditorHandler) {
|
||||||
|
this.onCopyToEditorHandler = onCopyToEditorHandler;
|
||||||
|
|
||||||
|
if(this.queryHistDetails) {
|
||||||
|
this.queryHistDetails.onCopyToEditorClick(this.onCopyToEditorHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setEditorPref(editorPref) {
|
setEditorPref(editorPref) {
|
||||||
this.editorPref = editorPref;
|
this.editorPref = {
|
||||||
|
...this.editorPref,
|
||||||
|
...editorPref,
|
||||||
|
};
|
||||||
if(this.queryHistDetails) {
|
if(this.queryHistDetails) {
|
||||||
this.queryHistDetails.setEditorPref(this.editorPref);
|
this.queryHistDetails.setEditorPref(this.editorPref);
|
||||||
}
|
}
|
||||||
@ -63,6 +75,7 @@ export default class QueryHistory {
|
|||||||
|
|
||||||
this.queryHistDetails = new QueryHistoryDetails($histDetails);
|
this.queryHistDetails = new QueryHistoryDetails($histDetails);
|
||||||
this.queryHistDetails.setEditorPref(this.editorPref);
|
this.queryHistDetails.setEditorPref(this.editorPref);
|
||||||
|
this.queryHistDetails.onCopyToEditorClick(this.onCopyToEditorHandler);
|
||||||
this.queryHistDetails.render();
|
this.queryHistDetails.render();
|
||||||
|
|
||||||
this.queryHistEntries = new QueryHistoryEntries($histEntries);
|
this.queryHistEntries = new QueryHistoryEntries($histEntries);
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import CodeMirror from 'bundled_codemirror';
|
import CodeMirror from 'bundled_codemirror';
|
||||||
import clipboard from 'sources/selection/clipboard';
|
import clipboard from 'sources/selection/clipboard';
|
||||||
import moment from 'moment';
|
import gettext from 'sources/gettext';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import _ from 'underscore';
|
||||||
|
|
||||||
export default class QueryHistoryDetails {
|
export default class QueryHistoryDetails {
|
||||||
constructor(parentNode) {
|
constructor(parentNode) {
|
||||||
@ -10,9 +11,11 @@ export default class QueryHistoryDetails {
|
|||||||
this.timeout = null;
|
this.timeout = null;
|
||||||
this.isRendered = false;
|
this.isRendered = false;
|
||||||
this.sqlFontSize = null;
|
this.sqlFontSize = null;
|
||||||
|
this.onCopyToEditorHandler = ()=>{};
|
||||||
|
|
||||||
this.editorPref = {
|
this.editorPref = {
|
||||||
'sql_font_size': '1em',
|
'sql_font_size': '1em',
|
||||||
|
'copy_to_editor': true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,13 +34,21 @@ export default class QueryHistoryDetails {
|
|||||||
...editorPref,
|
...editorPref,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(this.query_codemirror) {
|
if(this.query_codemirror && !_.isUndefined(editorPref.sql_font_size)) {
|
||||||
$(this.query_codemirror.getWrapperElement()).css(
|
$(this.query_codemirror.getWrapperElement()).css(
|
||||||
'font-size',this.editorPref.sql_font_size
|
'font-size',this.editorPref.sql_font_size
|
||||||
);
|
);
|
||||||
|
|
||||||
this.query_codemirror.refresh();
|
this.query_codemirror.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.$copyToEditor && !_.isUndefined(editorPref.copy_to_editor)) {
|
||||||
|
if(editorPref.copy_to_editor) {
|
||||||
|
this.$copyToEditor.removeClass('d-none');
|
||||||
|
} else {
|
||||||
|
this.$copyToEditor.addClass('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseErrorMessage(message) {
|
parseErrorMessage(message) {
|
||||||
@ -47,7 +58,7 @@ export default class QueryHistoryDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return moment(date).format('M-D-YY HH:mm:ss');
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||||
}
|
}
|
||||||
|
|
||||||
copyAllHandler() {
|
copyAllHandler() {
|
||||||
@ -62,6 +73,10 @@ export default class QueryHistoryDetails {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCopyToEditorClick(onCopyToEditorHandler) {
|
||||||
|
this.onCopyToEditorHandler = onCopyToEditorHandler;
|
||||||
|
}
|
||||||
|
|
||||||
clearPreviousTimeout() {
|
clearPreviousTimeout() {
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
clearTimeout(this.timeout);
|
clearTimeout(this.timeout);
|
||||||
@ -71,11 +86,11 @@ export default class QueryHistoryDetails {
|
|||||||
|
|
||||||
updateCopyButton(copied) {
|
updateCopyButton(copied) {
|
||||||
if (copied) {
|
if (copied) {
|
||||||
this.$copyBtn.attr('class', 'was-copied');
|
this.$copyBtn.addClass('was-copied').removeClass('copy-all');
|
||||||
this.$copyBtn.text('Copied!');
|
this.$copyBtn.text('Copied!');
|
||||||
} else {
|
} else {
|
||||||
this.$copyBtn.attr('class', 'copy-all');
|
this.$copyBtn.addClass('copy-all').removeClass('was-copied');
|
||||||
this.$copyBtn.text('Copy All');
|
this.$copyBtn.text('Copy');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +152,8 @@ export default class QueryHistoryDetails {
|
|||||||
<div class='metadata-block'></div>
|
<div class='metadata-block'></div>
|
||||||
<div class='query-statement-block'>
|
<div class='query-statement-block'>
|
||||||
<div id='history-detail-query'>
|
<div id='history-detail-query'>
|
||||||
<button class='' tabindex=0 accesskey='y'></button>
|
<button class='btn-copy' tabindex=0 accesskey='y'></button>
|
||||||
|
<button class='btn-copy-editor copy-to-editor' tabindex=0 accesskey='y'>` + gettext('Copy to Query Editor') + `</button>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,8 +170,13 @@ export default class QueryHistoryDetails {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.$errMsgBlock = this.parentNode.find('.error-message-block');
|
this.$errMsgBlock = this.parentNode.find('.error-message-block');
|
||||||
this.$copyBtn = this.parentNode.find('#history-detail-query button');
|
this.$copyBtn = this.parentNode.find('#history-detail-query .btn-copy');
|
||||||
this.$copyBtn.off('click').on('click', this.copyAllHandler.bind(this));
|
this.$copyBtn.off('click').on('click', this.copyAllHandler.bind(this));
|
||||||
|
this.$copyToEditor = this.parentNode.find('#history-detail-query .btn-copy-editor');
|
||||||
|
this.$copyToEditor.off('click').on('click', () => {
|
||||||
|
this.onCopyToEditorHandler(this.entry.query);
|
||||||
|
});
|
||||||
|
this.$copyToEditor.addClass(this.editorPref.copy_to_editor?'':'d-none');
|
||||||
this.$metaData = this.parentNode.find('.metadata-block');
|
this.$metaData = this.parentNode.find('.metadata-block');
|
||||||
this.query_codemirror = CodeMirror(
|
this.query_codemirror = CodeMirror(
|
||||||
this.parentNode.find('#history-detail-query div')[0],
|
this.parentNode.find('#history-detail-query div')[0],
|
||||||
|
@ -21,26 +21,20 @@ export class QueryHistoryEntryDateGroup {
|
|||||||
return prefix;
|
return prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDateFormatted(momentToFormat) {
|
getDateFormatted(date) {
|
||||||
return momentToFormat.format(this.formatString);
|
return date.toLocaleDateString();
|
||||||
}
|
|
||||||
|
|
||||||
getDateMoment() {
|
|
||||||
return moment(this.date);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isDaysBefore(before) {
|
isDaysBefore(before) {
|
||||||
return (
|
return (
|
||||||
this.getDateFormatted(this.getDateMoment()) ===
|
this.getDateFormatted(this.date) ===
|
||||||
this.getDateFormatted(moment().subtract(before, 'days'))
|
this.getDateFormatted(moment().subtract(before, 'days').toDate())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return $(`<div class='query-group' data-key='${this.groupKey}'>
|
return $(`<div class='query-group' data-key='${this.groupKey}'>
|
||||||
<div class='date-label'>${this.getDatePrefix()}${this.getDateFormatted(
|
<div class='date-label'>${this.getDatePrefix()}${this.getDateFormatted(this.date)}</div>
|
||||||
this.getDateMoment()
|
|
||||||
)}</div>
|
|
||||||
<ul class='query-entries'></ul>
|
<ul class='query-entries'></ul>
|
||||||
</div>`);
|
</div>`);
|
||||||
}
|
}
|
||||||
@ -66,9 +60,13 @@ export class QueryHistoryItem {
|
|||||||
return moment(date).format('HH:mm:ss');
|
return moment(date).format('HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataKey() {
|
||||||
|
return this.formatDate(this.entry.start_time);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.$el = $(
|
this.$el = $(
|
||||||
`<li class='list-item' tabindex='0' data-key='${this.formatDate(this.entry.start_time)}'>
|
`<li class='list-item' tabindex='0' data-key='${this.dataKey()}'>
|
||||||
<div class='entry ${this.entry.status ? '' : 'error'}'>
|
<div class='entry ${this.entry.status ? '' : 'error'}'>
|
||||||
<div class='query'>${this.entry.query}</div>
|
<div class='query'>${this.entry.query}</div>
|
||||||
<div class='other-info'>
|
<div class='other-info'>
|
||||||
@ -98,15 +96,11 @@ export class QueryHistoryEntries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
let self = this;
|
|
||||||
|
|
||||||
if (!this.$selectedItem) {
|
if (!this.$selectedItem) {
|
||||||
this.setSelectedListItem(this.$el.find('.list-item').first());
|
this.setSelectedListItem(this.$el.find('.list-item').first());
|
||||||
}
|
}
|
||||||
|
this.$selectedItem.trigger('click');
|
||||||
setTimeout(() => {
|
this.$el[0].focus();
|
||||||
self.$selectedItem.trigger('click');
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isArrowDown(event) {
|
isArrowDown(event) {
|
||||||
@ -170,7 +164,8 @@ export class QueryHistoryEntries {
|
|||||||
}
|
}
|
||||||
$listItem.addClass('selected');
|
$listItem.addClass('selected');
|
||||||
this.$selectedItem = $listItem;
|
this.$selectedItem = $listItem;
|
||||||
this.$selectedItem[0].scrollIntoView(false);
|
|
||||||
|
this.$selectedItem[0].scrollIntoView({block: 'center'});
|
||||||
|
|
||||||
if (this.onSelectedChangeHandler) {
|
if (this.onSelectedChangeHandler) {
|
||||||
this.onSelectedChangeHandler(this.$selectedItem.data('entrydata'));
|
this.onSelectedChangeHandler(this.$selectedItem.data('entrydata'));
|
||||||
@ -200,13 +195,20 @@ export class QueryHistoryEntries {
|
|||||||
entry.start_time,
|
entry.start_time,
|
||||||
entryGroupKey
|
entryGroupKey
|
||||||
).render();
|
).render();
|
||||||
if (groups[groupIdx]) {
|
|
||||||
$groupEl.insertBefore(groups[groupIdx]);
|
let i=0;
|
||||||
} else {
|
while(i<groupsKeys.length){
|
||||||
this.$el.prepend($groupEl);
|
if(entryGroupKey > groupsKeys[i]) {
|
||||||
|
$groupEl.insertBefore(groups[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if(i == groupsKeys.length) {
|
||||||
|
this.$el.append($groupEl);
|
||||||
}
|
}
|
||||||
} else if (groupIdx >= 0) {
|
} else if (groupIdx >= 0) {
|
||||||
/* if groups present, but this is a new one */
|
/* if the group is present */
|
||||||
$groupEl = $(groups[groupIdx]);
|
$groupEl = $(groups[groupIdx]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import simplejson as json
|
|||||||
from flask import Response, url_for, render_template, session, request, \
|
from flask import Response, url_for, render_template, session, request, \
|
||||||
current_app
|
current_app
|
||||||
from flask_babelex import gettext
|
from flask_babelex import gettext
|
||||||
from flask_security import login_required
|
from flask_security import login_required, current_user
|
||||||
|
|
||||||
from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT
|
from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT
|
||||||
from pgadmin.misc.file_manager import Filemanager
|
from pgadmin.misc.file_manager import Filemanager
|
||||||
@ -42,6 +42,7 @@ from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
|
|||||||
from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \
|
from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \
|
||||||
read_file_generator
|
read_file_generator
|
||||||
from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
|
from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
|
||||||
|
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
||||||
|
|
||||||
MODULE_NAME = 'sqleditor'
|
MODULE_NAME = 'sqleditor'
|
||||||
|
|
||||||
@ -113,7 +114,10 @@ class SqlEditorModule(PgAdminModule):
|
|||||||
'sqleditor.query_tool_download',
|
'sqleditor.query_tool_download',
|
||||||
'sqleditor.connection_status',
|
'sqleditor.connection_status',
|
||||||
'sqleditor.get_filter_data',
|
'sqleditor.get_filter_data',
|
||||||
'sqleditor.set_filter_data'
|
'sqleditor.set_filter_data',
|
||||||
|
'sqleditor.get_query_history',
|
||||||
|
'sqleditor.add_query_history',
|
||||||
|
'sqleditor.clear_query_history',
|
||||||
]
|
]
|
||||||
|
|
||||||
def register_preferences(self):
|
def register_preferences(self):
|
||||||
@ -1504,3 +1508,64 @@ def set_filter_data(trans_id):
|
|||||||
request=request,
|
request=request,
|
||||||
trans_id=trans_id
|
trans_id=trans_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route(
|
||||||
|
'/query_history/<int:trans_id>',
|
||||||
|
methods=["POST"], endpoint='add_query_history'
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
def add_query_history(trans_id):
|
||||||
|
"""
|
||||||
|
This method adds to query history for user/server/database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sid: server id
|
||||||
|
did: database id
|
||||||
|
"""
|
||||||
|
|
||||||
|
status, error_msg, conn, trans_obj, session_ob = \
|
||||||
|
check_transaction_status(trans_id)
|
||||||
|
|
||||||
|
return QueryHistory.save(current_user.id, trans_obj.sid, conn.db,
|
||||||
|
request=request)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route(
|
||||||
|
'/query_history/<int:trans_id>',
|
||||||
|
methods=["DELETE"], endpoint='clear_query_history'
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
def clear_query_history(trans_id):
|
||||||
|
"""
|
||||||
|
This method returns clears history for user/server/database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sid: server id
|
||||||
|
did: database id
|
||||||
|
"""
|
||||||
|
|
||||||
|
status, error_msg, conn, trans_obj, session_ob = \
|
||||||
|
check_transaction_status(trans_id)
|
||||||
|
|
||||||
|
return QueryHistory.clear(current_user.id, trans_obj.sid, conn.db)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route(
|
||||||
|
'/query_history/<int:trans_id>',
|
||||||
|
methods=["GET"], endpoint='get_query_history'
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
def get_query_history(trans_id):
|
||||||
|
"""
|
||||||
|
This method returns query history for user/server/database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sid: server id
|
||||||
|
did: database id
|
||||||
|
"""
|
||||||
|
|
||||||
|
status, error_msg, conn, trans_obj, session_ob = \
|
||||||
|
check_transaction_status(trans_id)
|
||||||
|
|
||||||
|
return QueryHistory.get(current_user.id, trans_obj.sid, conn.db)
|
||||||
|
@ -193,11 +193,11 @@ define('tools.querytool', [
|
|||||||
});
|
});
|
||||||
|
|
||||||
sql_panel.load(main_docker);
|
sql_panel.load(main_docker);
|
||||||
var sql_panel_obj = main_docker.addPanel('sql_panel', wcDocker.DOCK.TOP);
|
self.sql_panel_obj = main_docker.addPanel('sql_panel', wcDocker.DOCK.TOP);
|
||||||
|
|
||||||
var text_container = $('<textarea id="sql_query_tool" tabindex: "-1"></textarea>');
|
var text_container = $('<textarea id="sql_query_tool" tabindex: "-1"></textarea>');
|
||||||
var output_container = $('<div id="output-panel" tabindex: "0"></div>').append(text_container);
|
var output_container = $('<div id="output-panel" tabindex: "0"></div>').append(text_container);
|
||||||
sql_panel_obj.$container.find('.pg-panel-content').append(output_container);
|
self.sql_panel_obj.$container.find('.pg-panel-content').append(output_container);
|
||||||
|
|
||||||
self.query_tool_obj = CodeMirror.fromTextArea(text_container.get(0), {
|
self.query_tool_obj = CodeMirror.fromTextArea(text_container.get(0), {
|
||||||
tabindex: '0',
|
tabindex: '0',
|
||||||
@ -222,7 +222,7 @@ define('tools.querytool', [
|
|||||||
|
|
||||||
// Refresh Code mirror on SQL panel resize to
|
// Refresh Code mirror on SQL panel resize to
|
||||||
// display its value properly
|
// display its value properly
|
||||||
sql_panel_obj.on(wcDocker.EVENT.RESIZE_ENDED, function() {
|
self.sql_panel_obj.on(wcDocker.EVENT.RESIZE_ENDED, function() {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if (self && self.query_tool_obj) {
|
if (self && self.query_tool_obj) {
|
||||||
self.query_tool_obj.refresh();
|
self.query_tool_obj.refresh();
|
||||||
@ -312,8 +312,8 @@ define('tools.querytool', [
|
|||||||
geometry_viewer.load(main_docker);
|
geometry_viewer.load(main_docker);
|
||||||
|
|
||||||
// Add all the panels to the docker
|
// Add all the panels to the docker
|
||||||
self.scratch_panel = main_docker.addPanel('scratch', wcDocker.DOCK.RIGHT, sql_panel_obj);
|
self.scratch_panel = main_docker.addPanel('scratch', wcDocker.DOCK.RIGHT, self.sql_panel_obj);
|
||||||
self.history_panel = main_docker.addPanel('history', wcDocker.DOCK.STACKED, sql_panel_obj);
|
self.history_panel = main_docker.addPanel('history', wcDocker.DOCK.STACKED, self.sql_panel_obj);
|
||||||
self.data_output_panel = main_docker.addPanel('data_output', wcDocker.DOCK.BOTTOM);
|
self.data_output_panel = main_docker.addPanel('data_output', wcDocker.DOCK.BOTTOM);
|
||||||
self.explain_panel = main_docker.addPanel('explain', wcDocker.DOCK.STACKED, self.data_output_panel);
|
self.explain_panel = main_docker.addPanel('explain', wcDocker.DOCK.STACKED, self.data_output_panel);
|
||||||
self.messages_panel = main_docker.addPanel('messages', wcDocker.DOCK.STACKED, self.data_output_panel);
|
self.messages_panel = main_docker.addPanel('messages', wcDocker.DOCK.STACKED, self.data_output_panel);
|
||||||
@ -1309,13 +1309,51 @@ define('tools.querytool', [
|
|||||||
|
|
||||||
if(!self.historyComponent) {
|
if(!self.historyComponent) {
|
||||||
self.historyComponent = new QueryHistory($('#history_grid'), self.history_collection);
|
self.historyComponent = new QueryHistory($('#history_grid'), self.history_collection);
|
||||||
|
|
||||||
|
/* Copy query to query editor, set the focus to editor and move cursor to end */
|
||||||
|
self.historyComponent.onCopyToEditorClick((query)=>{
|
||||||
|
self.query_tool_obj.setValue(query);
|
||||||
|
self.sql_panel_obj.focus();
|
||||||
|
setTimeout(() => {
|
||||||
|
self.query_tool_obj.focus();
|
||||||
|
self.query_tool_obj.setCursor(self.query_tool_obj.lineCount(), 0);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
self.historyComponent.render();
|
self.historyComponent.render();
|
||||||
|
|
||||||
|
self.history_panel.off(wcDocker.EVENT.VISIBILITY_CHANGED);
|
||||||
|
self.history_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
|
||||||
|
if (self.history_panel.isVisible()) {
|
||||||
|
setTimeout(()=>{
|
||||||
|
self.historyComponent.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.history_panel.off(wcDocker.EVENT.VISIBILITY_CHANGED);
|
// Make ajax call to get history data except view/edit data
|
||||||
self.history_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
|
if(self.handler.is_query_tool) {
|
||||||
self.historyComponent.focus();
|
$.ajax({
|
||||||
});
|
url: url_for('sqleditor.get_query_history', {
|
||||||
|
'trans_id': self.handler.transId,
|
||||||
|
}),
|
||||||
|
method: 'GET',
|
||||||
|
contentType: 'application/json',
|
||||||
|
})
|
||||||
|
.done(function(res) {
|
||||||
|
res.data.result.map((entry) => {
|
||||||
|
let newEntry = JSON.parse(entry);
|
||||||
|
newEntry.start_time = new Date(newEntry.start_time);
|
||||||
|
self.history_collection.add(newEntry);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
/* history fetch fail should not affect query tool */
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.historyComponent.setEditorPref({'copy_to_editor':false});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Callback function for Add New Row button click.
|
// Callback function for Add New Row button click.
|
||||||
@ -1637,11 +1675,26 @@ define('tools.querytool', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
alertify.confirm(gettext('Clear history'),
|
alertify.confirm(gettext('Clear history'),
|
||||||
gettext('Are you sure you wish to clear the history?'),
|
gettext('Are you sure you wish to clear the history?') + '</br>' +
|
||||||
|
gettext('This will remove all of your query history from this and other sessions for this database.'),
|
||||||
function() {
|
function() {
|
||||||
if (self.history_collection) {
|
if (self.history_collection) {
|
||||||
self.history_collection.reset();
|
self.history_collection.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(self.handler.is_query_tool) {
|
||||||
|
$.ajax({
|
||||||
|
url: url_for('sqleditor.clear_query_history', {
|
||||||
|
'trans_id': self.handler.transId,
|
||||||
|
}),
|
||||||
|
method: 'DELETE',
|
||||||
|
contentType: 'application/json',
|
||||||
|
})
|
||||||
|
.done(function() {})
|
||||||
|
.fail(function() {
|
||||||
|
/* history clear fail should not affect query tool */
|
||||||
|
});
|
||||||
|
}
|
||||||
setTimeout(() => { self.query_tool_obj.focus(); }, 200);
|
setTimeout(() => { self.query_tool_obj.focus(); }, 200);
|
||||||
},
|
},
|
||||||
function() {
|
function() {
|
||||||
@ -2573,14 +2626,34 @@ define('tools.querytool', [
|
|||||||
self.query_start_time,
|
self.query_start_time,
|
||||||
new Date());
|
new Date());
|
||||||
}
|
}
|
||||||
self.gridView.history_collection.add({
|
|
||||||
|
let hist_entry = {
|
||||||
'status': status,
|
'status': status,
|
||||||
'start_time': self.query_start_time,
|
'start_time': self.query_start_time,
|
||||||
'query': self.query,
|
'query': self.query,
|
||||||
'row_affected': self.rows_affected,
|
'row_affected': self.rows_affected,
|
||||||
'total_time': self.total_time,
|
'total_time': self.total_time,
|
||||||
'message': msg,
|
'message': msg,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
/* Make ajax call to save the history data
|
||||||
|
* Do not bother query tool if failed to save
|
||||||
|
* Not applicable for view/edit data
|
||||||
|
*/
|
||||||
|
if(self.is_query_tool) {
|
||||||
|
$.ajax({
|
||||||
|
url: url_for('sqleditor.add_query_history', {
|
||||||
|
'trans_id': self.transId,
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(hist_entry),
|
||||||
|
})
|
||||||
|
.done(function() {})
|
||||||
|
.fail(function() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.gridView.history_collection.add(hist_entry);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@
|
|||||||
height: 0;
|
height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.copy-all, .was-copied {
|
.copy-all, .was-copied, .copy-to-editor {
|
||||||
float: left;
|
float: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
105
web/pgadmin/tools/sqleditor/tests/test_editor_history.py
Normal file
105
web/pgadmin/tools/sqleditor/tests/test_editor_history.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
##########################################################################
|
||||||
|
#
|
||||||
|
# pgAdmin 4 - PostgreSQL Tools
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 - 2019, 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
|
||||||
|
|
||||||
|
|
||||||
|
class TestEditorHistory(BaseTestGenerator):
|
||||||
|
""" This class will test the query tool polling. """
|
||||||
|
scenarios = [
|
||||||
|
('When first query is hit',
|
||||||
|
dict(
|
||||||
|
entry="""{
|
||||||
|
query: 'first sql statement',
|
||||||
|
start_time: '2017-05-03T14:03:15.150Z',
|
||||||
|
status: true,
|
||||||
|
row_affected: 12345,
|
||||||
|
total_time: '14 msec',
|
||||||
|
message: 'something important ERROR: message
|
||||||
|
from first sql query',
|
||||||
|
}""",
|
||||||
|
clear=False,
|
||||||
|
expected_len=1
|
||||||
|
)),
|
||||||
|
('When second query is hit',
|
||||||
|
dict(
|
||||||
|
entry="""{
|
||||||
|
query: 'second sql statement',
|
||||||
|
start_time: '2016-04-03T14:03:15.99Z',
|
||||||
|
status: true,
|
||||||
|
row_affected: 12345,
|
||||||
|
total_time: '14 msec',
|
||||||
|
message: 'something important ERROR: message from
|
||||||
|
second sql query',
|
||||||
|
}""",
|
||||||
|
clear=False,
|
||||||
|
expected_len=2
|
||||||
|
)),
|
||||||
|
('When cleared',
|
||||||
|
dict(
|
||||||
|
clear=True,
|
||||||
|
expected_len=0
|
||||||
|
))
|
||||||
|
]
|
||||||
|
|
||||||
|
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
|
||||||
|
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}'.format(
|
||||||
|
utils.SERVER_GROUP, self.server_id, self.db_id)
|
||||||
|
response = self.tester.post(url)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
response_data = json.loads(response.data.decode('utf-8'))
|
||||||
|
self.trans_id = response_data['data']['gridTransId']
|
||||||
|
|
||||||
|
def runTest(self):
|
||||||
|
url = '/sqleditor/query_history/{0}'.format(self.trans_id)
|
||||||
|
|
||||||
|
if not self.clear:
|
||||||
|
response = self.tester.post(url, data=self.entry)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.tester.get(url)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
response_data = json.loads(response.data.decode('utf-8'))
|
||||||
|
self.assertEquals(len(response_data['data']['result']),
|
||||||
|
self.expected_len)
|
||||||
|
else:
|
||||||
|
response = self.tester.delete(url)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.tester.get(url)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
response_data = json.loads(response.data.decode('utf-8'))
|
||||||
|
self.assertEquals(len(response_data['data']['result']),
|
||||||
|
self.expected_len)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# Disconnect the database
|
||||||
|
database_utils.disconnect_database(self, self.server_id, self.db_id)
|
137
web/pgadmin/tools/sqleditor/utils/query_history.py
Normal file
137
web/pgadmin/tools/sqleditor/utils/query_history.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
from pgadmin.utils.ajax import make_json_response
|
||||||
|
from pgadmin.model import db, QueryHistoryModel
|
||||||
|
from config import MAX_QUERY_HIST_STORED
|
||||||
|
|
||||||
|
|
||||||
|
class QueryHistory:
|
||||||
|
@staticmethod
|
||||||
|
def get(uid, sid, dbname):
|
||||||
|
|
||||||
|
result = db.session \
|
||||||
|
.query(QueryHistoryModel.query_info) \
|
||||||
|
.filter(QueryHistoryModel.uid == uid,
|
||||||
|
QueryHistoryModel.sid == sid,
|
||||||
|
QueryHistoryModel.dbname == dbname) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
return make_json_response(
|
||||||
|
data={
|
||||||
|
'status': True,
|
||||||
|
'msg': '',
|
||||||
|
'result': [rec.query_info for rec in result]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_history_dbname(uid, sid, old_dbname, new_dbname):
|
||||||
|
try:
|
||||||
|
db.session \
|
||||||
|
.query(QueryHistoryModel) \
|
||||||
|
.filter(QueryHistoryModel.uid == uid,
|
||||||
|
QueryHistoryModel.sid == sid,
|
||||||
|
QueryHistoryModel.dbname == old_dbname) \
|
||||||
|
.update({QueryHistoryModel.dbname: new_dbname})
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
# do not affect query execution if history clear fails
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save(uid, sid, dbname, request):
|
||||||
|
try:
|
||||||
|
max_srno = db.session\
|
||||||
|
.query(db.func.max(QueryHistoryModel.srno)) \
|
||||||
|
.filter(QueryHistoryModel.uid == uid,
|
||||||
|
QueryHistoryModel.sid == sid,
|
||||||
|
QueryHistoryModel.dbname == dbname)\
|
||||||
|
.scalar()
|
||||||
|
|
||||||
|
# if no records present
|
||||||
|
if max_srno is None:
|
||||||
|
new_srno = 1
|
||||||
|
else:
|
||||||
|
new_srno = max_srno + 1
|
||||||
|
|
||||||
|
# last updated flag is used to recognise the last
|
||||||
|
# inserted/updated record.
|
||||||
|
# It is helpful to cycle the records
|
||||||
|
last_updated_rec = db.session.query(QueryHistoryModel) \
|
||||||
|
.filter(QueryHistoryModel.uid == uid,
|
||||||
|
QueryHistoryModel.sid == sid,
|
||||||
|
QueryHistoryModel.dbname == dbname,
|
||||||
|
QueryHistoryModel.last_updated_flag == 'Y') \
|
||||||
|
.first()
|
||||||
|
|
||||||
|
# there should be a last updated record
|
||||||
|
# if not present start from sr no 1
|
||||||
|
if last_updated_rec is not None:
|
||||||
|
last_updated_rec.last_updated_flag = 'N'
|
||||||
|
|
||||||
|
# if max limit reached then recycle
|
||||||
|
if new_srno > MAX_QUERY_HIST_STORED:
|
||||||
|
new_srno = (
|
||||||
|
last_updated_rec.srno % MAX_QUERY_HIST_STORED) + 1
|
||||||
|
else:
|
||||||
|
new_srno = 1
|
||||||
|
|
||||||
|
# if the limit is lowered and number of records present is
|
||||||
|
# more, then cleanup
|
||||||
|
if max_srno > MAX_QUERY_HIST_STORED:
|
||||||
|
db.session.query(QueryHistoryModel)\
|
||||||
|
.filter(QueryHistoryModel.uid == uid,
|
||||||
|
QueryHistoryModel.sid == sid,
|
||||||
|
QueryHistoryModel.dbname == dbname,
|
||||||
|
QueryHistoryModel.srno >
|
||||||
|
MAX_QUERY_HIST_STORED)\
|
||||||
|
.delete()
|
||||||
|
|
||||||
|
history_entry = QueryHistoryModel(
|
||||||
|
srno=new_srno, uid=uid, sid=sid, dbname=dbname,
|
||||||
|
query_info=request.data, last_updated_flag='Y')
|
||||||
|
|
||||||
|
db.session.merge(history_entry)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
# do not affect query execution if history saving fails
|
||||||
|
|
||||||
|
return make_json_response(
|
||||||
|
data={
|
||||||
|
'status': True,
|
||||||
|
'msg': 'Success',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clear_history(uid, sid, dbname=None):
|
||||||
|
try:
|
||||||
|
if dbname is not None:
|
||||||
|
db.session.query(QueryHistoryModel) \
|
||||||
|
.filter(QueryHistoryModel.uid == uid,
|
||||||
|
QueryHistoryModel.sid == sid,
|
||||||
|
QueryHistoryModel.dbname == dbname) \
|
||||||
|
.delete()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
db.session.query(QueryHistoryModel) \
|
||||||
|
.filter(QueryHistoryModel.uid == uid,
|
||||||
|
QueryHistoryModel.sid == sid)\
|
||||||
|
.delete()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
# do not affect query execution if history clear fails
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clear(uid, sid, dbname=None):
|
||||||
|
QueryHistory.clear_history(uid, sid, dbname)
|
||||||
|
return make_json_response(
|
||||||
|
data={
|
||||||
|
'status': True,
|
||||||
|
'msg': 'Success',
|
||||||
|
}
|
||||||
|
)
|
@ -18,7 +18,6 @@ import moment from 'moment';
|
|||||||
describe('QueryHistory', () => {
|
describe('QueryHistory', () => {
|
||||||
let historyCollection;
|
let historyCollection;
|
||||||
let historyWrapper;
|
let historyWrapper;
|
||||||
let sqlEditorPref = {sql_font_size: '1.5em'};
|
|
||||||
let historyComponent;
|
let historyComponent;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -70,6 +69,7 @@ describe('QueryHistory', () => {
|
|||||||
|
|
||||||
historyCollection = new HistoryCollection(historyObjects);
|
historyCollection = new HistoryCollection(historyObjects);
|
||||||
historyComponent = new QueryHistory(historyWrapper, historyCollection);
|
historyComponent = new QueryHistory(historyWrapper, historyCollection);
|
||||||
|
historyComponent.onCopyToEditorClick(()=>{});
|
||||||
historyComponent.render();
|
historyComponent.render();
|
||||||
|
|
||||||
queryEntries = historyWrapper.find('#query_list .list-item');
|
queryEntries = historyWrapper.find('#query_list .list-item');
|
||||||
@ -92,8 +92,9 @@ describe('QueryHistory', () => {
|
|||||||
expect($(queryEntries[1]).find('.timestamp').text()).toBe('01:33:05');
|
expect($(queryEntries[1]).find('.timestamp').text()).toBe('01:33:05');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the most recent query as selected', () => {
|
it('renders the most recent query as selected', (done) => {
|
||||||
expect($(queryEntries[0]).hasClass('selected')).toBeTruthy();
|
expect($(queryEntries[0]).hasClass('selected')).toBeTruthy();
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the older query as not selected', () => {
|
it('renders the older query as not selected', () => {
|
||||||
@ -103,19 +104,26 @@ describe('QueryHistory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('the historydetails panel', () => {
|
describe('the historydetails panel', () => {
|
||||||
let copyAllButton;
|
let copyAllButton, copyEditorButton;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
copyAllButton = () => queryDetail.find('#history-detail-query > button');
|
copyAllButton = () => queryDetail.find('#history-detail-query .btn-copy');
|
||||||
|
copyEditorButton = () => queryDetail.find('#history-detail-query .btn-copy-editor');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change preferences', ()=>{
|
it('should change preference font size', ()=>{
|
||||||
historyComponent.setEditorPref(sqlEditorPref);
|
historyComponent.setEditorPref({sql_font_size: '1.5em'});
|
||||||
expect(queryDetail.find('#history-detail-query .CodeMirror').attr('style')).toBe('font-size: 1.5em;');
|
expect(queryDetail.find('#history-detail-query .CodeMirror').attr('style')).toBe('font-size: 1.5em;');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should change preference copy to editor false', ()=>{
|
||||||
|
historyComponent.setEditorPref({copy_to_editor: false});
|
||||||
|
expect($(queryDetail.find('#history-detail-query .btn-copy-editor')).hasClass('d-none')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('displays the formatted timestamp', () => {
|
it('displays the formatted timestamp', () => {
|
||||||
expect(queryDetail.text()).toContain('6-3-17 14:03:15');
|
let firstDate = new Date(2017, 5, 3, 14, 3, 15, 150);
|
||||||
|
expect(queryDetail.text()).toContain(firstDate.toLocaleDateString() + ' ' + firstDate.toLocaleTimeString());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the number of rows affected', () => {
|
it('displays the number of rows affected', () => {
|
||||||
@ -141,7 +149,7 @@ describe('QueryHistory', () => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the "Copy All" button is clicked', () => {
|
describe('when the "Copy" button is clicked', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(clipboard, 'copyTextToClipboard');
|
spyOn(clipboard, 'copyTextToClipboard');
|
||||||
copyAllButton().trigger('click');
|
copyAllButton().trigger('click');
|
||||||
@ -161,8 +169,8 @@ describe('QueryHistory', () => {
|
|||||||
jasmine.clock().uninstall();
|
jasmine.clock().uninstall();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have text \'Copy All\'', () => {
|
it('should have text \'Copy\'', () => {
|
||||||
expect(copyAllButton().text()).toBe('Copy All');
|
expect(copyAllButton().text()).toBe('Copy');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have the class \'was-copied\'', () => {
|
it('should not have the class \'was-copied\'', () => {
|
||||||
@ -193,8 +201,8 @@ describe('QueryHistory', () => {
|
|||||||
jasmine.clock().tick(1501);
|
jasmine.clock().tick(1501);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the button text back to \'Copy All\'', () => {
|
it('should change the button text back to \'Copy\'', () => {
|
||||||
expect(copyAllButton().text()).toBe('Copy All');
|
expect(copyAllButton().text()).toBe('Copy');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -224,14 +232,25 @@ describe('QueryHistory', () => {
|
|||||||
jasmine.clock().tick(1501);
|
jasmine.clock().tick(1501);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the button text back to \'Copy All\'', () => {
|
it('should change the button text back to \'Copy\'', () => {
|
||||||
expect(copyAllButton().text()).toBe('Copy All');
|
expect(copyAllButton().text()).toBe('Copy');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when the "Copy to query editor" button is clicked', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(historyComponent.queryHistDetails, 'onCopyToEditorHandler').and.callThrough();
|
||||||
|
copyEditorButton().trigger('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends the query to the onCopyToEditorHandler', () => {
|
||||||
|
expect(historyComponent.queryHistDetails.onCopyToEditorHandler).toHaveBeenCalledWith('first sql statement');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when the query failed', () => {
|
describe('when the query failed', () => {
|
||||||
let failedEntry;
|
let failedEntry;
|
||||||
|
|
||||||
@ -310,12 +329,17 @@ describe('QueryHistory', () => {
|
|||||||
|
|
||||||
describe('when several days of queries were executed', () => {
|
describe('when several days of queries were executed', () => {
|
||||||
let queryEntryDateGroups;
|
let queryEntryDateGroups;
|
||||||
|
let dateToday, dateYest, dateBeforeYest;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jasmine.clock().install();
|
jasmine.clock().install();
|
||||||
const mockedCurrentDate = moment('2017-07-01 13:30:00');
|
const mockedCurrentDate = moment('2017-07-01 13:30:00');
|
||||||
jasmine.clock().mockDate(mockedCurrentDate.toDate());
|
jasmine.clock().mockDate(mockedCurrentDate.toDate());
|
||||||
|
|
||||||
|
dateToday = mockedCurrentDate.toDate();
|
||||||
|
dateYest = mockedCurrentDate.clone().subtract(1, 'days').toDate();
|
||||||
|
dateBeforeYest = mockedCurrentDate.clone().subtract(3, 'days').toDate();
|
||||||
|
|
||||||
const historyObjects = [{
|
const historyObjects = [{
|
||||||
query: 'first today sql statement',
|
query: 'first today sql statement',
|
||||||
start_time: mockedCurrentDate.toDate(),
|
start_time: mockedCurrentDate.toDate(),
|
||||||
@ -371,9 +395,9 @@ describe('QueryHistory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has title above', () => {
|
it('has title above', () => {
|
||||||
expect($(queryEntryDateGroups[0]).find('.date-label').text()).toBe('Today - Jul 01 2017');
|
expect($(queryEntryDateGroups[0]).find('.date-label').text()).toBe('Today - ' + dateToday.toLocaleDateString());
|
||||||
expect($(queryEntryDateGroups[1]).find('.date-label').text()).toBe('Yesterday - Jun 30 2017');
|
expect($(queryEntryDateGroups[1]).find('.date-label').text()).toBe('Yesterday - ' + dateYest.toLocaleDateString());
|
||||||
expect($(queryEntryDateGroups[2]).find('.date-label').text()).toBe('Jun 28 2017');
|
expect($(queryEntryDateGroups[2]).find('.date-label').text()).toBe(dateBeforeYest.toLocaleDateString());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user