UX improvements of the history in the query tool.

- Added copy button for query text.
- Historical queries are binned/grouped by day.

Patch By: Hao Wang, Sarah McAlear
This commit is contained in:
Hao Wang 2017-08-09 16:39:06 +05:30 committed by Ashesh Vashi
parent 33bd9d4782
commit 5141debae7
16 changed files with 785 additions and 695 deletions

View File

@ -72,13 +72,14 @@ class QueryToolJourneyTest(BaseFeatureTest):
self.__clear_query_tool() self.__clear_query_tool()
editor_input = self.page.find_by_id("output-panel") editor_input = self.page.find_by_id("output-panel")
self.page.click_element(editor_input) 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") 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") 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) \ ActionChains(self.page.driver) \
.send_keys(Keys.ARROW_DOWN) \ .send_keys(Keys.ARROW_DOWN) \
.perform() .perform()
@ -86,10 +87,30 @@ class QueryToolJourneyTest(BaseFeatureTest):
self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_entry.text) self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_entry.text)
selected_history_detail_pane = self.page.find_by_id("query_detail") 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) 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) self.page.click_element(newly_selected_history_entry)
selected_history_detail_pane = self.page.find_by_id("query_detail") 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.__clear_query_tool()
self.page.click_element(editor_input) self.page.click_element(editor_input)

View File

@ -268,6 +268,7 @@
.wcFrameTitleBar { .wcFrameTitleBar {
background-color: #e8e8e8; background-color: #e8e8e8;
height: 35px; height: 35px;
border-bottom: #cccccc;
} }
.wcFloating .wcFrameTitleBar { .wcFloating .wcFrameTitleBar {

View File

@ -12,11 +12,49 @@ import 'codemirror/mode/sql/sql';
import CodeMirror from './code_mirror'; import CodeMirror from './code_mirror';
import Shapes from '../../react_shapes'; import Shapes from '../../react_shapes';
import clipboard from '../../../js/selection/clipboard';
export default class HistoryDetailQuery extends React.Component { 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() { render() {
return ( return (
<div id="history-detail-query"> <div id="history-detail-query">
<button className={this.copyButtonClass()}
onClick={this.copyAllHandler}>{this.copyButtonText()}</button>
<CodeMirror <CodeMirror
value={this.props.historyEntry.query} value={this.props.historyEntry.query}
options={{ options={{

View File

@ -12,8 +12,10 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import QueryHistoryEntry from './query_history_entry'; import _ from 'underscore';
import QueryHistoryDetail from './query_history_detail'; import QueryHistoryDetail from './query_history_detail';
import QueryHistoryEntries from './query_history_entries';
import Shapes from '../react_shapes'; import Shapes from '../react_shapes';
const queryEntryListDivStyle = { const queryEntryListDivStyle = {
@ -23,9 +25,6 @@ const queryDetailDivStyle = {
display: 'flex', display: 'flex',
}; };
const ARROWUP = 38;
const ARROWDOWN = 40;
export default class QueryHistory extends React.Component { export default class QueryHistory extends React.Component {
constructor(props) { constructor(props) {
@ -36,28 +35,34 @@ export default class QueryHistory extends React.Component {
selectedEntry: 0, selectedEntry: 0,
}; };
this.onKeyDownHandler = this.onKeyDownHandler.bind(this); this.selectHistoryEntry = this.selectHistoryEntry.bind(this);
this.navigateUpAndDown = this.navigateUpAndDown.bind(this);
} }
componentWillMount() { componentWillMount() {
this.resetCurrentHistoryDetail(this.props.historyCollection.historyList); this.setHistory(this.props.historyCollection.historyList);
this.selectHistoryEntry(0);
this.props.historyCollection.onChange((historyList) => { this.props.historyCollection.onChange((historyList) => {
this.resetCurrentHistoryDetail(historyList); this.setHistory(historyList);
this.selectHistoryEntry(0);
}); });
this.props.historyCollection.onReset((historyList) => { this.props.historyCollection.onReset(() => {
this.clearCurrentHistoryDetail(historyList); this.setState({
history: [],
currentHistoryDetail: undefined,
selectedEntry: 0,
});
}); });
} }
componentDidMount() { componentDidMount() {
this.resetCurrentHistoryDetail(this.state.history); this.selectHistoryEntry(0);
} }
refocus() { refocus() {
if (this.state.history.length > 0) { 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]; .getElementsByClassName('selected')[0];
} }
getCurrentHistoryDetail() { setHistory(historyList) {
return this.state.currentHistoryDetail; this.setState({history: this.orderedHistory(historyList)});
} }
setCurrentHistoryDetail(index, historyList) { selectHistoryEntry(index) {
this.setState({ this.setState({
history: historyList, currentHistoryDetail: this.state.history[index],
currentHistoryDetail: this.retrieveOrderedHistory().value()[index],
selectedEntry: index, selectedEntry: index,
}); });
} }
resetCurrentHistoryDetail(historyList) { orderedHistory(historyList) {
this.setCurrentHistoryDetail(0, historyList); return _.chain(historyList)
}
clearCurrentHistoryDetail(historyList) {
this.setState({
history: historyList,
currentHistoryDetail: undefined,
selectedEntry: 0,
});
}
retrieveOrderedHistory() {
return _.chain(this.state.history)
.sortBy(historyEntry => historyEntry.start_time) .sortBy(historyEntry => historyEntry.start_time)
.reverse(); .reverse()
} .value();
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;
} }
render() { render() {
return ( return (
<SplitPane defaultSize='50%' split='vertical' pane1Style={queryEntryListDivStyle} <SplitPane defaultSize='50%' split='vertical' pane1Style={queryEntryListDivStyle}
pane2Style={queryDetailDivStyle}> pane2Style={queryDetailDivStyle}>
<div id='query_list' <QueryHistoryEntries historyEntries={this.state.history}
className='query-history' selectedEntry={this.state.selectedEntry}
onKeyDown={this.navigateUpAndDown} onSelectEntry={this.selectHistoryEntry}
tabIndex={-1}> />
<ul> <QueryHistoryDetail historyEntry={this.state.currentHistoryDetail}/>
{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()}/>
</SplitPane>); </SplitPane>);
} }
} }

View File

@ -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,
};

View File

@ -13,7 +13,7 @@ import moment from 'moment';
export default class QueryHistoryEntry extends React.Component { export default class QueryHistoryEntry extends React.Component {
formatDate(date) { formatDate(date) {
return (moment(date).format('MMM D YYYY [] HH:mm:ss')); return (moment(date).format('HH:mm:ss'));
} }
renderWithClasses(outerDivStyle) { renderWithClasses(outerDivStyle) {

View File

@ -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';

View File

@ -1,92 +1,14 @@
/*doc .alert-icon {
---
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; display: flex;
} align-items: center;
.media-body { color: white;
flex: 1; padding: 15px 15px 15px 17px;
} width: 50px;
.media-middle { min-height: 50px;
align-self: center; font-size: 14px;
} text-align: center;
.media-bottom { align-self: stretch;
align-self: flex-end; flex-shrink: 0;
}
} @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-row { .alert-row {
@ -103,22 +25,12 @@ category: alerts
padding: 15px; padding: 15px;
} }
.alert-icon {
display: inline-block;
color: white;
padding: 15px;
width: 50px;
height: 50px;
font-size: 14px;
text-align: center;
}
.success-icon { .success-icon {
background: #3a773a; background: $color-green-3;
} }
.error-icon { .error-icon {
background: #d0021b; background: $color-red-3;
} }
.info-icon { .info-icon {
@ -128,17 +40,27 @@ category: alerts
.alert-text { .alert-text {
display: inline-block; display: inline-block;
padding: 0 12px 0 10px; padding: 0 12px 0 10px;
align-self: center;
// To make sure IE picks up the correct font // To make sure IE picks up the correct font
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
} }
.alert-info { .alert-info {
border-color: #84acdd border-color: $color-blue-2;
background-image: none;
} }
.media-body { .alert-danger {
vertical-align: top; background-image: none;
width: initial; }
.grid-error, .graph-error {
.alert-row {
align-items: center;
height: 100%;
display: flex;
justify-content: center;
}
} }
.ajs-message { .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 { .pg-prop-status-bar {
padding: 5px; padding: 5px;
.media-body { .media-body {
display: flex; display: flex;
width: auto;
} }
.alert-icon { .alert-icon {
padding: 8px; padding: 8px 8px 8px 10.5px;
width: 35px; width: 35px;
height: 35px; height: 35px;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
min-height: auto;
} }
.alert-text { .alert-text {
@ -167,8 +106,8 @@ category: alerts
border: 1px solid $color-red-2; border: 1px solid $color-red-2;
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
padding: 7px 12px 5px 10px; padding: 7px 12px 6px 10px;
border-left: 0px; border-left: none;
} }
.error-in-footer { .error-in-footer {
@ -193,7 +132,28 @@ category: alerts
height: 35px; height: 35px;
.alert-text { .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%;
}
}

View File

@ -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-1: #f9f9f9;
$color-gray-2: #e8e8e8; $color-gray-2: #e8e8e8;
$color-gray-3: #cccccc; $color-gray-3: #cccccc;
@ -120,7 +60,3 @@ $color-gray-6: #333333;
.font-gray-6 { .font-gray-6 {
color: $color-gray-6; color: $color-gray-6;
} }
.font-white {
color: #FFFFFF;
}

View File

@ -1,99 +1,22 @@
/*doc $color-blue-1: #e7f2ff;
--- $color-blue-2: #84acdd;
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-red-1: #f2dede; $color-red-1: #f2dede;
$color-red-2: #de8585; $color-red-2: #de8585;
$color-red-3: #d0021b; $color-red-3: #d0021b;
$color-green-1: #dff0d7;
$color-green-2: #a2c189; $color-green-2: #a2c189;
$color-green-3: #3a773a;
.bg-white-1 { .bg-white-1 {
background-color: #ffffff; background-color: #ffffff;
} }
.bg-blue-1 { .bg-blue-1 {
background-color: #e7f2ff; background-color: $color-blue-1;
} }
.bg-blue-2 { .bg-blue-2 {
background-color: #84acdd; background-color: $color-blue-2;
} }
.bg-red-1 { .bg-red-1 {
@ -109,23 +32,23 @@ $color-green-2: #a2c189;
} }
.bg-green-1 { .bg-green-1 {
background-color: #dff0d7; background-color: $color-green-1;
} }
.bg-green-2 { .bg-green-2 {
background-color: #a2c189; background-color: $color-green-2;
} }
.bg-green-3 { .bg-green-3 {
background-color: #3a773a; background-color: $color-green-3;
} }
.border-blue-1 { .border-blue-1 {
border-color: #e7f2ff; border-color: $color-blue-1;
} }
.border-blue-2 { .border-blue-2 {
border-color: #84acdd; border-color: $color-blue-2;
} }
.border-red-1 { .border-red-1 {
@ -141,15 +64,15 @@ $color-green-2: #a2c189;
} }
.border-green-1 { .border-green-1 {
border-color: #dff0d7; border-color: $color-green-1;
} }
.border-green-2 { .border-green-2 {
border-color: #a2c189; border-color: $color-green-2;
} }
.border-green-3 { .border-green-3 {
border-color: #3a773a; border-color: $color-green-3;
} }
.font-red-3 { .font-red-3 {
@ -157,9 +80,5 @@ $color-green-2: #a2c189;
} }
.font-green-3 { .font-green-3 {
color: #3a773a; color: $color-green-3;
}
.font-white {
color: #FFFFFF;
} }

View File

@ -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; $primary-blue: #2c76b4;
.bg-primary-blue { .bg-primary-blue {

View File

@ -1,68 +1,25 @@
/*doc $font-family-1: "Helvetica Neue", Arial, sans-serif;
---
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>
```
*/
.text-bold { .text-bold {
font-weight: bold; font-weight: bold;
} }
.text-14 { .text-14 {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: $font-family-1;
font-size: 14px; font-size: 14px;
} }
.text-13 { .text-13 {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: $font-family-1;
font-size: 13px; font-size: 13px;
} }
.text-12 { .text-12 {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: $font-family-1;
font-size: 12px; font-size: 12px;
} }
.text-11 { .text-11 {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: $font-family-1;
font-size: 11px; font-size: 11px;
} }

View File

@ -7,10 +7,10 @@
.entry { .entry {
@extend .text-14; @extend .text-14;
@extend .bg-white-1; @extend .bg-white-1;
padding: -2px 18px -2px 8px;
font-family: monospace; font-family: monospace;
border: 2px solid transparent; border: 2px solid transparent;
margin-left: 1px; margin-left: 1px;
padding: 0 8px 0 5px;
.other-info { .other-info {
@extend .text-13; @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 { .entry.error {
@extend .bg-red-1; @extend .bg-red-1;
} }
@ -114,6 +124,32 @@
margin-right: 10px; margin-right: 10px;
height: 0; height: 0;
position: relative; 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 { .block-divider {

View File

@ -215,7 +215,7 @@ define('tools.querytool', [
var history = new pgAdmin.Browser.Panel({ var history = new pgAdmin.Browser.Panel({
name: 'history', name: 'history',
title: gettext("History"), title: gettext("Query History"),
width: '100%', width: '100%',
height:'100%', height:'100%',
isCloseable: false, isCloseable: false,

View File

@ -12,10 +12,15 @@
import jasmineEnzyme from 'jasmine-enzyme'; import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import moment from 'moment';
import QueryHistory from '../../../pgadmin/static/jsx/history/query_history'; import QueryHistory from '../../../pgadmin/static/jsx/history/query_history';
import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry'; 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 QueryHistoryDetail from '../../../pgadmin/static/jsx/history/query_history_detail';
import HistoryCollection from '../../../pgadmin/static/js/history/history_collection'; import HistoryCollection from '../../../pgadmin/static/js/history/history_collection';
import clipboard from '../../../pgadmin/static/js/selection/clipboard';
import {mount} from 'enzyme'; import {mount} from 'enzyme';
@ -50,7 +55,7 @@ describe('QueryHistory', () => {
done(); done();
}); });
it('nothing is displayed on right panel', (done) => { it('nothing is displayed in the history details panel', (done) => {
let foundChildren = historyWrapper.find(QueryHistoryDetail); let foundChildren = historyWrapper.find(QueryHistoryDetail);
expect(foundChildren.length).toBe(1); expect(foundChildren.length).toBe(1);
done(); done();
@ -58,10 +63,14 @@ describe('QueryHistory', () => {
}); });
describe('when there is history', () => { describe('when there is history', () => {
let historyObjects; let queryEntries;
let queryDetail;
let isInvisibleSpy;
beforeEach(function () { describe('when two SQL queries were executed', () => {
historyObjects = [{
beforeEach(() => {
const historyObjects = [{
query: 'first sql statement', query: 'first sql statement',
start_time: new Date(2017, 5, 3, 14, 3, 15, 150), start_time: new Date(2017, 5, 3, 14, 3, 15, 150),
status: true, status: true,
@ -79,55 +88,50 @@ describe('QueryHistory', () => {
historyCollection = new HistoryCollection(historyObjects); historyCollection = new HistoryCollection(historyObjects);
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>); historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>);
});
describe('when all query entries are visible in the pane', () => { const queryHistoryEntriesComponent = historyWrapper.find(QueryHistoryEntries);
describe('when two SQL queries were executed', () => { isInvisibleSpy = spyOn(queryHistoryEntriesComponent.node, 'isInvisible')
let foundChildren; .and.returnValue(false);
let queryDetail;
beforeEach(() => {
spyOn(historyWrapper.node, 'isInvisible').and.returnValue(false);
foundChildren = historyWrapper.find(QueryHistoryEntry);
queryEntries = queryHistoryEntriesComponent.find(QueryHistoryEntry);
queryDetail = historyWrapper.find(QueryHistoryDetail); queryDetail = historyWrapper.find(QueryHistoryDetail);
}); });
describe('the main pane', () => { describe('the history entries panel', () => {
it('has two query history entries', () => { it('has two query history entries', () => {
expect(foundChildren.length).toBe(2); expect(queryEntries.length).toBe(2);
}); });
it('displays the query history entries in order', () => { it('displays the query history entries in order', () => {
expect(foundChildren.at(0).text()).toContain('first sql statement'); expect(queryEntries.at(0).text()).toContain('first sql statement');
expect(foundChildren.at(1).text()).toContain('second sql statement'); expect(queryEntries.at(1).text()).toContain('second sql statement');
}); });
it('displays the formatted timestamp of the queries in chronological order by most recent first', () => { 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(queryEntries.at(0).find('.timestamp').text()).toBe('14:03:15');
expect(foundChildren.at(1).text()).toContain('Dec 11 2016 01:33:05'); expect(queryEntries.at(1).find('.timestamp').text()).toBe('01:33:05');
}); });
it('renders the most recent query as selected', () => { it('renders the most recent query as selected', () => {
expect(foundChildren.at(0).nodes.length).toBe(1); expect(queryEntries.at(0).nodes.length).toBe(1);
expect(foundChildren.at(0).hasClass('selected')).toBeTruthy(); expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
}); });
it('renders the older query as not selected', () => { it('renders the older query as not selected', () => {
expect(foundChildren.at(1).nodes.length).toBe(1); expect(queryEntries.at(1).nodes.length).toBe(1);
expect(foundChildren.at(1).hasClass('selected')).toBeFalsy(); expect(queryEntries.at(1).hasClass('selected')).toBeFalsy();
expect(foundChildren.at(1).hasClass('error')).toBeTruthy(); expect(queryEntries.at(1).hasClass('error')).toBeTruthy();
}); });
describe('when the selected query is the most recent', () => { describe('when the selected query is the most recent', () => {
describe('when we press arrow down', () => { describe('when we press arrow down', () => {
beforeEach(() => { beforeEach(() => {
pressArrowDownKey(foundChildren.parent().at(0)); pressArrowDownKey(queryEntries.parent().at(0));
}); });
it('should select the next query', () => { it('should select the next query', () => {
expect(foundChildren.at(1).nodes.length).toBe(1); expect(queryEntries.at(1).nodes.length).toBe(1);
expect(foundChildren.at(1).hasClass('selected')).toBeTruthy(); expect(queryEntries.at(1).hasClass('selected')).toBeTruthy();
}); });
it('should display the corresponding detail on the right pane', () => { it('should display the corresponding detail on the right pane', () => {
@ -136,34 +140,39 @@ describe('QueryHistory', () => {
describe('when arrow down pressed again', () => { describe('when arrow down pressed again', () => {
it('should not change the selected query', () => { it('should not change the selected query', () => {
pressArrowDownKey(foundChildren.parent().at(0)); pressArrowDownKey(queryEntries.parent().at(0));
expect(foundChildren.at(1).nodes.length).toBe(1); expect(queryEntries.at(1).nodes.length).toBe(1);
expect(foundChildren.at(1).hasClass('selected')).toBeTruthy(); expect(queryEntries.at(1).hasClass('selected')).toBeTruthy();
}); });
}); });
describe('when arrow up is pressed', () => { describe('when arrow up is pressed', () => {
it('should select the most recent query', () => { it('should select the most recent query', () => {
pressArrowUpKey(foundChildren.parent().at(0)); pressArrowUpKey(queryEntries.parent().at(0));
expect(foundChildren.at(0).nodes.length).toBe(1); expect(queryEntries.at(0).nodes.length).toBe(1);
expect(foundChildren.at(0).hasClass('selected')).toBeTruthy(); expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
}); });
}); });
}); });
describe('when arrow up is pressed', () => { describe('when arrow up is pressed', () => {
it('should not change the selected query', () => { it('should not change the selected query', () => {
pressArrowUpKey(foundChildren.parent().at(0)); pressArrowUpKey(queryEntries.parent().at(0));
expect(foundChildren.at(0).nodes.length).toBe(1); expect(queryEntries.at(0).nodes.length).toBe(1);
expect(foundChildren.at(0).hasClass('selected')).toBeTruthy(); expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
}); });
}); });
}); });
}); });
describe('the details pane', () => { describe('the historydetails panel', () => {
let copyAllButton;
beforeEach(() => {
copyAllButton = () => queryDetail.find('#history-detail-query > button');
});
it('displays the formatted timestamp', () => { it('displays the formatted timestamp', () => {
expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date'); expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date');
}); });
@ -191,13 +200,105 @@ describe('QueryHistory', () => {
}, 1000); }, 1000);
}); });
describe('when the "Copy All" button is clicked', () => {
beforeEach(() => {
spyOn(clipboard, 'copyTextToClipboard');
copyAllButton().simulate('click');
});
it('copies the query to the clipboard', () => {
expect(clipboard.copyTextToClipboard).toHaveBeenCalledWith('first sql statement');
});
});
describe('Copy button', () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should have text \'Copy All\'', () => {
expect(copyAllButton().text()).toBe('Copy All');
});
it('should not have the class \'was-copied\'', () => {
expect(copyAllButton().hasClass('was-copied')).toBe(false);
});
describe('when the copy button is clicked', () => {
beforeEach(() => {
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);
});
});
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 query failed', () => { describe('when the query failed', () => {
let failedEntry; let failedEntry;
beforeEach(() => { beforeEach(() => {
failedEntry = foundChildren.at(1); failedEntry = queryEntries.at(1);
failedEntry.simulate('click'); failedEntry.simulate('click');
}); });
it('displays the error message on top of the details pane', () => { it('displays the error message on top of the details pane', () => {
expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query'); expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query');
}); });
@ -208,8 +309,8 @@ describe('QueryHistory', () => {
let firstEntry, secondEntry; let firstEntry, secondEntry;
beforeEach(() => { beforeEach(() => {
firstEntry = foundChildren.at(0); firstEntry = queryEntries.at(0);
secondEntry = foundChildren.at(1); secondEntry = queryEntries.at(1);
secondEntry.simulate('click'); secondEntry.simulate('click');
}); });
@ -219,48 +320,49 @@ describe('QueryHistory', () => {
it('deselects the first history entry', () => { it('deselects the first history entry', () => {
expect(firstEntry.nodes.length).toBe(1); expect(firstEntry.nodes.length).toBe(1);
expect(firstEntry.hasClass('selected')).toBe(false); expect(firstEntry.hasClass('selected')).toBeFalsy();
}); });
it('selects the second history entry', () => { it('selects the second history entry', () => {
expect(secondEntry.nodes.length).toBe(1); expect(secondEntry.nodes.length).toBe(1);
expect(secondEntry.hasClass('selected')).toBe(true); expect(secondEntry.hasClass('selected')).toBeTruthy();
}); });
}); });
describe('when the user clicks inside the main pane but not in any history entry', () => { describe('when the first query is outside the visible area', () => {
let queryList; beforeEach(() => {
let firstEntry, secondEntry; isInvisibleSpy.and.callFake((element) => {
return element.textContent.contains('first sql statement');
});
});
describe('when the first query is the selected query', () => {
describe('when refocus function is called', () => {
let selectedListItem;
beforeEach(() => { beforeEach(() => {
firstEntry = foundChildren.at(0); selectedListItem = ReactDOM.findDOMNode(historyWrapper.node)
secondEntry = foundChildren.at(1); .getElementsByClassName('selected')[0].parentElement;
queryList = historyWrapper.find('#query_list');
secondEntry.simulate('click'); spyOn(selectedListItem, 'focus');
queryList.simulate('click');
jasmine.clock().install();
}); });
it('should not change the selected entry', () => { afterEach(() => {
expect(firstEntry.hasClass('selected')).toBe(false); jasmine.clock().uninstall();
expect(secondEntry.hasClass('selected')).toBe(true);
}); });
describe('when up arrow is keyed', () => { it('the first query scrolls into view', () => {
beforeEach(() => { historyWrapper.node.refocus();
pressArrowUpKey(queryList); expect(selectedListItem.focus).toHaveBeenCalledTimes(0);
jasmine.clock().tick(1);
expect(selectedListItem.focus).toHaveBeenCalledTimes(1);
});
});
}); });
it('selects the first history entry', () => {
expect(firstEntry.nodes.length).toBe(1);
expect(firstEntry.hasClass('selected')).toBe(true);
});
it('deselects the second history entry', () => {
expect(secondEntry.nodes.length).toBe(1);
expect(secondEntry.hasClass('selected')).toBe(false);
});
});
}); });
describe('when a third SQL query is executed', () => { describe('when a third SQL query is executed', () => {
@ -274,7 +376,7 @@ describe('QueryHistory', () => {
message: 'pretext ERROR: third sql message', message: 'pretext ERROR: third sql message',
}); });
foundChildren = historyWrapper.find(QueryHistoryEntry); queryEntries = historyWrapper.find(QueryHistoryEntry);
}); });
it('displays third query SQL in the right pane', () => { it('displays third query SQL in the right pane', () => {
@ -282,30 +384,76 @@ describe('QueryHistory', () => {
}); });
}); });
}); });
});
describe('when the first query is outside the visible area', () => { describe('when several days of queries were executed', () => {
beforeEach(() => { let queryEntryDateGroups;
spyOn(historyWrapper.node, 'isInvisible').and.callFake((element) => {
return element.textContent.contains('first sql statement');
});
});
describe('when the first query is the selected query', () => {
describe('when refocus function is called', () => {
let selectedListItem;
beforeEach(() => { beforeEach(() => {
selectedListItem = ReactDOM.findDOMNode(historyWrapper.node) jasmine.clock().install();
.getElementsByClassName('list-item')[0]; const mockedCurrentDate = moment('2017-07-01 13:30:00');
jasmine.clock().mockDate(mockedCurrentDate.toDate());
spyOn(selectedListItem, 'focus'); const historyObjects = [{
historyWrapper.node.refocus(); 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);
}); });
it('the first query scrolls into view', function () { afterEach(() => {
expect(selectedListItem.focus).toHaveBeenCalledTimes(1); jasmine.clock().uninstall();
}); });
describe('the history entries panel', () => {
it('has three query history entry data groups', () => {
expect(queryEntryDateGroups.length).toBe(3);
});
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');
}); });
}); });
}); });

View File

@ -3438,11 +3438,11 @@ hyphenate-style-name@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b" 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" version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" 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" version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" 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" normalize-package-data "^2.3.2"
path-type "^2.0.0" 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" version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
dependencies: dependencies:
@ -5857,6 +5857,15 @@ read-pkg@^2.0.0:
isarray "0.0.1" isarray "0.0.1"
string_decoder "~0.10.x" 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: 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" version "2.3.1"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.1.tgz#84e26965bb9e785535ed256e8d38e92c69f09d10" 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" string_decoder "~1.0.0"
util-deprecate "~1.0.1" 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: readable-stream@~2.0.0:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"