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()
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)

View File

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

View File

@ -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={{

View File

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

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 {
formatDate(date) {
return (moment(date).format('MMM D YYYY [] HH:mm:ss'));
return (moment(date).format('HH:mm:ss'));
}
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
---
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;
}
.media-body {
vertical-align: top;
width: initial;
.alert-danger {
background-image: none;
}
.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%;
}
}

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

View File

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

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;
.bg-primary-blue {
@ -46,4 +10,4 @@ $primary-blue: #2c76b4;
.font-primary-blue {
color: $primary-blue;
}
}

View File

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

View File

@ -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 {

View File

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

View File

@ -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;
let queryEntries;
let queryDetail;
let isInvisibleSpy;
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);
describe('when two SQL queries were executed', () => {
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>);
});
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);
describe('when all query entries are visible in the pane', () => {
describe('when two SQL queries were executed', () => {
let foundChildren;
let queryDetail;
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>);
beforeEach(() => {
spyOn(historyWrapper.node, 'isInvisible').and.returnValue(false);
foundChildren = historyWrapper.find(QueryHistoryEntry);
const queryHistoryEntriesComponent = historyWrapper.find(QueryHistoryEntries);
isInvisibleSpy = spyOn(queryHistoryEntriesComponent.node, 'isInvisible')
.and.returnValue(false);
queryDetail = historyWrapper.find(QueryHistoryDetail);
queryEntries = queryHistoryEntriesComponent.find(QueryHistoryEntry);
queryDetail = historyWrapper.find(QueryHistoryDetail);
});
describe('the history entries panel', () => {
it('has two query history entries', () => {
expect(queryEntries.length).toBe(2);
});
describe('the main pane', () => {
it('has two query history entries', () => {
expect(foundChildren.length).toBe(2);
});
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');
});
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('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 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 most recent query as selected', () => {
expect(queryEntries.at(0).nodes.length).toBe(1);
expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
});
it('renders the most recent query as selected', () => {
expect(foundChildren.at(0).nodes.length).toBe(1);
expect(foundChildren.at(0).hasClass('selected')).toBeTruthy();
});
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 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();
});
describe('when the selected query is the most recent', () => {
describe('when we press arrow down', () => {
beforeEach(() => {
pressArrowDownKey(queryEntries.parent().at(0));
});
describe('when the selected query is the most recent', () => {
describe('when we press arrow down', () => {
beforeEach(() => {
pressArrowDownKey(foundChildren.parent().at(0));
});
it('should select the next query', () => {
expect(queryEntries.at(1).nodes.length).toBe(1);
expect(queryEntries.at(1).hasClass('selected')).toBeTruthy();
});
it('should select the next query', () => {
expect(foundChildren.at(1).nodes.length).toBe(1);
expect(foundChildren.at(1).hasClass('selected')).toBeTruthy();
});
it('should display the corresponding detail on the right pane', () => {
expect(queryDetail.at(0).text()).toContain('message from second sql query');
});
it('should display the corresponding detail on the right pane', () => {
expect(queryDetail.at(0).text()).toContain('message from second sql query');
});
describe('when arrow down pressed again', () => {
it('should not change the selected query', () => {
pressArrowDownKey(queryEntries.parent().at(0));
describe('when arrow down pressed again', () => {
it('should not change the selected query', () => {
pressArrowDownKey(foundChildren.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(1).nodes.length).toBe(1);
expect(queryEntries.at(1).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();
it('should select the most recent query', () => {
pressArrowUpKey(queryEntries.parent().at(0));
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(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');
});
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('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('the details pane', () => {
it('displays the formatted timestamp', () => {
expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date');
describe('when the query failed', () => {
let failedEntry;
beforeEach(() => {
failedEntry = queryEntries.at(1);
failedEntry.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('displays the error message on top of the details pane', () => {
expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query');
});
});
});
it('displays the total time', () => {
expect(queryDetail.at(0).text()).toContain('14 msecDuration');
describe('when the older query is clicked on', () => {
let firstEntry, secondEntry;
beforeEach(() => {
firstEntry = queryEntries.at(0);
secondEntry = queryEntries.at(1);
secondEntry.simulate('click');
});
it('displays the query in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('second sql statement');
});
it('deselects the first history entry', () => {
expect(firstEntry.nodes.length).toBe(1);
expect(firstEntry.hasClass('selected')).toBeFalsy();
});
it('selects the second history entry', () => {
expect(secondEntry.nodes.length).toBe(1);
expect(secondEntry.hasClass('selected')).toBeTruthy();
});
});
describe('when the first query is outside the visible area', () => {
beforeEach(() => {
isInvisibleSpy.and.callFake((element) => {
return element.textContent.contains('first sql statement');
});
});
it('displays the full message', () => {
expect(queryDetail.at(0).text()).toContain('message from first sql query');
});
it('displays first query SQL', (done) => {
setTimeout(() => {
expect(queryDetail.at(0).text()).toContain('first sql statement');
done();
}, 1000);
});
describe('when the query failed', () => {
let failedEntry;
describe('when the first query is the selected query', () => {
describe('when refocus function is called', () => {
let selectedListItem;
beforeEach(() => {
failedEntry = foundChildren.at(1);
failedEntry.simulate('click');
selectedListItem = ReactDOM.findDOMNode(historyWrapper.node)
.getElementsByClassName('selected')[0].parentElement;
spyOn(selectedListItem, 'focus');
jasmine.clock().install();
});
it('displays the error message on top of the details pane', () => {
expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query');
afterEach(() => {
jasmine.clock().uninstall();
});
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 the older query is clicked on', () => {
let firstEntry, secondEntry;
});
beforeEach(() => {
firstEntry = foundChildren.at(0);
secondEntry = foundChildren.at(1);
secondEntry.simulate('click');
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 the query in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('second sql statement');
});
it('deselects the first history entry', () => {
expect(firstEntry.nodes.length).toBe(1);
expect(firstEntry.hasClass('selected')).toBe(false);
});
it('selects the second history entry', () => {
expect(secondEntry.nodes.length).toBe(1);
expect(secondEntry.hasClass('selected')).toBe(true);
});
queryEntries = historyWrapper.find(QueryHistoryEntry);
});
describe('when the user clicks inside the main pane but not in any history entry', () => {
let queryList;
let firstEntry, secondEntry;
beforeEach(() => {
firstEntry = foundChildren.at(0);
secondEntry = foundChildren.at(1);
queryList = historyWrapper.find('#query_list');
secondEntry.simulate('click');
queryList.simulate('click');
});
it('should not change the selected entry', () => {
expect(firstEntry.hasClass('selected')).toBe(false);
expect(secondEntry.hasClass('selected')).toBe(true);
});
describe('when up arrow is keyed', () => {
beforeEach(() => {
pressArrowUpKey(queryList);
});
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', () => {
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);
});
it('displays third query SQL in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('third sql statement');
});
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;
afterEach(() => {
jasmine.clock().uninstall();
});
beforeEach(() => {
selectedListItem = ReactDOM.findDOMNode(historyWrapper.node)
.getElementsByClassName('list-item')[0];
describe('the history entries panel', () => {
it('has three query history entry data groups', () => {
expect(queryEntryDateGroups.length).toBe(3);
});
spyOn(selectedListItem, 'focus');
historyWrapper.node.refocus();
});
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');
});
});
});

View File

@ -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"