diff --git a/web/pgadmin/feature_tests/query_tool_journey_test.py b/web/pgadmin/feature_tests/query_tool_journey_test.py
index 0795028c..4ffcff8f 100644
--- a/web/pgadmin/feature_tests/query_tool_journey_test.py
+++ b/web/pgadmin/feature_tests/query_tool_journey_test.py
@@ -72,13 +72,14 @@ class QueryToolJourneyTest(BaseFeatureTest):
self.__clear_query_tool()
editor_input = self.page.find_by_id("output-panel")
self.page.click_element(editor_input)
- self._execute_query("SELECT * FROM shoes")
+ self._execute_query("SELECT * FROM table_that_doesnt_exist")
- self.page.click_tab("History")
+ self.page.click_tab("Query History")
selected_history_entry = self.page.find_by_css_selector("#query_list .selected")
- self.assertIn("SELECT * FROM shoes", selected_history_entry.text)
+ self.assertIn("SELECT * FROM table_that_doesnt_exist", selected_history_entry.text)
failed_history_detail_pane = self.page.find_by_id("query_detail")
- self.assertIn("ERROR: relation \"shoes\" does not exist", failed_history_detail_pane.text)
+
+ self.assertIn("Error Message relation \"table_that_doesnt_exist\" does not exist", failed_history_detail_pane.text)
ActionChains(self.page.driver) \
.send_keys(Keys.ARROW_DOWN) \
.perform()
@@ -86,10 +87,30 @@ class QueryToolJourneyTest(BaseFeatureTest):
self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_entry.text)
selected_history_detail_pane = self.page.find_by_id("query_detail")
self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_detail_pane.text)
- newly_selected_history_entry = self.page.find_by_xpath("//*[@id='query_list']/ul/li[1]")
+ newly_selected_history_entry = self.page.find_by_xpath("//*[@id='query_list']/ul/li[2]")
self.page.click_element(newly_selected_history_entry)
selected_history_detail_pane = self.page.find_by_id("query_detail")
- self.assertIn("SELECT * FROM shoes", selected_history_detail_pane.text)
+ self.assertIn("SELECT * FROM table_that_doesnt_exist", selected_history_detail_pane.text)
+
+ self.__clear_query_tool()
+
+ self.page.click_element(editor_input)
+ for _ in range(15):
+ self._execute_query("SELECT * FROM hats")
+
+ self.page.click_tab("Query History")
+
+ query_we_need_to_scroll_to = self.page.find_by_xpath("//*[@id='query_list']/ul/li[17]")
+
+ self.page.click_element(query_we_need_to_scroll_to)
+ self._assert_not_clickable_because_out_of_view(query_we_need_to_scroll_to)
+
+ for _ in range(17):
+ ActionChains(self.page.driver) \
+ .send_keys(Keys.ARROW_DOWN) \
+ .perform()
+
+ self._assert_clickable(query_we_need_to_scroll_to)
self.__clear_query_tool()
self.page.click_element(editor_input)
diff --git a/web/pgadmin/static/css/webcabin.overrides.css b/web/pgadmin/static/css/webcabin.overrides.css
index cfad4eb5..0fbbdb98 100644
--- a/web/pgadmin/static/css/webcabin.overrides.css
+++ b/web/pgadmin/static/css/webcabin.overrides.css
@@ -268,6 +268,7 @@
.wcFrameTitleBar {
background-color: #e8e8e8;
height: 35px;
+ border-bottom: #cccccc;
}
.wcFloating .wcFrameTitleBar {
diff --git a/web/pgadmin/static/jsx/history/detail/history_detail_query.jsx b/web/pgadmin/static/jsx/history/detail/history_detail_query.jsx
index b3eab01f..317cb3ab 100644
--- a/web/pgadmin/static/jsx/history/detail/history_detail_query.jsx
+++ b/web/pgadmin/static/jsx/history/detail/history_detail_query.jsx
@@ -12,11 +12,49 @@ 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 (
+
{
- this.resetCurrentHistoryDetail(historyList);
+ this.setHistory(historyList);
+ this.selectHistoryEntry(0);
});
- this.props.historyCollection.onReset((historyList) => {
- this.clearCurrentHistoryDetail(historyList);
+ this.props.historyCollection.onReset(() => {
+ this.setState({
+ history: [],
+ currentHistoryDetail: undefined,
+ selectedEntry: 0,
+ });
});
}
componentDidMount() {
- this.resetCurrentHistoryDetail(this.state.history);
+ this.selectHistoryEntry(0);
}
refocus() {
if (this.state.history.length > 0) {
- this.retrieveSelectedQuery().parentElement.focus();
+ setTimeout(() => this.retrieveSelectedQuery().parentElement.focus(), 0);
}
}
@@ -66,130 +71,33 @@ export default class QueryHistory extends React.Component {
.getElementsByClassName('selected')[0];
}
- getCurrentHistoryDetail() {
- return this.state.currentHistoryDetail;
+ setHistory(historyList) {
+ this.setState({history: this.orderedHistory(historyList)});
}
- setCurrentHistoryDetail(index, historyList) {
+ selectHistoryEntry(index) {
this.setState({
- history: historyList,
- currentHistoryDetail: this.retrieveOrderedHistory().value()[index],
+ currentHistoryDetail: this.state.history[index],
selectedEntry: index,
});
}
- resetCurrentHistoryDetail(historyList) {
- this.setCurrentHistoryDetail(0, historyList);
- }
-
- clearCurrentHistoryDetail(historyList) {
- this.setState({
- history: historyList,
- currentHistoryDetail: undefined,
- selectedEntry: 0,
- });
- }
-
- retrieveOrderedHistory() {
- return _.chain(this.state.history)
+ orderedHistory(historyList) {
+ return _.chain(historyList)
.sortBy(historyEntry => historyEntry.start_time)
- .reverse();
- }
-
- onClickHandler(index) {
- this.setCurrentHistoryDetail(index, this.state.history);
- }
-
- isInvisible(element) {
- return this.isAbovePaneTop(element) || this.isBelowPaneBottom(element);
- }
-
- isBelowPaneBottom(element) {
- const paneElement = ReactDOM.findDOMNode(this).getElementsByClassName('Pane1')[0];
- return element.getBoundingClientRect().bottom > paneElement.getBoundingClientRect().bottom;
- }
-
- isAbovePaneTop(element) {
- const paneElement = ReactDOM.findDOMNode(this).getElementsByClassName('Pane1')[0];
- return element.getBoundingClientRect().top < paneElement.getBoundingClientRect().top;
- }
-
- navigateUpAndDown(event) {
- const arrowKeys = [ARROWUP, ARROWDOWN];
- const 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.state.selectedEntry + 1;
- this.setCurrentHistoryDetail(nextEntry, this.state.history);
-
- if (this.isInvisible(this.getEntryFromList(nextEntry))) {
- this.getEntryFromList(nextEntry).scrollIntoView(false);
- }
- }
- } else if (this.isArrowUp(event)) {
- if (!this.isFirstEntry()) {
- let previousEntry = this.state.selectedEntry - 1;
- this.setCurrentHistoryDetail(previousEntry, this.state.history);
-
- if (this.isInvisible(this.getEntryFromList(previousEntry))) {
- this.getEntryFromList(previousEntry).scrollIntoView(true);
- }
- }
- }
- }
-
- getEntryFromList(entryIndex) {
- return ReactDOM.findDOMNode(this).getElementsByClassName('entry')[entryIndex];
- }
-
- isFirstEntry() {
- return this.state.selectedEntry === 0;
- }
-
- isLastEntry() {
- return this.state.selectedEntry === this.state.history.length - 1;
- }
-
- isArrowUp(event) {
- return (event.keyCode || event.which) === ARROWUP;
- }
-
- isArrowDown(event) {
- return (event.keyCode || event.which) === ARROWDOWN;
+ .reverse()
+ .value();
}
render() {
return (
-
-
- {this.retrieveOrderedHistory()
- .map((entry, index) =>
- -
-
-
)
- .value()
- }
-
-
-
+
+
);
}
}
diff --git a/web/pgadmin/static/jsx/history/query_history_entries.jsx b/web/pgadmin/static/jsx/history/query_history_entries.jsx
new file mode 100644
index 00000000..f0c3e603
--- /dev/null
+++ b/web/pgadmin/static/jsx/history/query_history_entries.jsx
@@ -0,0 +1,156 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, 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 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(
+
+
+ );
+ return groupElements;
+ });
+
+ return (
+
+ {_.flatten(elements).map(element => element)}
+
+ );
+ }
+
+ 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) =>
+ this.props.onSelectEntry(startingEntryIndex + index)}
+ onKeyDown={this.navigateUpAndDown}>
+
+ )
+ );
+ }
+
+ 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 (
+
+ {this.retrieveGroups()}
+
+ );
+ }
+}
+
+QueryHistoryEntries.propTypes = {
+ historyEntries: React.PropTypes.array.isRequired,
+ selectedEntry: React.PropTypes.number.isRequired,
+ onSelectEntry: React.PropTypes.func.isRequired,
+};
diff --git a/web/pgadmin/static/jsx/history/query_history_entry.jsx b/web/pgadmin/static/jsx/history/query_history_entry.jsx
index f2166081..acc38d27 100644
--- a/web/pgadmin/static/jsx/history/query_history_entry.jsx
+++ b/web/pgadmin/static/jsx/history/query_history_entry.jsx
@@ -13,7 +13,7 @@ import moment from 'moment';
export default class QueryHistoryEntry extends React.Component {
formatDate(date) {
- return (moment(date).format('MMM D YYYY [–] HH:mm:ss'));
+ return (moment(date).format('HH:mm:ss'));
}
renderWithClasses(outerDivStyle) {
diff --git a/web/pgadmin/static/jsx/history/query_history_entry_date_group.jsx b/web/pgadmin/static/jsx/history/query_history_entry_date_group.jsx
new file mode 100644
index 00000000..6d963773
--- /dev/null
+++ b/web/pgadmin/static/jsx/history/query_history_entry_date_group.jsx
@@ -0,0 +1,46 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import React from 'react';
+import moment from 'moment';
+
+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 ({this.getDatePrefix()}{this.getDateFormatted(this.getDateMoment())}
);
+ }
+}
+
+QueryHistoryEntryDateGroup.propTypes = {
+ date: React.PropTypes.instanceOf(Date).isRequired,
+};
+
+QueryHistoryEntryDateGroup.formatString = 'MMM DD YYYY';
diff --git a/web/pgadmin/static/scss/_alert.scss b/web/pgadmin/static/scss/_alert.scss
index fdd4546c..9b97ebca 100644
--- a/web/pgadmin/static/scss/_alert.scss
+++ b/web/pgadmin/static/scss/_alert.scss
@@ -1,92 +1,14 @@
-/*doc
----
-title: Alerts
-name: alerts
-category: alerts
----
-
-```html_example
-
-
-
-
-
-
-```
-*/
-
-
-// from bootstrap scss:
-
-@if $enable-flex {
- .media {
- display: flex;
- }
- .media-body {
- flex: 1;
- }
- .media-middle {
- align-self: center;
- }
- .media-bottom {
- align-self: flex-end;
- }
-} @else {
- .media,
- .media-body {
- overflow: hidden;
- }
- .media-body {
- width: 10000px;
- }
- .media-left,
- .media-right,
- .media-body {
- display: inline;
- vertical-align: top;
- }
- .media-middle {
- vertical-align: middle;
- }
- .media-bottom {
- vertical-align: bottom;
- }
+.alert-icon {
+ display: flex;
+ align-items: center;
+ color: white;
+ padding: 15px 15px 15px 17px;
+ width: 50px;
+ min-height: 50px;
+ font-size: 14px;
+ text-align: center;
+ align-self: stretch;
+ flex-shrink: 0;
}
.alert-row {
@@ -103,22 +25,12 @@ category: alerts
padding: 15px;
}
-.alert-icon {
- display: inline-block;
- color: white;
- padding: 15px;
- width: 50px;
- height: 50px;
- font-size: 14px;
- text-align: center;
-}
-
.success-icon {
- background: #3a773a;
+ background: $color-green-3;
}
.error-icon {
- background: #d0021b;
+ background: $color-red-3;
}
.info-icon {
@@ -128,17 +40,27 @@ category: alerts
.alert-text {
display: inline-block;
padding: 0 12px 0 10px;
+ align-self: center;
// To make sure IE picks up the correct font
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.alert-info {
- border-color: #84acdd
+ border-color: $color-blue-2;
+ background-image: none;
+}
+
+.alert-danger {
+ background-image: none;
}
-.media-body {
- vertical-align: top;
- width: initial;
+.grid-error, .graph-error {
+ .alert-row {
+ align-items: center;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ }
}
.ajs-message {
@@ -147,19 +69,36 @@ category: alerts
}
}
+.alert, .ajs-message {
+ .media {
+ .media-body {
+ display: inline-block;
+ width: auto;
+ .alert-icon {
+ display: inline-block;
+ }
+ .alert-text {
+ display: inline-block;
+ }
+ }
+ }
+}
+
.pg-prop-status-bar {
padding: 5px;
.media-body {
display: flex;
+ width: auto;
}
.alert-icon {
- padding: 8px;
+ padding: 8px 8px 8px 10.5px;
width: 35px;
height: 35px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
+ min-height: auto;
}
.alert-text {
@@ -167,8 +106,8 @@ category: alerts
border: 1px solid $color-red-2;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
- padding: 7px 12px 5px 10px;
- border-left: 0px;
+ padding: 7px 12px 6px 10px;
+ border-left: none;
}
.error-in-footer {
@@ -193,7 +132,28 @@ category: alerts
height: 35px;
.alert-text {
- border: 0px;
+ border: none;
+ }
+ }
+}
+
+//Internet Explorer specific CSS
+@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
+ .styleguide {
+ .alert-danger {
+ width: auto;
}
+
+ .alert-info {
+ width: auto;
+ }
+ }
+
+ .alert-danger {
+ width: 90%;
+ }
+
+ .alert-info {
+ width: 90%;
}
}
diff --git a/web/pgadmin/static/scss/_colorsgrey.scss b/web/pgadmin/static/scss/_colorsgrey.scss
index d7108eaf..d27d7042 100644
--- a/web/pgadmin/static/scss/_colorsgrey.scss
+++ b/web/pgadmin/static/scss/_colorsgrey.scss
@@ -1,63 +1,3 @@
-/*doc
----
-title: Grays
-name: Grays
-category: colors
----
-For text, avoid using black or #000 to lower the contrast between the background and text.
-
-```html_example
-
-```
-
-*/
-
-.color-chip {
- align-items: center;
- border-radius: 3px;
- box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, .15);
- color: rgba(0, 0, 0, .65);
- display: flex;
- font-size: 1.25em;
- height: 100px;
- justify-content: center;
- margin: 0 0 1em;
- width: 100%;
-}
-
$color-gray-1: #f9f9f9;
$color-gray-2: #e8e8e8;
$color-gray-3: #cccccc;
@@ -120,7 +60,3 @@ $color-gray-6: #333333;
.font-gray-6 {
color: $color-gray-6;
}
-
-.font-white {
- color: #FFFFFF;
-}
diff --git a/web/pgadmin/static/scss/_othercolors.scss b/web/pgadmin/static/scss/_othercolors.scss
index 9f6fbcda..5144217a 100644
--- a/web/pgadmin/static/scss/_othercolors.scss
+++ b/web/pgadmin/static/scss/_othercolors.scss
@@ -1,99 +1,22 @@
-/*doc
----
-title: Others
-name: z-othercolors
-category: colors
----
-These colors should be used to highlight hover options in dropdown menus and catalog browser or to tell the user when something is right or wrong.
-
-
-```html_example
-
-
-
-
-
-```
-
-*/
-
-.color-chip {
- align-items: center;
- border-radius: 3px;
- box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, .15);
- color: rgba(0, 0, 0, .65);
- display: flex;
- font-size: 1.25em;
- height: 100px;
- justify-content: center;
- margin: 0 0 1em;
- width: 100%;
-}
-
+$color-blue-1: #e7f2ff;
+$color-blue-2: #84acdd;
$color-red-1: #f2dede;
$color-red-2: #de8585;
$color-red-3: #d0021b;
+$color-green-1: #dff0d7;
$color-green-2: #a2c189;
+$color-green-3: #3a773a;
.bg-white-1 {
background-color: #ffffff;
}
.bg-blue-1 {
- background-color: #e7f2ff;
+ background-color: $color-blue-1;
}
.bg-blue-2 {
- background-color: #84acdd;
+ background-color: $color-blue-2;
}
.bg-red-1 {
@@ -109,23 +32,23 @@ $color-green-2: #a2c189;
}
.bg-green-1 {
- background-color: #dff0d7;
+ background-color: $color-green-1;
}
.bg-green-2 {
- background-color: #a2c189;
+ background-color: $color-green-2;
}
.bg-green-3 {
- background-color: #3a773a;
+ background-color: $color-green-3;
}
.border-blue-1 {
- border-color: #e7f2ff;
+ border-color: $color-blue-1;
}
.border-blue-2 {
- border-color: #84acdd;
+ border-color: $color-blue-2;
}
.border-red-1 {
@@ -141,15 +64,15 @@ $color-green-2: #a2c189;
}
.border-green-1 {
- border-color: #dff0d7;
+ border-color: $color-green-1;
}
.border-green-2 {
- border-color: #a2c189;
+ border-color: $color-green-2;
}
.border-green-3 {
- border-color: #3a773a;
+ border-color: $color-green-3;
}
.font-red-3 {
@@ -157,9 +80,5 @@ $color-green-2: #a2c189;
}
.font-green-3 {
- color: #3a773a;
-}
-
-.font-white {
- color: #FFFFFF;
+ color: $color-green-3;
}
diff --git a/web/pgadmin/static/scss/_primaryblue.scss b/web/pgadmin/static/scss/_primaryblue.scss
index ac7b1cfd..49d62a34 100644
--- a/web/pgadmin/static/scss/_primaryblue.scss
+++ b/web/pgadmin/static/scss/_primaryblue.scss
@@ -1,39 +1,3 @@
-/*doc
----
-title: Primary blue
-name: colors-primaryblue
-category: colors
----
-This color should be used to call attention to the main part of the app. Use sparingly.
-
-```html_example
-
-
-```
-
-*/
-
-.color-chip {
- align-items: center;
- border-radius: 3px;
- box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, .15);
- color: rgba(0, 0, 0, .65);
- display: flex;
- font-size: 1.25em;
- height: 100px;
- justify-content: center;
- margin: 0 0 1em;
- width: 100%;
-}
-
$primary-blue: #2c76b4;
.bg-primary-blue {
@@ -46,4 +10,4 @@ $primary-blue: #2c76b4;
.font-primary-blue {
color: $primary-blue;
-}
+}
\ No newline at end of file
diff --git a/web/pgadmin/static/scss/_typography.scss b/web/pgadmin/static/scss/_typography.scss
index 32d1cb8c..1b01abcc 100644
--- a/web/pgadmin/static/scss/_typography.scss
+++ b/web/pgadmin/static/scss/_typography.scss
@@ -1,68 +1,25 @@
-/*doc
----
-title: Typography
-name: typography
-category: typography
----
-
-Font Typography
-
-```html_example_table
-
- Body 14 px Helvetica Neue
-
-
-
- Body 14 px Helvetica Neue bold
-
-
-
- Body 13 px Helvetica Neue
-
-
-
- Body 13 px Helvetica Neue bold
-
-
-
- Body 12 px Helvetica Neue
-
-
-
- Body 12 px Helvetica Neue bold
-
-
-
- Body 11 px Helvetica Neue
-
-
-
- Body 11 px Helvetica Neue bold
-
-```
-
-*/
+$font-family-1: "Helvetica Neue", Arial, sans-serif;
.text-bold {
font-weight: bold;
}
.text-14 {
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-family: $font-family-1;
font-size: 14px;
}
.text-13 {
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-family: $font-family-1;
font-size: 13px;
}
.text-12 {
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-family: $font-family-1;
font-size: 12px;
}
.text-11 {
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-family: $font-family-1;
font-size: 11px;
}
diff --git a/web/pgadmin/static/scss/sqleditor/_history.scss b/web/pgadmin/static/scss/sqleditor/_history.scss
index 942af03f..518eba2a 100644
--- a/web/pgadmin/static/scss/sqleditor/_history.scss
+++ b/web/pgadmin/static/scss/sqleditor/_history.scss
@@ -7,10 +7,10 @@
.entry {
@extend .text-14;
@extend .bg-white-1;
- padding: -2px 18px -2px 8px;
font-family: monospace;
border: 2px solid transparent;
margin-left: 1px;
+ padding: 0 8px 0 5px;
.other-info {
@extend .text-13;
@@ -33,6 +33,16 @@
}
}
+ .date-label {
+ font-family: monospace;
+ background: #e8e8e8;
+ padding: 2px 9px;
+ font-size: 11px;
+ font-weight: bold;
+ color: #888888;
+ border-bottom: 1px solid #cccccc;
+ }
+
.entry.error {
@extend .bg-red-1;
}
@@ -114,6 +124,32 @@
margin-right: 10px;
height: 0;
position: relative;
+
+ .copy-all, .was-copied {
+ float: left;
+ position: relative;
+ z-index: 10;
+ border: 1px solid $color-gray-3;
+ color: $primary-blue;
+ font-size: 12px;
+ box-shadow: 1px 2px 4px 0px $color-gray-3;
+ padding: 3px 12px 3px 10px;
+ font-weight: 500;
+ min-width: 75px;
+ }
+
+ .copy-all {
+ background-color: #ffffff;
+ }
+
+ .was-copied {
+ background-color: $color-blue-1;
+ border-color: $color-blue-2;
+ }
+
+ .CodeMirror-scroll {
+ padding-top: 25px;
+ }
}
.block-divider {
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index 64527449..f4e1c141 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -215,7 +215,7 @@ define('tools.querytool', [
var history = new pgAdmin.Browser.Panel({
name: 'history',
- title: gettext("History"),
+ title: gettext("Query History"),
width: '100%',
height:'100%',
isCloseable: false,
diff --git a/web/regression/javascript/history/query_history_spec.jsx b/web/regression/javascript/history/query_history_spec.jsx
index e107e966..0a962449 100644
--- a/web/regression/javascript/history/query_history_spec.jsx
+++ b/web/regression/javascript/history/query_history_spec.jsx
@@ -12,10 +12,15 @@
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import ReactDOM from 'react-dom';
+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';
@@ -50,7 +55,7 @@ describe('QueryHistory', () => {
done();
});
- it('nothing is displayed on right panel', (done) => {
+ it('nothing is displayed in the history details panel', (done) => {
let foundChildren = historyWrapper.find(QueryHistoryDetail);
expect(foundChildren.length).toBe(1);
done();
@@ -58,254 +63,397 @@ describe('QueryHistory', () => {
});
describe('when there is history', () => {
- let historyObjects;
-
- beforeEach(function () {
- historyObjects = [{
- query: 'first sql statement',
- start_time: new Date(2017, 5, 3, 14, 3, 15, 150),
- status: true,
- row_affected: 12345,
- total_time: '14 msec',
- message: 'something important ERROR: message from first sql query',
- }, {
- query: 'second sql statement',
- start_time: new Date(2016, 11, 11, 1, 33, 5, 99),
- status: false,
- row_affected: 1,
- total_time: '234 msec',
- message: 'something important ERROR: message from second sql query',
- }];
- historyCollection = new HistoryCollection(historyObjects);
+ let queryEntries;
+ let queryDetail;
+ let isInvisibleSpy;
- historyWrapper = mount();
- });
+ describe('when two SQL queries were executed', () => {
- describe('when all query entries are visible in the pane', () => {
- describe('when two SQL queries were executed', () => {
- let foundChildren;
- let queryDetail;
+ beforeEach(() => {
+ const historyObjects = [{
+ query: 'first sql statement',
+ start_time: new Date(2017, 5, 3, 14, 3, 15, 150),
+ status: true,
+ row_affected: 12345,
+ total_time: '14 msec',
+ message: 'something important ERROR: message from first sql query',
+ }, {
+ query: 'second sql statement',
+ start_time: new Date(2016, 11, 11, 1, 33, 5, 99),
+ status: false,
+ row_affected: 1,
+ total_time: '234 msec',
+ message: 'something important ERROR: message from second sql query',
+ }];
+ historyCollection = new HistoryCollection(historyObjects);
+
+ historyWrapper = mount();
+
+ const queryHistoryEntriesComponent = historyWrapper.find(QueryHistoryEntries);
+ isInvisibleSpy = spyOn(queryHistoryEntriesComponent.node, 'isInvisible')
+ .and.returnValue(false);
+
+ queryEntries = queryHistoryEntriesComponent.find(QueryHistoryEntry);
+ queryDetail = historyWrapper.find(QueryHistoryDetail);
+ });
- beforeEach(() => {
- spyOn(historyWrapper.node, 'isInvisible').and.returnValue(false);
- foundChildren = historyWrapper.find(QueryHistoryEntry);
+ describe('the history entries panel', () => {
+ it('has two query history entries', () => {
+ expect(queryEntries.length).toBe(2);
+ });
- queryDetail = historyWrapper.find(QueryHistoryDetail);
+ 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');
});
- describe('the main pane', () => {
- it('has two query history entries', () => {
- expect(foundChildren.length).toBe(2);
- });
+ 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');
+ });
- it('displays the query history entries in order', () => {
- expect(foundChildren.at(0).text()).toContain('first sql statement');
- expect(foundChildren.at(1).text()).toContain('second sql statement');
- });
+ it('renders the most recent query as selected', () => {
+ expect(queryEntries.at(0).nodes.length).toBe(1);
+ expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
+ });
- it('displays the formatted timestamp of the queries in chronological order by most recent first', () => {
- expect(foundChildren.at(0).text()).toContain('Jun 3 2017 – 14:03:15');
- expect(foundChildren.at(1).text()).toContain('Dec 11 2016 – 01:33:05');
- });
+ it('renders the older query as not selected', () => {
+ expect(queryEntries.at(1).nodes.length).toBe(1);
+ expect(queryEntries.at(1).hasClass('selected')).toBeFalsy();
+ expect(queryEntries.at(1).hasClass('error')).toBeTruthy();
+ });
- it('renders the most recent query as selected', () => {
- expect(foundChildren.at(0).nodes.length).toBe(1);
- expect(foundChildren.at(0).hasClass('selected')).toBeTruthy();
- });
+ describe('when the selected query is the most recent', () => {
+ describe('when we press arrow down', () => {
+ beforeEach(() => {
+ pressArrowDownKey(queryEntries.parent().at(0));
+ });
- it('renders the older query as not selected', () => {
- expect(foundChildren.at(1).nodes.length).toBe(1);
- expect(foundChildren.at(1).hasClass('selected')).toBeFalsy();
- expect(foundChildren.at(1).hasClass('error')).toBeTruthy();
- });
+ it('should select the next query', () => {
+ expect(queryEntries.at(1).nodes.length).toBe(1);
+ expect(queryEntries.at(1).hasClass('selected')).toBeTruthy();
+ });
- describe('when the selected query is the most recent', () => {
- describe('when we press arrow down', () => {
- beforeEach(() => {
- pressArrowDownKey(foundChildren.parent().at(0));
- });
+ it('should display the corresponding detail on the right pane', () => {
+ expect(queryDetail.at(0).text()).toContain('message from second sql query');
+ });
- it('should select the next query', () => {
- expect(foundChildren.at(1).nodes.length).toBe(1);
- expect(foundChildren.at(1).hasClass('selected')).toBeTruthy();
- });
+ describe('when arrow down pressed again', () => {
+ it('should not change the selected query', () => {
+ pressArrowDownKey(queryEntries.parent().at(0));
- it('should display the corresponding detail on the right pane', () => {
- expect(queryDetail.at(0).text()).toContain('message from second sql query');
+ expect(queryEntries.at(1).nodes.length).toBe(1);
+ expect(queryEntries.at(1).hasClass('selected')).toBeTruthy();
});
+ });
- describe('when arrow down pressed again', () => {
- it('should not change the selected query', () => {
- pressArrowDownKey(foundChildren.parent().at(0));
+ describe('when arrow up is pressed', () => {
+ it('should select the most recent query', () => {
+ pressArrowUpKey(queryEntries.parent().at(0));
- expect(foundChildren.at(1).nodes.length).toBe(1);
- expect(foundChildren.at(1).hasClass('selected')).toBeTruthy();
- });
- });
-
- describe('when arrow up is pressed', () => {
- it('should select the most recent query', () => {
- pressArrowUpKey(foundChildren.parent().at(0));
-
- expect(foundChildren.at(0).nodes.length).toBe(1);
- expect(foundChildren.at(0).hasClass('selected')).toBeTruthy();
- });
+ expect(queryEntries.at(0).nodes.length).toBe(1);
+ expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
});
});
+ });
- describe('when arrow up is pressed', () => {
- it('should not change the selected query', () => {
- pressArrowUpKey(foundChildren.parent().at(0));
- expect(foundChildren.at(0).nodes.length).toBe(1);
- expect(foundChildren.at(0).hasClass('selected')).toBeTruthy();
- });
+ describe('when arrow up is pressed', () => {
+ it('should not change the selected query', () => {
+ pressArrowUpKey(queryEntries.parent().at(0));
+ expect(queryEntries.at(0).nodes.length).toBe(1);
+ expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
});
});
});
+ });
+
+ describe('the historydetails panel', () => {
+ let copyAllButton;
+
+ beforeEach(() => {
+ copyAllButton = () => queryDetail.find('#history-detail-query > button');
+ });
+ it('displays the formatted timestamp', () => {
+ expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date');
+ });
+
+ it('displays the number of rows affected', () => {
+ if (/PhantomJS/.test(window.navigator.userAgent)) {
+ expect(queryDetail.at(0).text()).toContain('12345Rows Affected');
+ } else {
+ expect(queryDetail.at(0).text()).toContain('12,345Rows Affected');
+ }
+ });
+
+ it('displays the total time', () => {
+ expect(queryDetail.at(0).text()).toContain('14 msecDuration');
+ });
+
+ it('displays the full message', () => {
+ expect(queryDetail.at(0).text()).toContain('message from first sql query');
+ });
- describe('the details pane', () => {
- it('displays the formatted timestamp', () => {
- expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date');
+ it('displays first query SQL', (done) => {
+ setTimeout(() => {
+ expect(queryDetail.at(0).text()).toContain('first sql statement');
+ done();
+ }, 1000);
+ });
+
+ describe('when the "Copy All" button is clicked', () => {
+ beforeEach(() => {
+ spyOn(clipboard, 'copyTextToClipboard');
+ copyAllButton().simulate('click');
});
- it('displays the number of rows affected', () => {
- if (/PhantomJS/.test(window.navigator.userAgent)) {
- expect(queryDetail.at(0).text()).toContain('12345Rows Affected');
- } else {
- expect(queryDetail.at(0).text()).toContain('12,345Rows Affected');
- }
+ it('copies the query to the clipboard', () => {
+ expect(clipboard.copyTextToClipboard).toHaveBeenCalledWith('first sql statement');
});
+ });
- it('displays the total time', () => {
- expect(queryDetail.at(0).text()).toContain('14 msecDuration');
+ describe('Copy button', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
});
- it('displays the full message', () => {
- expect(queryDetail.at(0).text()).toContain('message from first sql query');
+ afterEach(() => {
+ jasmine.clock().uninstall();
});
- it('displays first query SQL', (done) => {
- setTimeout(() => {
- expect(queryDetail.at(0).text()).toContain('first sql statement');
- done();
- }, 1000);
+ it('should have text \'Copy All\'', () => {
+ expect(copyAllButton().text()).toBe('Copy All');
});
- describe('when the query failed', () => {
- let failedEntry;
+ it('should not have the class \'was-copied\'', () => {
+ expect(copyAllButton().hasClass('was-copied')).toBe(false);
+ });
+ describe('when the copy button is clicked', () => {
beforeEach(() => {
- failedEntry = foundChildren.at(1);
- failedEntry.simulate('click');
+ copyAllButton().simulate('click');
+ });
+
+ describe('before 1.5 seconds', () => {
+ beforeEach(() => {
+ jasmine.clock().tick(1499);
+ });
+
+ it('should change the button text to \'Copied!\'', () => {
+ expect(copyAllButton().text()).toBe('Copied!');
+ });
+
+ it('should have the class \'was-copied\'', () => {
+ expect(copyAllButton().hasClass('was-copied')).toBe(true);
+ });
});
- it('displays the error message on top of the details pane', () => {
- expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query');
+
+ describe('after 1.5 seconds', () => {
+ beforeEach(() => {
+ jasmine.clock().tick(1501);
+ });
+
+ it('should change the button text back to \'Copy All\'', () => {
+ expect(copyAllButton().text()).toBe('Copy All');
+ });
+ });
+
+ describe('when is clicked again after 1s', () => {
+ beforeEach(() => {
+ jasmine.clock().tick(1000);
+ copyAllButton().simulate('click');
+
+ });
+
+ describe('before 2.5 seconds', () => {
+ beforeEach(() => {
+ jasmine.clock().tick(1499);
+ });
+
+ it('should change the button text to \'Copied!\'', () => {
+ expect(copyAllButton().text()).toBe('Copied!');
+ });
+
+ it('should have the class \'was-copied\'', () => {
+ expect(copyAllButton().hasClass('was-copied')).toBe(true);
+ });
+ });
+
+ describe('after 2.5 seconds', () => {
+ beforeEach(() => {
+ jasmine.clock().tick(1501);
+ });
+
+ it('should change the button text back to \'Copy All\'', () => {
+ expect(copyAllButton().text()).toBe('Copy All');
+ });
+ });
});
});
});
- describe('when the older query is clicked on', () => {
- let firstEntry, secondEntry;
+ describe('when the query failed', () => {
+ let failedEntry;
beforeEach(() => {
- firstEntry = foundChildren.at(0);
- secondEntry = foundChildren.at(1);
- secondEntry.simulate('click');
+ failedEntry = queryEntries.at(1);
+ failedEntry.simulate('click');
});
- it('displays the query in the right pane', () => {
- expect(queryDetail.at(0).text()).toContain('second sql statement');
+ it('displays the error message on top of the details pane', () => {
+ expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query');
});
+ });
+ });
- it('deselects the first history entry', () => {
- expect(firstEntry.nodes.length).toBe(1);
- expect(firstEntry.hasClass('selected')).toBe(false);
- });
+ describe('when the older query is clicked on', () => {
+ let firstEntry, secondEntry;
- it('selects the second history entry', () => {
- expect(secondEntry.nodes.length).toBe(1);
- expect(secondEntry.hasClass('selected')).toBe(true);
- });
+ beforeEach(() => {
+ firstEntry = queryEntries.at(0);
+ secondEntry = queryEntries.at(1);
+ secondEntry.simulate('click');
});
- describe('when the user clicks inside the main pane but not in any history entry', () => {
- let queryList;
- let firstEntry, secondEntry;
+ it('displays the query in the right pane', () => {
+ expect(queryDetail.at(0).text()).toContain('second sql statement');
+ });
- beforeEach(() => {
- firstEntry = foundChildren.at(0);
- secondEntry = foundChildren.at(1);
- queryList = historyWrapper.find('#query_list');
+ it('deselects the first history entry', () => {
+ expect(firstEntry.nodes.length).toBe(1);
+ expect(firstEntry.hasClass('selected')).toBeFalsy();
- secondEntry.simulate('click');
- queryList.simulate('click');
- });
+ });
+
+ it('selects the second history entry', () => {
+ expect(secondEntry.nodes.length).toBe(1);
+ expect(secondEntry.hasClass('selected')).toBeTruthy();
+ });
+ });
- it('should not change the selected entry', () => {
- expect(firstEntry.hasClass('selected')).toBe(false);
- expect(secondEntry.hasClass('selected')).toBe(true);
+ 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;
- describe('when up arrow is keyed', () => {
beforeEach(() => {
- pressArrowUpKey(queryList);
+ selectedListItem = ReactDOM.findDOMNode(historyWrapper.node)
+ .getElementsByClassName('selected')[0].parentElement;
+
+ spyOn(selectedListItem, 'focus');
+
+ jasmine.clock().install();
});
- it('selects the first history entry', () => {
- expect(firstEntry.nodes.length).toBe(1);
- expect(firstEntry.hasClass('selected')).toBe(true);
+ afterEach(() => {
+ jasmine.clock().uninstall();
});
- it('deselects the second history entry', () => {
- expect(secondEntry.nodes.length).toBe(1);
- expect(secondEntry.hasClass('selected')).toBe(false);
+ it('the first query scrolls into view', () => {
+ historyWrapper.node.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({
- query: 'third sql statement',
- start_time: new Date(2017, 11, 11, 1, 33, 5, 99),
- status: false,
- row_affected: 5,
- total_time: '26 msec',
- message: 'pretext ERROR: third sql message',
- });
+ });
- foundChildren = historyWrapper.find(QueryHistoryEntry);
+ describe('when a third SQL query is executed', () => {
+ beforeEach(() => {
+ historyCollection.add({
+ query: 'third sql statement',
+ start_time: new Date(2017, 11, 11, 1, 33, 5, 99),
+ status: false,
+ row_affected: 5,
+ total_time: '26 msec',
+ message: 'pretext ERROR: third sql message',
});
- it('displays third query SQL in the right pane', () => {
- expect(queryDetail.at(0).text()).toContain('third sql statement');
- });
+ queryEntries = historyWrapper.find(QueryHistoryEntry);
+ });
+
+ it('displays third query SQL in the right pane', () => {
+ expect(queryDetail.at(0).text()).toContain('third sql statement');
});
});
});
- describe('when the first query is outside the visible area', () => {
+ describe('when several days of queries were executed', () => {
+ let queryEntryDateGroups;
+
beforeEach(() => {
- spyOn(historyWrapper.node, 'isInvisible').and.callFake((element) => {
- return element.textContent.contains('first sql statement');
- });
+ jasmine.clock().install();
+ const mockedCurrentDate = moment('2017-07-01 13:30:00');
+ jasmine.clock().mockDate(mockedCurrentDate.toDate());
+
+ const historyObjects = [{
+ query: 'first today sql statement',
+ start_time: mockedCurrentDate.toDate(),
+ status: true,
+ row_affected: 12345,
+ total_time: '14 msec',
+ message: 'message from first today sql query',
+ }, {
+ query: 'second today sql statement',
+ start_time: mockedCurrentDate.clone().subtract(1, 'hours').toDate(),
+ status: false,
+ row_affected: 1,
+ total_time: '234 msec',
+ message: 'message from second today sql query',
+ }, {
+ query: 'first yesterday sql statement',
+ start_time: mockedCurrentDate.clone().subtract(1, 'days').toDate(),
+ status: true,
+ row_affected: 12345,
+ total_time: '14 msec',
+ message: 'message from first yesterday sql query',
+ }, {
+ query: 'second yesterday sql statement',
+ start_time: mockedCurrentDate.clone().subtract(1, 'days').subtract(1, 'hours').toDate(),
+ status: false,
+ row_affected: 1,
+ total_time: '234 msec',
+ message: 'message from second yesterday sql query',
+ }, {
+ query: 'older than yesterday sql statement',
+ start_time: mockedCurrentDate.clone().subtract(3, 'days').toDate(),
+ status: true,
+ row_affected: 12345,
+ total_time: '14 msec',
+ message: 'message from older than yesterday sql query',
+ }];
+ historyCollection = new HistoryCollection(historyObjects);
+
+ historyWrapper = mount();
+
+ const queryHistoryEntriesComponent = historyWrapper.find(QueryHistoryEntries);
+ isInvisibleSpy = spyOn(queryHistoryEntriesComponent.node, 'isInvisible')
+ .and.returnValue(false);
+
+ queryEntries = queryHistoryEntriesComponent.find(QueryHistoryEntry);
+ queryEntryDateGroups = queryHistoryEntriesComponent.find(QueryHistoryEntryDateGroup);
});
- describe('when the first query is the selected query', () => {
- describe('when refocus function is called', () => {
- let selectedListItem;
-
- beforeEach(() => {
- selectedListItem = ReactDOM.findDOMNode(historyWrapper.node)
- .getElementsByClassName('list-item')[0];
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
- spyOn(selectedListItem, 'focus');
- historyWrapper.node.refocus();
- });
+ describe('the history entries panel', () => {
+ it('has three query history entry data groups', () => {
+ expect(queryEntryDateGroups.length).toBe(3);
+ });
- it('the first query scrolls into view', function () {
- expect(selectedListItem.focus).toHaveBeenCalledTimes(1);
- });
+ 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');
});
});
});
diff --git a/web/yarn.lock b/web/yarn.lock
index 7c26413c..217d4911 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -3438,11 +3438,11 @@ hyphenate-style-name@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
-iconv-lite@0.4.15, iconv-lite@~0.4.13:
+iconv-lite@0.4.15:
version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
-iconv-lite@^0.4.15:
+iconv-lite@^0.4.15, iconv-lite@~0.4.13:
version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
@@ -5848,7 +5848,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
-"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@^1.0.33, readable-stream@~1.0.2:
+"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.2:
version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
dependencies:
@@ -5857,6 +5857,15 @@ read-pkg@^2.0.0:
isarray "0.0.1"
string_decoder "~0.10.x"
+readable-stream@^1.0.33, readable-stream@~1.1.9:
+ version "1.1.14"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.6:
version "2.3.1"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.1.tgz#84e26965bb9e785535ed256e8d38e92c69f09d10"
@@ -5869,15 +5878,6 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable
string_decoder "~1.0.0"
util-deprecate "~1.0.1"
-readable-stream@~1.1.9:
- version "1.1.14"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "0.0.1"
- string_decoder "~0.10.x"
-
readable-stream@~2.0.0:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"