Hi Hackers,

Here is a patch for query tools history UX improvements:

   - Add copy button for query text
   - Historical queries are binned by day

Thanks,
Sarah & Hao
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 (
       <div id="history-detail-query">
+        <button className={this.copyButtonClass()}
+                onClick={this.copyAllHandler}>{this.copyButtonText()}</button>
         <CodeMirror
           value={this.props.historyEntry.query}
           options={{
diff --git a/web/pgadmin/static/jsx/history/query_history.jsx 
b/web/pgadmin/static/jsx/history/query_history.jsx
index 3345b6bd..295a3665 100644
--- a/web/pgadmin/static/jsx/history/query_history.jsx
+++ b/web/pgadmin/static/jsx/history/query_history.jsx
@@ -12,8 +12,10 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import SplitPane from 'react-split-pane';
-import QueryHistoryEntry from './query_history_entry';
+import _ from 'underscore';
+
 import QueryHistoryDetail from './query_history_detail';
+import QueryHistoryEntries from './query_history_entries';
 import Shapes from '../react_shapes';
 
 const queryEntryListDivStyle = {
@@ -23,9 +25,6 @@ const queryDetailDivStyle = {
   display: 'flex',
 };
 
-const ARROWUP = 38;
-const ARROWDOWN = 40;
-
 export default class QueryHistory extends React.Component {
 
   constructor(props) {
@@ -36,28 +35,34 @@ export default class QueryHistory extends React.Component {
       selectedEntry: 0,
     };
 
-    this.onKeyDownHandler = this.onKeyDownHandler.bind(this);
-    this.navigateUpAndDown = this.navigateUpAndDown.bind(this);
+    this.selectHistoryEntry = this.selectHistoryEntry.bind(this);
   }
 
   componentWillMount() {
-    this.resetCurrentHistoryDetail(this.props.historyCollection.historyList);
+    this.setHistory(this.props.historyCollection.historyList);
+    this.selectHistoryEntry(0);
+
     this.props.historyCollection.onChange((historyList) => {
-      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 (
       <SplitPane defaultSize='50%' split='vertical' 
pane1Style={queryEntryListDivStyle}
                  pane2Style={queryDetailDivStyle}>
-        <div id='query_list'
-             className='query-history'
-             onKeyDown={this.navigateUpAndDown}
-             tabIndex={-1}>
-          <ul>
-            {this.retrieveOrderedHistory()
-              .map((entry, index) =>
-                <li key={index} className='list-item'
-                    onClick={this.onClickHandler.bind(this, index)}
-                    tabIndex={-1}>
-                  <QueryHistoryEntry
-                    historyEntry={entry}
-                    isSelected={index == this.state.selectedEntry}/>
-                </li>)
-              .value()
-            }
-          </ul>
-        </div>
-        <QueryHistoryDetail historyEntry={this.getCurrentHistoryDetail()}/>
+        <QueryHistoryEntries historyEntries={this.state.history}
+                             selectedEntry={this.state.selectedEntry}
+                             onSelectEntry={this.selectHistoryEntry}
+        />
+        <QueryHistoryDetail historyEntry={this.state.currentHistoryDetail}/>
       </SplitPane>);
   }
 }
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(
+        <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: 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 (<div 
className="date-label">{this.getDatePrefix()}{this.getDateFormatted(this.getDateMoment())}</div>);
+  }
+}
+
+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
-<div class="alert-row">
-  <div class="alert alert-success text-14 alert-box">
-    <div class="media">
-      <div class="media-body media-middle">
-        <div class="alert-icon success-icon">
-          <i class="fa fa-check" aria-hidden="true"></i>
-        </div>
-        <div class="alert-text">
-          Successfully run. Total query runtime: 32 msec. 1 row retrieved
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="alert-row">
-  <div class="alert alert-danger font-red text-14 alert-box">
-    <div class="media">
-      <div class="media-body media-middle">
-        <div class="alert-icon error-icon">
-          <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
-        </div>
-        <div class="alert-text">
-          Error retrieving properties - INTERNAL SERVER ERROR
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="alert-row">
-  <div class="alert alert-info font-blue text-14 alert-box">
-    <div class="media">
-      <div class="media-body media-middle">
-        <div class="alert-text">
-          This is a neutral message
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-
-```
-*/
-
-
-// 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
-<div class="row">
-  <div class="row">
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-gray-1">
-        #f9f9f9
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-gray-2">
-        #e8e8e8
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-gray-3">
-        #cccccc
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-gray-4">
-        #888888
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-gray-5 font-white">
-        #555555
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-gray-6 font-white">
-        #333333
-      </div>
-    </div>
-  </div>
-</div>
-```
-
-*/
-
-.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
-<div class="row">
-  <div class="row">
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-blue-1">
-        #e7f2ff
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-blue-2">
-        #84acdd
-      </div>
-    </div>
-  </div>
-</div>
-<br>
-<div class="row">
-  <div class="row">
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-red-1">
-        #f2dede
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-red-2">
-        #de8585
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-red-3">
-        #d0021b
-      </div>
-    </div>
-  </div>
-</div>
-<br>
-<div class="row">
-  <div class="row">
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-green-1">
-        #dff0d7
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-green-2">
-        #a2c189
-      </div>
-    </div>
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-green-3">
-        #3a773a
-      </div>
-    </div>
-  </div>
-</div>
-```
-
-*/
-
-.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
-<div class="row">
-  <div class="row">
-    <div class="col-xs-6 col-md-3">
-      <div class="color-chip bg-primary-blue font-white">
-        #2c76b4
-      </div>
-    </div>
-  </div>
-</div>
-
-```
-
-*/
-
-.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
-<div class="text-14">
-  Body 14 px Helvetica Neue
-</div>
-
-<div class="text-14 text-bold">
-  Body 14 px Helvetica Neue bold
-</div>
-
-<div class="text-13">
-  Body 13 px Helvetica Neue
-</div>
-
-<div class="text-13 text-bold">
-  Body 13 px Helvetica Neue bold
-</div>
-
-<div class="text-12">
-  Body 12 px Helvetica Neue
-</div>
-
-<div class="text-12 text-bold">
-  Body 12 px Helvetica Neue bold
-</div>
-
-<div class="text-11">
-  Body 11 px Helvetica Neue
-</div>
-
-<div class="text-11 text-bold">
-  Body 11 px Helvetica Neue bold
-</div>
-```
-
-*/
+$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(<QueryHistory 
historyCollection={historyCollection}/>);
-    });
+    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(<QueryHistory 
historyCollection={historyCollection}/>);
+
+        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(<QueryHistory 
historyCollection={historyCollection}/>);
+
+        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";

Reply via email to