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