Remove the large and unnecessary dependency on React and 87 other related libraries. Fixes #4018

This commit is contained in:
Aditya Toshniwal 2019-03-07 10:51:59 +00:00 committed by Dave Page
parent d7bf6ec69f
commit 4b895941b3
31 changed files with 792 additions and 3465 deletions

View File

@ -10,6 +10,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 #4018 <https://redmine.postgresql.org/issues/4018>`_ - Remove the large and unnecessary dependency on React and 87 other related libraries.
Bug fixes
*********

File diff suppressed because it is too large Load Diff

View File

@ -16,17 +16,14 @@ module.exports = {
},
'extends': [
'eslint:recommended',
"plugin:react/recommended",
],
'parserOptions': {
'ecmaFeatures': {
'experimentalObjectRestSpread': true,
'jsx': true
},
'sourceType': 'module'
},
'plugins': [
'react'
],
'globals': {
'_': true,

View File

@ -28,14 +28,13 @@ module.exports = function (config) {
* filename: null, // if no value is provided the sourcemap is inlined
*/
filename: '[name].js.map',
test: /\.jsx?$/i, // process .js and .jsx files only
test: /\.js$/i, // process .js files only
}),
],
files: [
'pgadmin/static/bundle/slickgrid.js',
{pattern: 'pgadmin/static/**/*.js', included: false, watched: true},
{pattern: 'pgadmin/browser/static/js/**/*.js', included: false, watched: true},
'regression/javascript/**/*.jsx',
{pattern: 'regression/javascript/**/*.js', watched: true},
],
@ -48,9 +47,8 @@ module.exports = function (config) {
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'pgadmin/**/js/**/*.js[x]?': ['sourcemap'],
'pgadmin/**/js/**/*.js?': ['sourcemap'],
'regression/javascript/**/*.js': ['webpack', 'sourcemap'],
'regression/javascript/**/*.jsx': ['webpack', 'sourcemap'],
'pgadmin/static/bundle/slickgrid.js': ['webpack', 'sourcemap'],
},

View File

@ -5,22 +5,15 @@
"babel-core": "~6.24.0",
"babel-loader": "~7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-airbnb": "^2.4.0",
"babel-preset-es2015": "~6.24.0",
"babel-preset-react": "~6.23.0",
"cross-env": "^5.0.1",
"eclint": "^2.3.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-matchers": "^4.1.1",
"eslint": "3.19.0",
"eslint-plugin-react": "^6.10.3",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^0.11.2",
"image-webpack-loader": "^3.3.1",
"is-docker": "^1.1.0",
"jasmine-core": "~2.99.0",
"jasmine-enzyme": "~4.1.1",
"karma": "~2.0.0",
"karma-babel-preprocessor": "^7.0.0",
"karma-browserify": "~5.2.0",
@ -82,15 +75,13 @@
"moment-timezone": "^0.5.21",
"mousetrap": "^1.6.1",
"prop-types": "^15.5.10",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-split-pane": "^0.1.63",
"requirejs": "~2.3.3",
"select2": "^4.0.6-rc.1",
"shim-loader": "^1.0.1",
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.16",
"snapsvg": "^0.5.1",
"spectrum-colorpicker": "^1.8.0",
"split.js": "^1.5.10",
"sprintf-js": "^1.1.1",
"tablesorter": "^2.30.6",
"tempusdominus-bootstrap-4": "^5.1.2",
@ -102,7 +93,7 @@
"wkx": "^0.4.5"
},
"scripts": {
"linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js --ext .jsx .",
"linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js .",
"webpacker": "yarn run webpack --config webpack.config.js --progress",
"webpacker:watch": "yarn run webpack --config webpack.config.js --progress --watch",
"bundle:watch": "yarn run linter && yarn run webpacker:watch",

View File

@ -10,8 +10,8 @@
export default class HistoryCollection {
constructor(history_model) {
this.historyList = history_model;
this.onChange(() => {});
this.historyList = _.sortBy(history_model, o=>o.start_time);
this.onAdd(() => {});
}
length() {
@ -19,8 +19,10 @@ export default class HistoryCollection {
}
add(object) {
this.historyList.push(object);
this.onChangeHandler(this.historyList);
/* add object in sorted order */
let pushAt = _.sortedIndex(this.historyList, object, o=>o.start_time);
this.historyList.splice(pushAt, 0, object);
this.onAddHandler(object);
}
reset() {
@ -28,8 +30,8 @@ export default class HistoryCollection {
this.onResetHandler(this.historyList);
}
onChange(onChangeHandler) {
this.onChangeHandler = onChangeHandler;
onAdd(onAddHandler) {
this.onAddHandler = onAddHandler;
}
onReset(onResetHandler) {

View File

@ -0,0 +1,81 @@
import QueryHistoryDetails from './query_history_details';
import { QueryHistoryEntries } from './query_history_entries';
import Split from 'split.js';
import gettext from 'sources/gettext';
import $ from 'jquery';
export default class QueryHistory {
constructor(parentNode, histModel) {
this.parentNode = parentNode;
this.histCollection = histModel;
this.editorPref = {};
this.histCollection.onAdd(this.onAddEntry.bind(this));
this.histCollection.onReset(this.onResetEntries.bind(this));
}
focus() {
if (this.queryHistEntries) {
this.queryHistEntries.focus();
}
}
onAddEntry(entry) {
if (this.histCollection.length() == 1) {
this.render();
} else if (this.queryHistEntries) {
this.queryHistEntries.addEntry(entry);
}
}
onResetEntries() {
this.isRendered = false;
this.queryHistEntries = null;
this.queryHistDetails = null;
this.render();
}
setEditorPref(editorPref) {
this.editorPref = editorPref;
if(this.queryHistDetails) {
this.queryHistDetails.setEditorPref(this.editorPref);
}
}
render() {
if (this.histCollection.length() == 0) {
this.parentNode.empty()
.removeClass('d-flex')
.append(
`<div class='alert alert-info pg-panel-message'>${gettext(
'No history found'
)}</div>`
);
} else {
this.parentNode.empty().addClass('d-flex');
let $histEntries = $('<div><div>').appendTo(this.parentNode);
let $histDetails = $('<div><div>').appendTo(this.parentNode);
Split([$histEntries[0], $histDetails[0]], {
gutterSize: 1,
cursor: 'ew-resize',
});
this.queryHistDetails = new QueryHistoryDetails($histDetails);
this.queryHistDetails.setEditorPref(this.editorPref);
this.queryHistDetails.render();
this.queryHistEntries = new QueryHistoryEntries($histEntries);
this.queryHistEntries.onSelectedChange(
(entry => {
this.queryHistDetails.setEntry(entry);
}).bind(this)
);
this.queryHistEntries.render();
this.histCollection.historyList.map((entry)=>{
this.queryHistEntries.addEntry(entry);
});
}
}
}

View File

@ -0,0 +1,177 @@
import CodeMirror from 'bundled_codemirror';
import clipboard from 'sources/selection/clipboard';
import moment from 'moment';
import $ from 'jquery';
export default class QueryHistoryDetails {
constructor(parentNode) {
this.parentNode = parentNode;
this.isCopied = false;
this.timeout = null;
this.isRendered = false;
this.sqlFontSize = null;
this.editorPref = {
'sql_font_size': '1em',
};
}
setEntry(entry) {
this.entry = entry;
if (this.isRendered) {
this.selectiveRender();
} else {
this.render();
}
}
setEditorPref(editorPref={}) {
this.editorPref = {
...this.editorPref,
...editorPref,
};
if(this.query_codemirror) {
$(this.query_codemirror.getWrapperElement()).css(
'font-size',this.editorPref.sql_font_size
);
this.query_codemirror.refresh();
}
}
parseErrorMessage(message) {
return message.match(/ERROR:\s*([^\n\r]*)/i)
? message.match(/ERROR:\s*([^\n\r]*)/i)[1]
: message;
}
formatDate(date) {
return moment(date).format('M-D-YY HH:mm:ss');
}
copyAllHandler() {
clipboard.copyTextToClipboard(this.entry.query);
this.clearPreviousTimeout();
this.updateCopyButton(true);
this.timeout = setTimeout(() => {
this.updateCopyButton(false);
}, 1500);
}
clearPreviousTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
updateCopyButton(copied) {
if (copied) {
this.$copyBtn.attr('class', 'was-copied');
this.$copyBtn.text('Copied!');
} else {
this.$copyBtn.attr('class', 'copy-all');
this.$copyBtn.text('Copy All');
}
}
updateQueryMetaData() {
let itemTemplate = (data, description) => {
return `<div class='item'>
<span class='value'>${data}</span>
<span class='description'>${description}</span>
</div>`;
};
this.$metaData.empty().append(
`<div class='metadata'>
${itemTemplate(this.formatDate(this.entry.start_time), 'Date')}
${itemTemplate(
this.entry.row_affected.toLocaleString(),
'Rows Affected'
)}
${itemTemplate(this.entry.total_time, 'Duration')}
</div>`
);
}
updateMessageContent() {
this.$message_content
.empty()
.append(`<pre class='content-value'>${this.entry.message}</pre>`);
}
updateErrorMessage() {
if (!this.entry.status) {
this.$errMsgBlock.removeClass('d-none');
this.$errMsgBlock.empty().append(
`<div class='history-error-text'>
<span>Error Message</span> ${this.parseErrorMessage(
this.entry.message
)}
</div>`
);
} else {
this.$errMsgBlock.addClass('d-none');
this.$errMsgBlock.empty();
}
}
selectiveRender() {
this.updateErrorMessage();
this.updateCopyButton(false);
this.updateQueryMetaData();
this.query_codemirror.setValue(this.entry.query);
this.updateMessageContent();
}
render() {
if (this.entry) {
this.parentNode.empty().append(
`<div id='query_detail' class='query-detail'>
<div class='error-message-block'></div>
<div class='metadata-block'></div>
<div class='query-statement-block'>
<div id='history-detail-query'>
<button class='' tabindex=0 accesskey='y'></button>
<div></div>
</div>
</div>
<div>
<hr class='block-divider'/>
</div>
<div class='message-block'>
<div class='message'>
<div class='message-header'>Messages</div>
<div class='content'></div>
</div>
</div>
</div>`
);
this.$errMsgBlock = this.parentNode.find('.error-message-block');
this.$copyBtn = this.parentNode.find('#history-detail-query button');
this.$copyBtn.off('click').on('click', this.copyAllHandler.bind(this));
this.$metaData = this.parentNode.find('.metadata-block');
this.query_codemirror = CodeMirror(
this.parentNode.find('#history-detail-query div')[0],
{
tabindex: -1,
mode: 'text/x-pgsql',
readOnly: true,
}
);
$(this.query_codemirror.getWrapperElement()).css(
'font-size',this.editorPref.sql_font_size
);
this.$message_content = this.parentNode.find('.message-block .content');
this.isRendered = true;
this.selectiveRender();
}
}
}

View File

@ -0,0 +1,230 @@
import moment from 'moment';
import $ from 'jquery';
const ARROWUP = 38;
const ARROWDOWN = 40;
export class QueryHistoryEntryDateGroup {
constructor(date, groupKey) {
this.date = date;
this.formatString = 'MMM DD YYYY';
this.groupKey = groupKey;
}
getDatePrefix() {
let prefix = '';
if (this.isDaysBefore(0)) {
prefix = 'Today - ';
} else if (this.isDaysBefore(1)) {
prefix = 'Yesterday - ';
}
return prefix;
}
getDateFormatted(momentToFormat) {
return momentToFormat.format(this.formatString);
}
getDateMoment() {
return moment(this.date);
}
isDaysBefore(before) {
return (
this.getDateFormatted(this.getDateMoment()) ===
this.getDateFormatted(moment().subtract(before, 'days'))
);
}
render() {
return $(`<div class='query-group' data-key='${this.groupKey}'>
<div class='date-label'>${this.getDatePrefix()}${this.getDateFormatted(
this.getDateMoment()
)}</div>
<ul class='query-entries'></ul>
</div>`);
}
}
export class QueryHistoryItem {
constructor(entry) {
this.entry = entry;
this.$el = null;
this.onClickHandler = null;
}
onClick(onClickHandler) {
this.onClickHandler = onClickHandler;
if (this.$el) {
this.$el.off('click').on('click', e => {
this.onClickHandler($(e.currentTarget));
});
}
}
formatDate(date) {
return moment(date).format('HH:mm:ss');
}
render() {
this.$el = $(
`<li class='list-item' tabindex='0' data-key='${this.formatDate(this.entry.start_time)}'>
<div class='entry ${this.entry.status ? '' : 'error'}'>
<div class='query'>${this.entry.query}</div>
<div class='other-info'>
<div class='timestamp'>${this.formatDate(this.entry.start_time)}</div>
</div>
</div>
</li>`
)
.data('entrydata', this.entry)
.on('click', e => {
this.onClickHandler($(e.currentTarget));
});
}
}
export class QueryHistoryEntries {
constructor(parentNode) {
this.parentNode = parentNode;
this.$selectedItem = null;
this.groupKeyFormat = 'YYYY MM DD';
this.$el = null;
}
onSelectedChange(onSelectedChangeHandler) {
this.onSelectedChangeHandler = onSelectedChangeHandler;
}
focus() {
let self = this;
if (!this.$selectedItem) {
this.setSelectedListItem(this.$el.find('.list-item').first());
}
setTimeout(() => {
self.$selectedItem.trigger('click');
}, 500);
}
isArrowDown(event) {
return (event.keyCode || event.which) === ARROWDOWN;
}
isArrowUp(event) {
return (event.keyCode || event.which) === ARROWUP;
}
navigateUpAndDown(event) {
let arrowKeys = [ARROWUP, ARROWDOWN];
let key = event.keyCode || event.which;
if (arrowKeys.indexOf(key) > -1) {
event.preventDefault();
this.onKeyDownHandler(event);
return false;
}
return true;
}
onKeyDownHandler(event) {
if (this.isArrowDown(event)) {
if (this.$selectedItem.next().length > 0) {
this.setSelectedListItem(this.$selectedItem.next());
} else {
/* if last, jump to next group */
let $group = this.$selectedItem.closest('.query-group');
if ($group.next().length > 0) {
this.setSelectedListItem(
$group.next().find('.list-item').first()
);
}
}
} else if (this.isArrowUp(event)) {
if (this.$selectedItem.prev().length > 0) {
this.setSelectedListItem(this.$selectedItem.prev());
} else {
/* if first, jump to prev group */
let $group = this.$selectedItem.closest('.query-group');
if ($group.prev().length > 0) {
this.setSelectedListItem(
$group.prev().find('.list-item').last()
);
}
}
}
}
onSelectListItem(event) {
this.setSelectedListItem($(event.currentTarget));
}
dateAsGroupKey(date) {
return moment(date).format(this.groupKeyFormat);
}
setSelectedListItem($listItem) {
if (this.$selectedItem) {
this.$selectedItem.removeClass('selected');
}
$listItem.addClass('selected');
this.$selectedItem = $listItem;
this.$selectedItem[0].scrollIntoView(false);
if (this.onSelectedChangeHandler) {
this.onSelectedChangeHandler(this.$selectedItem.data('entrydata'));
}
}
addEntry(entry) {
/* Add the entry in respective date group in descending sorted order. */
let groups = this.$el.find('.query-group');
let groupsKeys = $.map(groups, group => {
return $(group).attr('data-key');
});
let entryGroupKey = this.dateAsGroupKey(entry.start_time);
let groupIdx = _.indexOf(groupsKeys, entryGroupKey);
let $groupEl = null;
/* if no groups present */
if (groups.length == 0) {
$groupEl = new QueryHistoryEntryDateGroup(
entry.start_time,
entryGroupKey
).render();
this.$el.prepend($groupEl);
} else if (groupIdx < 0 && groups.length != 0) {
/* if groups are present, but this is a new group */
$groupEl = new QueryHistoryEntryDateGroup(
entry.start_time,
entryGroupKey
).render();
if (groups[groupIdx]) {
$groupEl.insertBefore(groups[groupIdx]);
} else {
this.$el.prepend($groupEl);
}
} else if (groupIdx >= 0) {
/* if groups present, but this is a new one */
$groupEl = $(groups[groupIdx]);
}
let newItem = new QueryHistoryItem(entry);
newItem.onClick(this.setSelectedListItem.bind(this));
newItem.render();
$groupEl.find('.query-entries').prepend(newItem.$el);
this.setSelectedListItem(newItem.$el);
}
render() {
let self = this;
self.$el = $(`
<div id='query_list' class='query-history' tabindex='0'>
</div>
`).on('keydown', this.navigateUpAndDown.bind(this));
self.parentNode.empty().append(self.$el);
}
}

View File

@ -173,7 +173,9 @@ function updateUIPreferences(sqlEditor) {
sqlEditor.query_tool_obj.refresh();
/* Render history to reflect Font size change */
sqlEditor.render_history_grid();
sqlEditor.historyComponent.setEditorPref({
'sql_font_size' : sql_font_size,
});
}
export {updateUIPreferences};

View File

@ -1,66 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import $ from 'jquery';
import code_mirror from 'sources/../bundle/codemirror';
export default class CodeMirror extends React.Component {
constructor(props) {
super(props);
this.state = {
shouldHydrate: true,
};
}
componentDidMount() {
this.editor = code_mirror(this.container);
this.hydrateInterval = setInterval(this.hydrateWhenBecomesVisible.bind(this), 100);
this.hydrate(this.props);
}
componentWillUnmount() {
clearInterval(this.hydrateInterval);
}
componentWillReceiveProps(nextProps) {
this.hydrate(nextProps);
}
hydrateWhenBecomesVisible() {
const isVisible = $(this.container).is(':visible');
if (isVisible && this.state.shouldHydrate) {
this.hydrate(this.props);
this.setState({shouldHydrate: false});
} else if (!isVisible) {
this.setState({shouldHydrate: true});
}
}
hydrate(props) {
Object.keys(props.options || {}).forEach(key => this.editor.setOption(key, props.options[key]));
this.editor.setValue(props.value || '');
if(props.sqlFontSize) {
$(this.editor.getWrapperElement()).css('font-size', props.sqlFontSize);
}
this.editor.refresh();
}
render() {
return (
<div ref={(self) => this.container = self}/>
);
}
}

View File

@ -1,33 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import Shapes from '../../react_shapes';
export default class HistoryDetailMessage extends React.Component {
render() {
return (
<div className='message'>
<div className='message-header'>
Messages
</div>
<div className='content'>
<pre className='content-value'>
{this.props.historyEntry.message}
</pre>
</div>
</div>);
}
}
HistoryDetailMessage.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -1,42 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import moment from 'moment';
import Shapes from '../../react_shapes';
export default class HistoryDetailMetadata extends React.Component {
formatDate(date) {
return (moment(date).format('M-D-YY HH:mm:ss'));
}
queryMetaData(data, description) {
return <div className='item'>
<span className='value'>
{data}
</span>
<span className='description'>
{description}
</span>
</div>;
}
render() {
return <div className='metadata'>
{this.queryMetaData(this.formatDate(this.props.historyEntry.start_time), 'Date')}
{this.queryMetaData(this.props.historyEntry.row_affected.toLocaleString(), 'Rows Affected')}
{this.queryMetaData(this.props.historyEntry.total_time, 'Duration')}
</div>;
}
}
HistoryDetailMetadata.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -1,76 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import 'codemirror/mode/sql/sql';
import CodeMirror from './code_mirror';
import Shapes from '../../react_shapes';
import clipboard from '../../../js/selection/clipboard';
export default class HistoryDetailQuery extends React.Component {
constructor(props) {
super(props);
this.copyAllHandler = this.copyAllHandler.bind(this);
this.state = {isCopied: false};
this.timeout = undefined;
}
copyAllHandler() {
clipboard.copyTextToClipboard(this.props.historyEntry.query);
this.clearPreviousTimeout();
this.setState({isCopied: true});
this.timeout = setTimeout(() => {
this.setState({isCopied: false});
}, 1500);
}
clearPreviousTimeout() {
if (this.timeout !== undefined) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
}
copyButtonText() {
return this.state.isCopied ? 'Copied!' : 'Copy All';
}
copyButtonClass() {
return this.state.isCopied ? 'was-copied' : 'copy-all';
}
render() {
return (
<div id="history-detail-query">
<button className={this.copyButtonClass()}
tabIndex={0}
accessKey={'y'}
onClick={this.copyAllHandler}>{this.copyButtonText()}</button>
<CodeMirror
value={this.props.historyEntry.query}
options={{
tabindex: -1,
mode: 'text/x-pgsql',
readOnly: true,
}}
sqlFontSize= {this.props.sqlEditorPref.sql_font_size}
/>
</div>);
}
}
HistoryDetailQuery.propTypes = {
historyEntry: Shapes.historyDetail,
sqlEditorPref: Shapes.sqlEditorPrefObj,
};

View File

@ -1,32 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import Shapes from '../../react_shapes';
export default class HistoryErrorMessage extends React.Component {
parseErrorMessage(message) {
return message.match(/ERROR:\s*([^\n\r]*)/i) ?
message.match(/ERROR:\s*([^\n\r]*)/i)[1] :
message;
}
render() {
return (
<div className='history-error-text'>
<span>Error Message</span> {this.parseErrorMessage(this.props.historyEntry.message)}
</div>);
}
}
HistoryErrorMessage.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -1,121 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
/* eslint-disable react/no-find-dom-node */
import React from 'react';
import ReactDOM from 'react-dom';
import SplitPane from 'react-split-pane';
import _ from 'underscore';
import gettext from 'sources/gettext';
import QueryHistoryDetail from './query_history_detail';
import QueryHistoryEntries from './query_history_entries';
import Shapes from '../react_shapes';
const queryEntryListDivStyle = {
overflowY: 'auto',
};
const queryDetailDivStyle = {
display: 'flex',
};
export default class QueryHistory extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [],
selectedEntry: 0,
};
this.selectHistoryEntry = this.selectHistoryEntry.bind(this);
}
componentWillMount() {
this.setHistory(this.props.historyCollection.historyList);
this.selectHistoryEntry(0);
this.props.historyCollection.onChange((historyList) => {
this.setHistory(historyList);
this.selectHistoryEntry(0);
});
this.props.historyCollection.onReset(() => {
this.setState({
history: [],
currentHistoryDetail: undefined,
selectedEntry: 0,
});
});
}
componentDidMount() {
this.selectHistoryEntry(0);
}
refocus() {
if (this.state.history.length > 0) {
setTimeout(() => this.retrieveSelectedQuery().parentElement.focus(), 0);
}
}
retrieveSelectedQuery() {
return ReactDOM.findDOMNode(this)
.getElementsByClassName('selected')[0];
}
setHistory(historyList) {
this.setState({history: this.orderedHistory(historyList)});
}
selectHistoryEntry(index) {
this.setState({
currentHistoryDetail: this.state.history[index],
selectedEntry: index,
});
}
orderedHistory(historyList) {
return _.chain(historyList)
.sortBy(historyEntry => historyEntry.start_time)
.reverse()
.value();
}
render() {
if(this.state.history.length == 0) {
return(
<div className="alert alert-info pg-panel-message">{gettext('No history found')}</div>
);
} else {
return (
<SplitPane defaultSize='50%' split='vertical' pane1Style={queryEntryListDivStyle}
pane2Style={queryDetailDivStyle}>
<QueryHistoryEntries historyEntries={this.state.history}
selectedEntry={this.state.selectedEntry}
onSelectEntry={this.selectHistoryEntry}
/>
<QueryHistoryDetail historyEntry={this.state.currentHistoryDetail}
sqlEditorPref={this.props.sqlEditorPref}
/>
</SplitPane>);
}
}
}
QueryHistory.propTypes = {
historyCollection: Shapes.historyCollectionClass.isRequired,
sqlEditorPref: Shapes.sqlEditorPrefObj,
};
export {
QueryHistory,
};

View File

@ -1,52 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import HistoryDetailMetadata from './detail/history_detail_metadata';
import HistoryDetailQuery from './detail/history_detail_query';
import HistoryDetailMessage from './detail/history_detail_message';
import HistoryErrorMessage from './detail/history_error_message';
import Shapes from '../react_shapes';
export default class QueryHistoryDetail extends React.Component {
render() {
if (!_.isUndefined(this.props.historyEntry)) {
let historyErrorMessage = null;
if (!this.props.historyEntry.status) {
historyErrorMessage = <div className='error-message-block'>
<HistoryErrorMessage {...this.props} />
</div>;
}
return (
<div id='query_detail' className='query-detail'>
{historyErrorMessage}
<div className='metadata-block'>
<HistoryDetailMetadata {...this.props} />
</div>
<div className='query-statement-block'>
<HistoryDetailQuery {...this.props}/>
</div>
<div>
<hr className='block-divider'/>
</div>
<div className='message-block'>
<HistoryDetailMessage {...this.props}/>
</div>
</div>);
} else {
return <p></p>;
}
}
}
QueryHistoryDetail.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -1,157 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
/* eslint-disable react/no-find-dom-node */
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'underscore';
import moment from 'moment';
import PropTypes from 'prop-types';
import QueryHistoryEntry from './query_history_entry';
import QueryHistoryEntryDateGroup from './query_history_entry_date_group';
const ARROWUP = 38;
const ARROWDOWN = 40;
export default class QueryHistoryEntries extends React.Component {
constructor(props) {
super(props);
this.navigateUpAndDown = this.navigateUpAndDown.bind(this);
}
navigateUpAndDown(event) {
let arrowKeys = [ARROWUP, ARROWDOWN];
let key = event.keyCode || event.which;
if (arrowKeys.indexOf(key) > -1) {
event.preventDefault();
this.onKeyDownHandler(event);
return false;
}
return true;
}
onKeyDownHandler(event) {
if (this.isArrowDown(event)) {
if (!this.isLastEntry()) {
let nextEntry = this.props.selectedEntry + 1;
this.props.onSelectEntry(nextEntry);
if (this.isInvisible(this.getEntryFromList(nextEntry))) {
this.getEntryFromList(nextEntry).scrollIntoView(false);
}
}
} else if (this.isArrowUp(event)) {
if (!this.isFirstEntry()) {
let previousEntry = this.props.selectedEntry - 1;
this.props.onSelectEntry(previousEntry);
if (this.isInvisible(this.getEntryFromList(previousEntry))) {
this.getEntryFromList(previousEntry).scrollIntoView(true);
}
}
}
}
retrieveGroups() {
const sortableKeyFormat = 'YYYY MM DD';
const entriesGroupedByDate = _.groupBy(this.props.historyEntries, entry => moment(entry.start_time).format(sortableKeyFormat));
const elements = this.sortDesc(entriesGroupedByDate).map((key, index) => {
const groupElements = this.retrieveDateGroup(entriesGroupedByDate, key, index);
const keyAsDate = moment(key, sortableKeyFormat).toDate();
groupElements.unshift(
<li key={'group-' + index}>
<QueryHistoryEntryDateGroup date={keyAsDate}/>
</li>);
return groupElements;
});
return (
<ul>
{_.flatten(elements).map(element => element)}
</ul>
);
}
retrieveDateGroup(entriesGroupedByDate, key, parentIndex) {
const startingEntryIndex = _.reduce(
_.first(this.sortDesc(entriesGroupedByDate), parentIndex),
(memo, key) => memo + entriesGroupedByDate[key].length, 0);
return (
entriesGroupedByDate[key].map((entry, index) =>
<li key={`group-${parentIndex}-entry-${index}`}
className='list-item'
tabIndex={0}
onClick={() => this.props.onSelectEntry(startingEntryIndex + index)}
onKeyDown={this.navigateUpAndDown}>
<QueryHistoryEntry
historyEntry={entry}
isSelected={(startingEntryIndex + index) === this.props.selectedEntry}/>
</li>)
);
}
sortDesc(entriesGroupedByDate) {
return Object.keys(entriesGroupedByDate).sort().reverse();
}
isInvisible(element) {
return this.isAbovePaneTop(element) || this.isBelowPaneBottom(element);
}
isArrowUp(event) {
return (event.keyCode || event.which) === ARROWUP;
}
isArrowDown(event) {
return (event.keyCode || event.which) === ARROWDOWN;
}
isFirstEntry() {
return this.props.selectedEntry === 0;
}
isLastEntry() {
return this.props.selectedEntry === this.props.historyEntries.length - 1;
}
isAbovePaneTop(element) {
const paneElement = ReactDOM.findDOMNode(this).parentElement;
return element.getBoundingClientRect().top < paneElement.getBoundingClientRect().top;
}
isBelowPaneBottom(element) {
const paneElement = ReactDOM.findDOMNode(this).parentElement;
return element.getBoundingClientRect().bottom > paneElement.getBoundingClientRect().bottom;
}
getEntryFromList(entryIndex) {
return ReactDOM.findDOMNode(this).getElementsByClassName('entry')[entryIndex];
}
render() {
return (
<div id='query_list'
className="query-history">
{this.retrieveGroups()}
</div>
);
}
}
QueryHistoryEntries.propTypes = {
historyEntries: PropTypes.array.isRequired,
selectedEntry: PropTypes.number.isRequired,
onSelectEntry: PropTypes.func.isRequired,
};

View File

@ -1,59 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import Shapes from '../react_shapes';
import moment from 'moment';
import PropTypes from 'prop-types';
export default class QueryHistoryEntry extends React.Component {
formatDate(date) {
return (moment(date).format('HH:mm:ss'));
}
renderWithClasses(outerDivStyle) {
return (
<div className={'entry ' + outerDivStyle}>
<div className='query'>
{this.props.historyEntry.query}
</div>
<div className='other-info'>
<div className='timestamp'>
{this.formatDate(this.props.historyEntry.start_time)}
</div>
</div>
</div>
);
}
render() {
if (this.hasError()) {
if (this.props.isSelected) {
return this.renderWithClasses('error selected');
} else {
return this.renderWithClasses('error');
}
} else {
if (this.props.isSelected) {
return this.renderWithClasses('selected');
} else {
return this.renderWithClasses('');
}
}
}
hasError() {
return !this.props.historyEntry.status;
}
}
QueryHistoryEntry.propTypes = {
historyEntry: Shapes.historyDetail,
isSelected: PropTypes.bool,
};

View File

@ -1,47 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
export default class QueryHistoryEntryDateGroup extends React.Component {
getDatePrefix() {
let prefix = '';
if (this.isDaysBefore(0)) {
prefix = 'Today - ';
} else if (this.isDaysBefore(1)) {
prefix = 'Yesterday - ';
}
return prefix;
}
getDateFormatted(momentToFormat) {
return momentToFormat.format(QueryHistoryEntryDateGroup.formatString);
}
getDateMoment() {
return moment(this.props.date);
}
isDaysBefore(before) {
return this.getDateFormatted(this.getDateMoment()) === this.getDateFormatted(moment().subtract(before, 'days'));
}
render() {
return (<div className="date-label">{this.getDatePrefix()}{this.getDateFormatted(this.getDateMoment())}</div>);
}
}
QueryHistoryEntryDateGroup.propTypes = {
date: PropTypes.instanceOf(Date).isRequired,
};
QueryHistoryEntryDateGroup.formatString = 'MMM DD YYYY';

View File

@ -1,37 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import PropTypes from 'prop-types';
let historyDetail =
PropTypes.shape({
query: PropTypes.string,
start_time: PropTypes.instanceOf(Date),
status: PropTypes.bool,
total_time: PropTypes.string,
row_affected: PropTypes.int,
message: PropTypes.string,
});
let historyCollectionClass =
PropTypes.shape({
historyList: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
});
let sqlEditorPrefObj =
PropTypes.shape({
sql_font_size: PropTypes.string.isRequired,
});
export default {
historyDetail,
historyCollectionClass,
sqlEditorPrefObj,
};

View File

@ -25,9 +25,8 @@ define('tools.querytool', [
'sources/sqleditor/query_tool_http_error_handler',
'sources/sqleditor/filter_dialog',
'sources/sqleditor/geometry_viewer',
'sources/history/index.js',
'sourcesjsx/history/query_history',
'react', 'react-dom',
'sources/sqleditor/history/history_collection.js',
'sources/sqleditor/history/query_history',
'sources/keyboard_shortcuts',
'sources/sqleditor/query_tool_actions',
'sources/sqleditor/query_tool_notifications',
@ -48,7 +47,7 @@ define('tools.querytool', [
babelPollyfill, gettext, url_for, $, jqueryui, jqueryui_position, _, S, alertify, pgAdmin, Backbone, codemirror,
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
GeometryViewer, HistoryBundle, queryHistory, React, ReactDOM,
GeometryViewer, historyColl, queryHist,
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref) {
/* Return back, this has been called more than once */
@ -60,7 +59,9 @@ define('tools.querytool', [
var wcDocker = window.wcDocker,
pgBrowser = pgAdmin.Browser,
CodeMirror = codemirror.default,
Slick = window.Slick;
Slick = window.Slick,
HistoryCollection = historyColl.default,
QueryHistory = queryHist.default;
var is_query_running = false;
@ -1303,25 +1304,17 @@ define('tools.querytool', [
/* Should not reset if function called again */
if(!self.history_collection) {
self.history_collection = new HistoryBundle.HistoryCollection([]);
self.history_collection = new HistoryCollection([]);
}
var historyComponent;
var historyCollectionReactElement = React.createElement(
queryHistory.QueryHistory, {
historyCollection: self.history_collection,
ref: function(component) {
historyComponent = component;
},
sqlEditorPref: {
sql_font_size: SqlEditorUtils.calcFontSize(this.preferences.sql_font_size),
},
});
ReactDOM.render(historyCollectionReactElement, $('#history_grid')[0]);
if(!self.historyComponent) {
self.historyComponent = new QueryHistory($('#history_grid'), self.history_collection);
self.historyComponent.render();
}
self.history_panel.off(wcDocker.EVENT.VISIBILITY_CHANGED);
self.history_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
historyComponent.refocus();
self.historyComponent.focus();
});
},
@ -2574,6 +2567,12 @@ define('tools.querytool', [
$('#btn-flash').prop('disabled', false);
$('#btn-download').prop('disabled', false);
self.trigger('pgadmin-sqleditor:loading-icon:hide');
if(!self.total_time) {
self.total_time = calculateQueryRunTime.calculateQueryRunTime(
self.query_start_time,
new Date());
}
self.gridView.history_collection.add({
'status': status,
'start_time': self.query_start_time,

View File

@ -1,5 +1,6 @@
.query-history {
height: 100%;
overflow: auto;
.list-item {
border-bottom: $panel-border;
background-color: $color-bg-theme;
@ -46,19 +47,21 @@
background: $sql-history-error-bg;
}
.entry.selected.error {
background-color: $sql-history-error-bg;
}
.selected {
& .entry.error {
background-color: $sql-history-error-bg;
}
.entry.selected {
color: $sql-history-success-fg;
border: $table-hover-border;
background: $sql-history-success-bg;
font-weight: bold;
.other-info {
& .entry {
color: $sql-history-success-fg;
border: $table-hover-border;
background: $sql-history-success-bg;
font-weight: bold;
.other-info {
color: $sql-history-success-fg;
font-weight: bold;
}
}
}
}
@ -86,6 +89,7 @@
.query-detail {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
@ -205,3 +209,14 @@
}
}
}
#history_grid {
.gutter.gutter-horizontal {
width: $panel-border-width;
background: $panel-border-color;
&:hover {
cursor: ew-resize;
}
}
}

View File

@ -1,68 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import $ from 'jquery';
import CodeMirror from '../../pgadmin/static/jsx/history/detail/code_mirror';
import jasmineEnzyme from 'jasmine-enzyme';
import {shallow} from 'enzyme';
import './helper/enzyme.helper';
describe('CodeMirror', () => {
beforeEach(() => {
jasmineEnzyme();
});
describe('#hydrateWhenBecomesVisible', () => {
let codeMirror, isVisibleSpy;
beforeEach(() => {
codeMirror = shallow(<CodeMirror />).instance();
isVisibleSpy = spyOn($.fn, 'is');
spyOn(codeMirror, 'hydrate');
});
describe('when component is visible', () => {
beforeEach(() => {
isVisibleSpy.and.returnValue(true);
});
it('should hydrate the codemirror element', () => {
codeMirror.hydrateWhenBecomesVisible();
expect(codeMirror.hydrate).toHaveBeenCalledTimes(1);
});
});
describe('when component is not visible', () => {
beforeEach(() => {
isVisibleSpy.and.returnValue(false);
});
it('should not hydrate the codemirror element', () => {
codeMirror.hydrateWhenBecomesVisible();
expect(codeMirror.hydrate).not.toHaveBeenCalled();
});
describe('when becomes visible', () => {
beforeEach(() => {
isVisibleSpy.and.returnValue(true);
});
it('should hydrate the codemirror element', (done) => {
setTimeout(() => {
codeMirror.hydrateWhenBecomesVisible();
expect(codeMirror.hydrate).toHaveBeenCalledTimes(1);
done();
}, 150);
});
});
});
});
});

View File

@ -1,13 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({adapter: new Adapter()});

View File

@ -7,18 +7,17 @@
//
//////////////////////////////////////////////////////////////
import HistoryCollection from '../../../pgadmin/static/js/history/history_collection';
import '../helper/enzyme.helper';
import HistoryCollection from 'sources/sqleditor/history/history_collection';
describe('historyCollection', function () {
let historyCollection, historyModel, onChangeSpy, onResetSpy;
let historyCollection, historyModel, onAddSpy, onResetSpy;
beforeEach(() => {
historyModel = [{some: 'thing', someOther: ['array element']}];
historyCollection = new HistoryCollection(historyModel);
onChangeSpy = jasmine.createSpy('onChangeHandler');
onAddSpy = jasmine.createSpy('onAddHandler');
onResetSpy = jasmine.createSpy('onResetHandler');
historyCollection.onChange(onChangeSpy);
historyCollection.onAdd(onAddSpy);
historyCollection.onReset(onResetSpy);
});
@ -36,12 +35,13 @@ describe('historyCollection', function () {
describe('add', function () {
let expectedHistory;
let newEntry = {some: 'new thing', someOther: ['value1', 'value2']};
beforeEach(() => {
historyCollection.add({some: 'new thing', someOther: ['value1', 'value2']});
historyCollection.add(newEntry);
expectedHistory = [
{some: 'thing', someOther: ['array element']},
{some: 'new thing', someOther: ['value1', 'value2']},
{some: 'thing', someOther: ['array element']},
];
});
@ -50,7 +50,7 @@ describe('historyCollection', function () {
});
it('calls the onChange function', function () {
expect(onChangeSpy).toHaveBeenCalledWith(expectedHistory);
expect(onAddSpy).toHaveBeenCalledWith(newEntry);
});
});

View File

@ -9,59 +9,38 @@
/* eslint-disable react/no-find-dom-node */
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import ReactDOM from 'react-dom';
import QueryHistory from 'sources/sqleditor/history/query_history';
import HistoryCollection from 'sources/sqleditor/history/history_collection';
import clipboard from 'sources/selection/clipboard';
import $ from 'jquery';
import moment from 'moment';
import QueryHistory from '../../../pgadmin/static/jsx/history/query_history';
import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry';
import QueryHistoryEntryDateGroup from '../../../pgadmin/static/jsx/history/query_history_entry_date_group';
import QueryHistoryEntries from '../../../pgadmin/static/jsx/history/query_history_entries';
import QueryHistoryDetail from '../../../pgadmin/static/jsx/history/query_history_detail';
import HistoryCollection from '../../../pgadmin/static/js/history/history_collection';
import clipboard from '../../../pgadmin/static/js/selection/clipboard';
import {mount} from 'enzyme';
import '../helper/enzyme.helper';
describe('QueryHistory', () => {
let historyCollection;
let historyWrapper;
let sqlEditorPref = {sql_font_size: '1em'};
let sqlEditorPref = {sql_font_size: '1.5em'};
let historyComponent;
beforeEach(() => {
jasmineEnzyme();
historyWrapper = $('<div id="history_grid"></div>').appendTo('body');
});
describe('when there is no history', () => {
beforeEach(() => {
historyCollection = new HistoryCollection([]);
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}
sqlEditorPref={sqlEditorPref}
/>);
});
describe('when we switch to the query history tab', () => {
beforeEach(() => {
historyWrapper.instance().refocus();
spyOn(historyWrapper.instance(), 'retrieveSelectedQuery');
});
it('does not try to focus on any element', () => {
expect(historyWrapper.instance().retrieveSelectedQuery).not.toHaveBeenCalled();
});
historyComponent = new QueryHistory(historyWrapper, historyCollection);
historyComponent.render();
});
it('has no entries', (done) => {
let foundChildren = historyWrapper.find(QueryHistoryEntry);
let foundChildren = historyWrapper.find('#query_list');
expect(foundChildren.length).toBe(0);
done();
});
it('nothing is displayed in the history details panel', (done) => {
let foundChildren = historyWrapper.find(QueryHistoryDetail);
expect(foundChildren.length).toBe(0);
it('No history found is displayed', (done) => {
expect(historyWrapper.find('.pg-panel-message').html()).toBe('No history found');
done();
});
});
@ -69,8 +48,6 @@ describe('QueryHistory', () => {
describe('when there is history', () => {
let queryEntries;
let queryDetail;
let isInvisibleSpy;
let queryHistoryEntriesComponent;
describe('when two SQL queries were executed', () => {
@ -90,91 +67,38 @@ describe('QueryHistory', () => {
total_time: '234 msec',
message: 'something important ERROR: message from second sql query',
}];
historyCollection = new HistoryCollection(historyObjects);
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}
sqlEditorPref={sqlEditorPref}
/>);
historyComponent = new QueryHistory(historyWrapper, historyCollection);
historyComponent.render();
queryHistoryEntriesComponent = historyWrapper.find(QueryHistoryEntries);
isInvisibleSpy = spyOn(queryHistoryEntriesComponent.instance(), 'isInvisible')
.and.returnValue(false);
queryEntries = queryHistoryEntriesComponent.find(QueryHistoryEntry);
queryDetail = historyWrapper.find(QueryHistoryDetail);
queryEntries = historyWrapper.find('#query_list .list-item');
queryDetail = historyWrapper.find('#query_detail');
});
describe('the history entries panel', () => {
it('has two query history entries', () => {
it('has two query history entries', (done) => {
expect(queryEntries.length).toBe(2);
done();
});
it('displays the query history entries in order', () => {
expect(queryEntries.at(0).text()).toContain('first sql statement');
expect(queryEntries.at(1).text()).toContain('second sql statement');
expect($(queryEntries[0]).text()).toContain('first sql statement');
expect($(queryEntries[1]).text()).toContain('second sql statement');
});
it('displays the formatted timestamp of the queries in chronological order by most recent first', () => {
expect(queryEntries.at(0).find('.timestamp').text()).toBe('14:03:15');
expect(queryEntries.at(1).find('.timestamp').text()).toBe('01:33:05');
expect($(queryEntries[0]).find('.timestamp').text()).toBe('14:03:15');
expect($(queryEntries[1]).find('.timestamp').text()).toBe('01:33:05');
});
it('renders the most recent query as selected', () => {
expect(queryEntries.at(0).getElements().length).toBe(1);
expect(queryEntries.at(0).render().hasClass('selected')).toBeTruthy();
expect($(queryEntries[0]).hasClass('selected')).toBeTruthy();
});
it('renders the older query as not selected', () => {
expect(queryEntries.at(1).getElements().length).toBe(1);
expect(queryEntries.at(1).render().hasClass('selected')).toBeFalsy();
expect(queryEntries.at(1).render().hasClass('error')).toBeTruthy();
});
describe('when the selected query is the most recent', () => {
describe('when we press arrow down', () => {
beforeEach(() => {
pressArrowDownKey(queryHistoryEntriesComponent.find('.list-item').at(0));
});
it('should select the next query', () => {
expect(queryEntries.at(1).getElements().length).toBe(1);
expect(queryEntries.at(1).render().hasClass('selected')).toBeTruthy();
});
it('should display the corresponding detail on the right pane', () => {
expect(queryDetail.at(0).render().text()).toContain('message from second sql query');
});
describe('when arrow down pressed again', () => {
it('should not change the selected query', () => {
pressArrowDownKey(queryHistoryEntriesComponent.find('.list-item').at(0));
expect(queryEntries.at(1).getElements().length).toBe(1);
expect(queryEntries.at(1).render().hasClass('selected')).toBeTruthy();
});
});
describe('when arrow up is pressed', () => {
beforeEach(() => {
pressArrowUpKey(queryHistoryEntriesComponent.find('.list-item').at(0));
});
it('should select the most recent query', () => {
expect(queryEntries.at(0).getElements().length).toBe(1);
expect(queryEntries.at(0).render().hasClass('selected')).toBeTruthy();
});
});
});
describe('when arrow up is pressed', () => {
beforeEach(() => {
pressArrowUpKey(queryHistoryEntriesComponent.find('.list-item').at(0));
});
it('should not change the selected query', () => {
expect(queryEntries.at(0).getElements().length).toBe(1);
expect(queryEntries.at(0).render().hasClass('selected')).toBeTruthy();
});
});
expect($(queryEntries[1]).hasClass('selected')).toBeFalsy();
expect($(queryEntries[1]).find('.entry').hasClass('error')).toBeTruthy();
});
});
@ -185,29 +109,34 @@ describe('QueryHistory', () => {
copyAllButton = () => queryDetail.find('#history-detail-query > button');
});
it('should change preferences', ()=>{
historyComponent.setEditorPref(sqlEditorPref);
expect(queryDetail.find('#history-detail-query .CodeMirror').attr('style')).toBe('font-size: 1.5em;');
});
it('displays the formatted timestamp', () => {
expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date');
expect(queryDetail.text()).toContain('6-3-17 14:03:15');
});
it('displays the number of rows affected', () => {
if (/PhantomJS/.test(window.navigator.userAgent)) {
expect(queryDetail.at(0).text()).toContain('12345Rows Affected');
expect(queryDetail.text()).toContain('12345');
} else {
expect(queryDetail.at(0).text()).toContain('12,345Rows Affected');
expect(queryDetail.text()).toContain('12,345');
}
});
it('displays the total time', () => {
expect(queryDetail.at(0).text()).toContain('14 msecDuration');
expect(queryDetail.text()).toContain('14 msec');
});
it('displays the full message', () => {
expect(queryDetail.at(0).text()).toContain('message from first sql query');
expect(queryDetail.text()).toContain('message from first sql query');
});
it('displays first query SQL', (done) => {
setTimeout(() => {
expect(queryDetail.at(0).text()).toContain('first sql statement');
expect(queryDetail.text()).toContain('first sql statement');
done();
}, 1000);
});
@ -215,7 +144,7 @@ describe('QueryHistory', () => {
describe('when the "Copy All" button is clicked', () => {
beforeEach(() => {
spyOn(clipboard, 'copyTextToClipboard');
copyAllButton().simulate('click');
copyAllButton().trigger('click');
});
it('copies the query to the clipboard', () => {
@ -242,7 +171,7 @@ describe('QueryHistory', () => {
describe('when the copy button is clicked', () => {
beforeEach(() => {
copyAllButton().simulate('click');
copyAllButton().trigger('click');
});
describe('before 1.5 seconds', () => {
@ -255,7 +184,7 @@ describe('QueryHistory', () => {
});
it('should have the class \'was-copied\'', () => {
expect(copyAllButton().render().hasClass('was-copied')).toBe(true);
expect(copyAllButton().hasClass('was-copied')).toBe(true);
});
});
@ -272,7 +201,7 @@ describe('QueryHistory', () => {
describe('when is clicked again after 1s', () => {
beforeEach(() => {
jasmine.clock().tick(1000);
copyAllButton().simulate('click');
copyAllButton().trigger('click');
});
@ -286,7 +215,7 @@ describe('QueryHistory', () => {
});
it('should have the class \'was-copied\'', () => {
expect(copyAllButton().render().hasClass('was-copied')).toBe(true);
expect(copyAllButton().hasClass('was-copied')).toBe(true);
});
});
@ -307,76 +236,39 @@ describe('QueryHistory', () => {
let failedEntry;
beforeEach(() => {
failedEntry = queryEntries.at(1);
failedEntry.simulate('click');
failedEntry = $(queryEntries[1]);
failedEntry.trigger('click');
});
it('displays the error message on top of the details pane', () => {
expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query');
expect(queryDetail.text()).toContain('Error Message message from second sql query');
});
});
});
describe('when the older query is clicked on', () => {
let firstEntry, secondEntry;
beforeEach(() => {
firstEntry = queryEntries.at(0);
secondEntry = queryEntries.at(1);
secondEntry.simulate('click');
firstEntry = $(queryEntries[0]);
secondEntry = $(queryEntries[1]);
secondEntry.trigger('click');
});
it('displays the query in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('second sql statement');
expect(queryDetail.text()).toContain('second sql statement');
});
it('deselects the first history entry', () => {
expect(firstEntry.getElements().length).toBe(1);
expect(firstEntry.hasClass('selected')).toBeFalsy();
});
it('selects the second history entry', () => {
expect(secondEntry.getElements().length).toBe(1);
expect(secondEntry.render().hasClass('selected')).toBeTruthy();
expect(secondEntry.hasClass('selected')).toBeTruthy();
});
});
describe('when the first query is outside the visible area', () => {
beforeEach(() => {
isInvisibleSpy.and.callFake((element) => {
return element.textContent.contains('first sql statement');
});
});
describe('when the first query is the selected query', () => {
describe('when refocus function is called', () => {
let selectedListItem;
beforeEach(() => {
selectedListItem = ReactDOM.findDOMNode(historyWrapper.instance())
.getElementsByClassName('selected')[0].parentElement;
spyOn(selectedListItem, 'focus');
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('the first query scrolls into view', () => {
historyWrapper.instance().refocus();
expect(selectedListItem.focus).toHaveBeenCalledTimes(0);
jasmine.clock().tick(1);
expect(selectedListItem.focus).toHaveBeenCalledTimes(1);
});
});
});
});
describe('when a third SQL query is executed', () => {
beforeEach(() => {
historyCollection.add({
@ -388,11 +280,11 @@ describe('QueryHistory', () => {
message: 'pretext ERROR: third sql message',
});
queryEntries = historyWrapper.find(QueryHistoryEntry);
queryEntries = historyWrapper.find('#query_list .list-item');
});
it('displays third query SQL in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('third sql statement');
expect(queryDetail.text()).toContain('third sql statement');
});
});
@ -407,33 +299,13 @@ describe('QueryHistory', () => {
message: 'ERROR: unexpected error from fourth sql message',
});
queryEntries = historyWrapper.find(QueryHistoryEntry);
queryEntries = historyWrapper.find('#query_list .list-item');
});
it('displays fourth query SQL in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('Error Message unexpected error from fourth sql message');
expect(queryDetail.text()).toContain('Error Message unexpected error from fourth sql message');
});
});
describe('when a fifth SQL query is executed', () => {
beforeEach(() => {
historyCollection.add({
query: 'fifth sql statement',
start_time: new Date(2017, 12, 12, 1, 33, 5, 99),
status: false,
row_affected: 0,
total_time: '26 msec',
message: 'unknown error',
});
queryEntries = historyWrapper.find(QueryHistoryEntry);
});
it('displays fourth query SQL in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('Error Message unknown error');
});
});
});
describe('when several days of queries were executed', () => {
@ -482,16 +354,11 @@ describe('QueryHistory', () => {
}];
historyCollection = new HistoryCollection(historyObjects);
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}
sqlEditorPref={sqlEditorPref}
/>);
historyComponent = new QueryHistory(historyWrapper, historyCollection);
historyComponent.render();
const queryHistoryEntriesComponent = historyWrapper.find(QueryHistoryEntries);
isInvisibleSpy = spyOn(queryHistoryEntriesComponent.instance(), 'isInvisible')
.and.returnValue(false);
queryEntries = queryHistoryEntriesComponent.find(QueryHistoryEntry);
queryEntryDateGroups = queryHistoryEntriesComponent.find(QueryHistoryEntryDateGroup);
queryEntries = historyWrapper.find('#query_list .list-item');
queryEntryDateGroups = historyWrapper.find('#query_list .query-group');
});
afterEach(() => {
@ -504,23 +371,11 @@ describe('QueryHistory', () => {
});
it('has title above', () => {
expect(queryEntryDateGroups.at(0).text()).toBe('Today - Jul 01 2017');
expect(queryEntryDateGroups.at(1).text()).toBe('Yesterday - Jun 30 2017');
expect(queryEntryDateGroups.at(2).text()).toBe('Jun 28 2017');
expect($(queryEntryDateGroups[0]).find('.date-label').text()).toBe('Today - Jul 01 2017');
expect($(queryEntryDateGroups[1]).find('.date-label').text()).toBe('Yesterday - Jun 30 2017');
expect($(queryEntryDateGroups[2]).find('.date-label').text()).toBe('Jun 28 2017');
});
});
});
});
function pressArrowUpKey(node) {
pressKey(node, 38);
}
function pressArrowDownKey(node) {
pressKey(node, 40);
}
function pressKey(node, keyCode) {
node.simulate('keyDown', {keyCode: keyCode});
}
});

View File

@ -201,12 +201,12 @@ module.exports = {
// It solves number of problems
// Ref: http:/github.com/webpack-contrib/imports-loader/
rules: [{
test: /\.jsx?$/,
test: /\.js$/,
exclude: [/node_modules/, /vendor/],
use: {
loader: 'babel-loader',
options: {
presets: ['es2015', 'react'],
presets: ['es2015'],
plugins: ['transform-object-rest-spread'],
},
},
@ -391,7 +391,7 @@ module.exports = {
resolve: {
alias: webpackShimConfig.resolveAlias,
modules: ['node_modules', '.'],
extensions: ['.js', '.jsx'],
extensions: ['.js'],
unsafeCache: true,
},
// Watch mode Configuration: After initial build, webpack will watch for

View File

@ -134,7 +134,6 @@ var webpackShimConfig = {
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
'bundled_browser': path.join(__dirname, './pgadmin/static/bundle/browser'),
'sources': path.join(__dirname, './pgadmin/static/js'),
'sourcesjsx': path.join(__dirname, './pgadmin/static/jsx'),
'pgadmin': path.join(__dirname, './pgadmin/static/js/pgadmin'),
'translations': path.join(__dirname, './pgadmin/tools/templates/js/translations'),
'sources/gettext': path.join(__dirname, './pgadmin/static/js/gettext'),

View File

@ -31,12 +31,13 @@ module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
test: /\.js$/,
exclude: [/node_modules/, /vendor/],
use: {
loader: 'babel-loader',
options: {
presets: ['es2015', 'react', 'airbnb'],
presets: ['es2015'],
plugins: ['transform-object-rest-spread'],
},
},
}, {
@ -67,7 +68,7 @@ module.exports = {
},
resolve: {
extensions: ['.js', '.jsx'],
extensions: ['.js'],
alias: {
'top': path.join(__dirname, './pgadmin'),
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
@ -100,6 +101,7 @@ module.exports = {
'pgbrowser': path.resolve(__dirname, 'regression/javascript/fake_browser'),
'pgadmin.schema.dir': path.resolve(__dirname, 'pgadmin/browser/server_groups/servers/databases/schemas/static/js'),
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
},
},
};

File diff suppressed because it is too large Load Diff