mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
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:
parent
33bd9d4782
commit
5141debae7
@ -72,13 +72,14 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
self.__clear_query_tool()
|
self.__clear_query_tool()
|
||||||
editor_input = self.page.find_by_id("output-panel")
|
editor_input = self.page.find_by_id("output-panel")
|
||||||
self.page.click_element(editor_input)
|
self.page.click_element(editor_input)
|
||||||
self._execute_query("SELECT * FROM shoes")
|
self._execute_query("SELECT * FROM table_that_doesnt_exist")
|
||||||
|
|
||||||
self.page.click_tab("History")
|
self.page.click_tab("Query History")
|
||||||
selected_history_entry = self.page.find_by_css_selector("#query_list .selected")
|
selected_history_entry = self.page.find_by_css_selector("#query_list .selected")
|
||||||
self.assertIn("SELECT * FROM shoes", selected_history_entry.text)
|
self.assertIn("SELECT * FROM table_that_doesnt_exist", selected_history_entry.text)
|
||||||
failed_history_detail_pane = self.page.find_by_id("query_detail")
|
failed_history_detail_pane = self.page.find_by_id("query_detail")
|
||||||
self.assertIn("ERROR: relation \"shoes\" does not exist", failed_history_detail_pane.text)
|
|
||||||
|
self.assertIn("Error Message relation \"table_that_doesnt_exist\" does not exist", failed_history_detail_pane.text)
|
||||||
ActionChains(self.page.driver) \
|
ActionChains(self.page.driver) \
|
||||||
.send_keys(Keys.ARROW_DOWN) \
|
.send_keys(Keys.ARROW_DOWN) \
|
||||||
.perform()
|
.perform()
|
||||||
@ -86,10 +87,30 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
|||||||
self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_entry.text)
|
self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_entry.text)
|
||||||
selected_history_detail_pane = self.page.find_by_id("query_detail")
|
selected_history_detail_pane = self.page.find_by_id("query_detail")
|
||||||
self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_detail_pane.text)
|
self.assertIn("SELECT * FROM test_table ORDER BY value", selected_history_detail_pane.text)
|
||||||
newly_selected_history_entry = self.page.find_by_xpath("//*[@id='query_list']/ul/li[1]")
|
newly_selected_history_entry = self.page.find_by_xpath("//*[@id='query_list']/ul/li[2]")
|
||||||
self.page.click_element(newly_selected_history_entry)
|
self.page.click_element(newly_selected_history_entry)
|
||||||
selected_history_detail_pane = self.page.find_by_id("query_detail")
|
selected_history_detail_pane = self.page.find_by_id("query_detail")
|
||||||
self.assertIn("SELECT * FROM shoes", selected_history_detail_pane.text)
|
self.assertIn("SELECT * FROM table_that_doesnt_exist", selected_history_detail_pane.text)
|
||||||
|
|
||||||
|
self.__clear_query_tool()
|
||||||
|
|
||||||
|
self.page.click_element(editor_input)
|
||||||
|
for _ in range(15):
|
||||||
|
self._execute_query("SELECT * FROM hats")
|
||||||
|
|
||||||
|
self.page.click_tab("Query History")
|
||||||
|
|
||||||
|
query_we_need_to_scroll_to = self.page.find_by_xpath("//*[@id='query_list']/ul/li[17]")
|
||||||
|
|
||||||
|
self.page.click_element(query_we_need_to_scroll_to)
|
||||||
|
self._assert_not_clickable_because_out_of_view(query_we_need_to_scroll_to)
|
||||||
|
|
||||||
|
for _ in range(17):
|
||||||
|
ActionChains(self.page.driver) \
|
||||||
|
.send_keys(Keys.ARROW_DOWN) \
|
||||||
|
.perform()
|
||||||
|
|
||||||
|
self._assert_clickable(query_we_need_to_scroll_to)
|
||||||
|
|
||||||
self.__clear_query_tool()
|
self.__clear_query_tool()
|
||||||
self.page.click_element(editor_input)
|
self.page.click_element(editor_input)
|
||||||
|
@ -268,6 +268,7 @@
|
|||||||
.wcFrameTitleBar {
|
.wcFrameTitleBar {
|
||||||
background-color: #e8e8e8;
|
background-color: #e8e8e8;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
|
border-bottom: #cccccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wcFloating .wcFrameTitleBar {
|
.wcFloating .wcFrameTitleBar {
|
||||||
|
@ -12,11 +12,49 @@ import 'codemirror/mode/sql/sql';
|
|||||||
|
|
||||||
import CodeMirror from './code_mirror';
|
import CodeMirror from './code_mirror';
|
||||||
import Shapes from '../../react_shapes';
|
import Shapes from '../../react_shapes';
|
||||||
|
import clipboard from '../../../js/selection/clipboard';
|
||||||
|
|
||||||
export default class HistoryDetailQuery extends React.Component {
|
export default class HistoryDetailQuery extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.copyAllHandler = this.copyAllHandler.bind(this);
|
||||||
|
this.state = {isCopied: false};
|
||||||
|
this.timeout = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyAllHandler() {
|
||||||
|
clipboard.copyTextToClipboard(this.props.historyEntry.query);
|
||||||
|
|
||||||
|
this.clearPreviousTimeout();
|
||||||
|
|
||||||
|
this.setState({isCopied: true});
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.setState({isCopied: false});
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPreviousTimeout() {
|
||||||
|
if (this.timeout !== undefined) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyButtonText() {
|
||||||
|
return this.state.isCopied ? 'Copied!' : 'Copy All';
|
||||||
|
}
|
||||||
|
|
||||||
|
copyButtonClass() {
|
||||||
|
return this.state.isCopied ? 'was-copied' : 'copy-all';
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div id="history-detail-query">
|
<div id="history-detail-query">
|
||||||
|
<button className={this.copyButtonClass()}
|
||||||
|
onClick={this.copyAllHandler}>{this.copyButtonText()}</button>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={this.props.historyEntry.query}
|
value={this.props.historyEntry.query}
|
||||||
options={{
|
options={{
|
||||||
|
@ -12,8 +12,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
import QueryHistoryEntry from './query_history_entry';
|
import _ from 'underscore';
|
||||||
|
|
||||||
import QueryHistoryDetail from './query_history_detail';
|
import QueryHistoryDetail from './query_history_detail';
|
||||||
|
import QueryHistoryEntries from './query_history_entries';
|
||||||
import Shapes from '../react_shapes';
|
import Shapes from '../react_shapes';
|
||||||
|
|
||||||
const queryEntryListDivStyle = {
|
const queryEntryListDivStyle = {
|
||||||
@ -23,9 +25,6 @@ const queryDetailDivStyle = {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ARROWUP = 38;
|
|
||||||
const ARROWDOWN = 40;
|
|
||||||
|
|
||||||
export default class QueryHistory extends React.Component {
|
export default class QueryHistory extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -36,28 +35,34 @@ export default class QueryHistory extends React.Component {
|
|||||||
selectedEntry: 0,
|
selectedEntry: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onKeyDownHandler = this.onKeyDownHandler.bind(this);
|
this.selectHistoryEntry = this.selectHistoryEntry.bind(this);
|
||||||
this.navigateUpAndDown = this.navigateUpAndDown.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.resetCurrentHistoryDetail(this.props.historyCollection.historyList);
|
this.setHistory(this.props.historyCollection.historyList);
|
||||||
|
this.selectHistoryEntry(0);
|
||||||
|
|
||||||
this.props.historyCollection.onChange((historyList) => {
|
this.props.historyCollection.onChange((historyList) => {
|
||||||
this.resetCurrentHistoryDetail(historyList);
|
this.setHistory(historyList);
|
||||||
|
this.selectHistoryEntry(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.historyCollection.onReset((historyList) => {
|
this.props.historyCollection.onReset(() => {
|
||||||
this.clearCurrentHistoryDetail(historyList);
|
this.setState({
|
||||||
|
history: [],
|
||||||
|
currentHistoryDetail: undefined,
|
||||||
|
selectedEntry: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.resetCurrentHistoryDetail(this.state.history);
|
this.selectHistoryEntry(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
refocus() {
|
refocus() {
|
||||||
if (this.state.history.length > 0) {
|
if (this.state.history.length > 0) {
|
||||||
this.retrieveSelectedQuery().parentElement.focus();
|
setTimeout(() => this.retrieveSelectedQuery().parentElement.focus(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,130 +71,33 @@ export default class QueryHistory extends React.Component {
|
|||||||
.getElementsByClassName('selected')[0];
|
.getElementsByClassName('selected')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentHistoryDetail() {
|
setHistory(historyList) {
|
||||||
return this.state.currentHistoryDetail;
|
this.setState({history: this.orderedHistory(historyList)});
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentHistoryDetail(index, historyList) {
|
selectHistoryEntry(index) {
|
||||||
this.setState({
|
this.setState({
|
||||||
history: historyList,
|
currentHistoryDetail: this.state.history[index],
|
||||||
currentHistoryDetail: this.retrieveOrderedHistory().value()[index],
|
|
||||||
selectedEntry: index,
|
selectedEntry: index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resetCurrentHistoryDetail(historyList) {
|
orderedHistory(historyList) {
|
||||||
this.setCurrentHistoryDetail(0, historyList);
|
return _.chain(historyList)
|
||||||
}
|
|
||||||
|
|
||||||
clearCurrentHistoryDetail(historyList) {
|
|
||||||
this.setState({
|
|
||||||
history: historyList,
|
|
||||||
currentHistoryDetail: undefined,
|
|
||||||
selectedEntry: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
retrieveOrderedHistory() {
|
|
||||||
return _.chain(this.state.history)
|
|
||||||
.sortBy(historyEntry => historyEntry.start_time)
|
.sortBy(historyEntry => historyEntry.start_time)
|
||||||
.reverse();
|
.reverse()
|
||||||
}
|
.value();
|
||||||
|
|
||||||
onClickHandler(index) {
|
|
||||||
this.setCurrentHistoryDetail(index, this.state.history);
|
|
||||||
}
|
|
||||||
|
|
||||||
isInvisible(element) {
|
|
||||||
return this.isAbovePaneTop(element) || this.isBelowPaneBottom(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
isBelowPaneBottom(element) {
|
|
||||||
const paneElement = ReactDOM.findDOMNode(this).getElementsByClassName('Pane1')[0];
|
|
||||||
return element.getBoundingClientRect().bottom > paneElement.getBoundingClientRect().bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
isAbovePaneTop(element) {
|
|
||||||
const paneElement = ReactDOM.findDOMNode(this).getElementsByClassName('Pane1')[0];
|
|
||||||
return element.getBoundingClientRect().top < paneElement.getBoundingClientRect().top;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateUpAndDown(event) {
|
|
||||||
const arrowKeys = [ARROWUP, ARROWDOWN];
|
|
||||||
const key = event.keyCode || event.which;
|
|
||||||
if (arrowKeys.indexOf(key) > -1) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.onKeyDownHandler(event);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDownHandler(event) {
|
|
||||||
if (this.isArrowDown(event)) {
|
|
||||||
if (!this.isLastEntry()) {
|
|
||||||
let nextEntry = this.state.selectedEntry + 1;
|
|
||||||
this.setCurrentHistoryDetail(nextEntry, this.state.history);
|
|
||||||
|
|
||||||
if (this.isInvisible(this.getEntryFromList(nextEntry))) {
|
|
||||||
this.getEntryFromList(nextEntry).scrollIntoView(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.isArrowUp(event)) {
|
|
||||||
if (!this.isFirstEntry()) {
|
|
||||||
let previousEntry = this.state.selectedEntry - 1;
|
|
||||||
this.setCurrentHistoryDetail(previousEntry, this.state.history);
|
|
||||||
|
|
||||||
if (this.isInvisible(this.getEntryFromList(previousEntry))) {
|
|
||||||
this.getEntryFromList(previousEntry).scrollIntoView(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getEntryFromList(entryIndex) {
|
|
||||||
return ReactDOM.findDOMNode(this).getElementsByClassName('entry')[entryIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
isFirstEntry() {
|
|
||||||
return this.state.selectedEntry === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLastEntry() {
|
|
||||||
return this.state.selectedEntry === this.state.history.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
isArrowUp(event) {
|
|
||||||
return (event.keyCode || event.which) === ARROWUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
isArrowDown(event) {
|
|
||||||
return (event.keyCode || event.which) === ARROWDOWN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SplitPane defaultSize='50%' split='vertical' pane1Style={queryEntryListDivStyle}
|
<SplitPane defaultSize='50%' split='vertical' pane1Style={queryEntryListDivStyle}
|
||||||
pane2Style={queryDetailDivStyle}>
|
pane2Style={queryDetailDivStyle}>
|
||||||
<div id='query_list'
|
<QueryHistoryEntries historyEntries={this.state.history}
|
||||||
className='query-history'
|
selectedEntry={this.state.selectedEntry}
|
||||||
onKeyDown={this.navigateUpAndDown}
|
onSelectEntry={this.selectHistoryEntry}
|
||||||
tabIndex={-1}>
|
/>
|
||||||
<ul>
|
<QueryHistoryDetail historyEntry={this.state.currentHistoryDetail}/>
|
||||||
{this.retrieveOrderedHistory()
|
|
||||||
.map((entry, index) =>
|
|
||||||
<li key={index} className='list-item'
|
|
||||||
onClick={this.onClickHandler.bind(this, index)}
|
|
||||||
tabIndex={-1}>
|
|
||||||
<QueryHistoryEntry
|
|
||||||
historyEntry={entry}
|
|
||||||
isSelected={index == this.state.selectedEntry}/>
|
|
||||||
</li>)
|
|
||||||
.value()
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<QueryHistoryDetail historyEntry={this.getCurrentHistoryDetail()}/>
|
|
||||||
</SplitPane>);
|
</SplitPane>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
156
web/pgadmin/static/jsx/history/query_history_entries.jsx
Normal file
156
web/pgadmin/static/jsx/history/query_history_entries.jsx
Normal 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,
|
||||||
|
};
|
@ -13,7 +13,7 @@ import moment from 'moment';
|
|||||||
|
|
||||||
export default class QueryHistoryEntry extends React.Component {
|
export default class QueryHistoryEntry extends React.Component {
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return (moment(date).format('MMM D YYYY [–] HH:mm:ss'));
|
return (moment(date).format('HH:mm:ss'));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWithClasses(outerDivStyle) {
|
renderWithClasses(outerDivStyle) {
|
||||||
|
@ -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';
|
@ -1,92 +1,14 @@
|
|||||||
/*doc
|
.alert-icon {
|
||||||
---
|
display: flex;
|
||||||
title: Alerts
|
align-items: center;
|
||||||
name: alerts
|
color: white;
|
||||||
category: alerts
|
padding: 15px 15px 15px 17px;
|
||||||
---
|
width: 50px;
|
||||||
|
min-height: 50px;
|
||||||
```html_example
|
font-size: 14px;
|
||||||
<div class="alert-row">
|
text-align: center;
|
||||||
<div class="alert alert-success text-14 alert-box">
|
align-self: stretch;
|
||||||
<div class="media">
|
flex-shrink: 0;
|
||||||
<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-row {
|
.alert-row {
|
||||||
@ -103,22 +25,12 @@ category: alerts
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-icon {
|
|
||||||
display: inline-block;
|
|
||||||
color: white;
|
|
||||||
padding: 15px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-icon {
|
.success-icon {
|
||||||
background: #3a773a;
|
background: $color-green-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-icon {
|
.error-icon {
|
||||||
background: #d0021b;
|
background: $color-red-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
@ -128,17 +40,27 @@ category: alerts
|
|||||||
.alert-text {
|
.alert-text {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 12px 0 10px;
|
padding: 0 12px 0 10px;
|
||||||
|
align-self: center;
|
||||||
// To make sure IE picks up the correct font
|
// To make sure IE picks up the correct font
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info {
|
.alert-info {
|
||||||
border-color: #84acdd
|
border-color: $color-blue-2;
|
||||||
|
background-image: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-body {
|
.alert-danger {
|
||||||
vertical-align: top;
|
background-image: none;
|
||||||
width: initial;
|
}
|
||||||
|
|
||||||
|
.grid-error, .graph-error {
|
||||||
|
.alert-row {
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ajs-message {
|
.ajs-message {
|
||||||
@ -147,19 +69,36 @@ category: alerts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert, .ajs-message {
|
||||||
|
.media {
|
||||||
|
.media-body {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
.alert-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.alert-text {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pg-prop-status-bar {
|
.pg-prop-status-bar {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
.media-body {
|
.media-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-icon {
|
.alert-icon {
|
||||||
padding: 8px;
|
padding: 8px 8px 8px 10.5px;
|
||||||
width: 35px;
|
width: 35px;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-text {
|
.alert-text {
|
||||||
@ -167,8 +106,8 @@ category: alerts
|
|||||||
border: 1px solid $color-red-2;
|
border: 1px solid $color-red-2;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
padding: 7px 12px 5px 10px;
|
padding: 7px 12px 6px 10px;
|
||||||
border-left: 0px;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-in-footer {
|
.error-in-footer {
|
||||||
@ -193,7 +132,28 @@ category: alerts
|
|||||||
height: 35px;
|
height: 35px;
|
||||||
|
|
||||||
.alert-text {
|
.alert-text {
|
||||||
border: 0px;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Internet Explorer specific CSS
|
||||||
|
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
|
||||||
|
.styleguide {
|
||||||
|
.alert-danger {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,63 +1,3 @@
|
|||||||
/*doc
|
|
||||||
---
|
|
||||||
title: Grays
|
|
||||||
name: Grays
|
|
||||||
category: colors
|
|
||||||
---
|
|
||||||
For text, avoid using black or #000 to lower the contrast between the background and text.
|
|
||||||
|
|
||||||
```html_example
|
|
||||||
<div class="row">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-gray-1">
|
|
||||||
#f9f9f9
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-gray-2">
|
|
||||||
#e8e8e8
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-gray-3">
|
|
||||||
#cccccc
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-gray-4">
|
|
||||||
#888888
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-gray-5 font-white">
|
|
||||||
#555555
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-gray-6 font-white">
|
|
||||||
#333333
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.color-chip {
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, .15);
|
|
||||||
color: rgba(0, 0, 0, .65);
|
|
||||||
display: flex;
|
|
||||||
font-size: 1.25em;
|
|
||||||
height: 100px;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 0 1em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
$color-gray-1: #f9f9f9;
|
$color-gray-1: #f9f9f9;
|
||||||
$color-gray-2: #e8e8e8;
|
$color-gray-2: #e8e8e8;
|
||||||
$color-gray-3: #cccccc;
|
$color-gray-3: #cccccc;
|
||||||
@ -120,7 +60,3 @@ $color-gray-6: #333333;
|
|||||||
.font-gray-6 {
|
.font-gray-6 {
|
||||||
color: $color-gray-6;
|
color: $color-gray-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-white {
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
|
@ -1,99 +1,22 @@
|
|||||||
/*doc
|
$color-blue-1: #e7f2ff;
|
||||||
---
|
$color-blue-2: #84acdd;
|
||||||
title: Others
|
|
||||||
name: z-othercolors
|
|
||||||
category: colors
|
|
||||||
---
|
|
||||||
These colors should be used to highlight hover options in dropdown menus and catalog browser or to tell the user when something is right or wrong.
|
|
||||||
|
|
||||||
|
|
||||||
```html_example
|
|
||||||
<div class="row">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-blue-1">
|
|
||||||
#e7f2ff
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-blue-2">
|
|
||||||
#84acdd
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="row">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-red-1">
|
|
||||||
#f2dede
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-red-2">
|
|
||||||
#de8585
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-red-3">
|
|
||||||
#d0021b
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="row">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-green-1">
|
|
||||||
#dff0d7
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-green-2">
|
|
||||||
#a2c189
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-green-3">
|
|
||||||
#3a773a
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.color-chip {
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, .15);
|
|
||||||
color: rgba(0, 0, 0, .65);
|
|
||||||
display: flex;
|
|
||||||
font-size: 1.25em;
|
|
||||||
height: 100px;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 0 1em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
$color-red-1: #f2dede;
|
$color-red-1: #f2dede;
|
||||||
$color-red-2: #de8585;
|
$color-red-2: #de8585;
|
||||||
$color-red-3: #d0021b;
|
$color-red-3: #d0021b;
|
||||||
|
$color-green-1: #dff0d7;
|
||||||
$color-green-2: #a2c189;
|
$color-green-2: #a2c189;
|
||||||
|
$color-green-3: #3a773a;
|
||||||
|
|
||||||
.bg-white-1 {
|
.bg-white-1 {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-blue-1 {
|
.bg-blue-1 {
|
||||||
background-color: #e7f2ff;
|
background-color: $color-blue-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-blue-2 {
|
.bg-blue-2 {
|
||||||
background-color: #84acdd;
|
background-color: $color-blue-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-red-1 {
|
.bg-red-1 {
|
||||||
@ -109,23 +32,23 @@ $color-green-2: #a2c189;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bg-green-1 {
|
.bg-green-1 {
|
||||||
background-color: #dff0d7;
|
background-color: $color-green-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-green-2 {
|
.bg-green-2 {
|
||||||
background-color: #a2c189;
|
background-color: $color-green-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-green-3 {
|
.bg-green-3 {
|
||||||
background-color: #3a773a;
|
background-color: $color-green-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-blue-1 {
|
.border-blue-1 {
|
||||||
border-color: #e7f2ff;
|
border-color: $color-blue-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-blue-2 {
|
.border-blue-2 {
|
||||||
border-color: #84acdd;
|
border-color: $color-blue-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-red-1 {
|
.border-red-1 {
|
||||||
@ -141,15 +64,15 @@ $color-green-2: #a2c189;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.border-green-1 {
|
.border-green-1 {
|
||||||
border-color: #dff0d7;
|
border-color: $color-green-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-green-2 {
|
.border-green-2 {
|
||||||
border-color: #a2c189;
|
border-color: $color-green-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-green-3 {
|
.border-green-3 {
|
||||||
border-color: #3a773a;
|
border-color: $color-green-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-red-3 {
|
.font-red-3 {
|
||||||
@ -157,9 +80,5 @@ $color-green-2: #a2c189;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.font-green-3 {
|
.font-green-3 {
|
||||||
color: #3a773a;
|
color: $color-green-3;
|
||||||
}
|
|
||||||
|
|
||||||
.font-white {
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,3 @@
|
|||||||
/*doc
|
|
||||||
---
|
|
||||||
title: Primary blue
|
|
||||||
name: colors-primaryblue
|
|
||||||
category: colors
|
|
||||||
---
|
|
||||||
This color should be used to call attention to the main part of the app. Use sparingly.
|
|
||||||
|
|
||||||
```html_example
|
|
||||||
<div class="row">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6 col-md-3">
|
|
||||||
<div class="color-chip bg-primary-blue font-white">
|
|
||||||
#2c76b4
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.color-chip {
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, .15);
|
|
||||||
color: rgba(0, 0, 0, .65);
|
|
||||||
display: flex;
|
|
||||||
font-size: 1.25em;
|
|
||||||
height: 100px;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 0 1em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
$primary-blue: #2c76b4;
|
$primary-blue: #2c76b4;
|
||||||
|
|
||||||
.bg-primary-blue {
|
.bg-primary-blue {
|
||||||
@ -46,4 +10,4 @@ $primary-blue: #2c76b4;
|
|||||||
|
|
||||||
.font-primary-blue {
|
.font-primary-blue {
|
||||||
color: $primary-blue;
|
color: $primary-blue;
|
||||||
}
|
}
|
@ -1,68 +1,25 @@
|
|||||||
/*doc
|
$font-family-1: "Helvetica Neue", Arial, sans-serif;
|
||||||
---
|
|
||||||
title: Typography
|
|
||||||
name: typography
|
|
||||||
category: typography
|
|
||||||
---
|
|
||||||
|
|
||||||
Font Typography
|
|
||||||
|
|
||||||
```html_example_table
|
|
||||||
<div class="text-14">
|
|
||||||
Body 14 px Helvetica Neue
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-14 text-bold">
|
|
||||||
Body 14 px Helvetica Neue bold
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-13">
|
|
||||||
Body 13 px Helvetica Neue
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-13 text-bold">
|
|
||||||
Body 13 px Helvetica Neue bold
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-12">
|
|
||||||
Body 12 px Helvetica Neue
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-12 text-bold">
|
|
||||||
Body 12 px Helvetica Neue bold
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-11">
|
|
||||||
Body 11 px Helvetica Neue
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-11 text-bold">
|
|
||||||
Body 11 px Helvetica Neue bold
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.text-bold {
|
.text-bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-14 {
|
.text-14 {
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: $font-family-1;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-13 {
|
.text-13 {
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: $font-family-1;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-12 {
|
.text-12 {
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: $font-family-1;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-11 {
|
.text-11 {
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: $font-family-1;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@
|
|||||||
.entry {
|
.entry {
|
||||||
@extend .text-14;
|
@extend .text-14;
|
||||||
@extend .bg-white-1;
|
@extend .bg-white-1;
|
||||||
padding: -2px 18px -2px 8px;
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
margin-left: 1px;
|
margin-left: 1px;
|
||||||
|
padding: 0 8px 0 5px;
|
||||||
|
|
||||||
.other-info {
|
.other-info {
|
||||||
@extend .text-13;
|
@extend .text-13;
|
||||||
@ -33,6 +33,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
font-family: monospace;
|
||||||
|
background: #e8e8e8;
|
||||||
|
padding: 2px 9px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #888888;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
.entry.error {
|
.entry.error {
|
||||||
@extend .bg-red-1;
|
@extend .bg-red-1;
|
||||||
}
|
}
|
||||||
@ -114,6 +124,32 @@
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
height: 0;
|
height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.copy-all, .was-copied {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
border: 1px solid $color-gray-3;
|
||||||
|
color: $primary-blue;
|
||||||
|
font-size: 12px;
|
||||||
|
box-shadow: 1px 2px 4px 0px $color-gray-3;
|
||||||
|
padding: 3px 12px 3px 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-all {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.was-copied {
|
||||||
|
background-color: $color-blue-1;
|
||||||
|
border-color: $color-blue-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scroll {
|
||||||
|
padding-top: 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-divider {
|
.block-divider {
|
||||||
|
@ -215,7 +215,7 @@ define('tools.querytool', [
|
|||||||
|
|
||||||
var history = new pgAdmin.Browser.Panel({
|
var history = new pgAdmin.Browser.Panel({
|
||||||
name: 'history',
|
name: 'history',
|
||||||
title: gettext("History"),
|
title: gettext("Query History"),
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height:'100%',
|
height:'100%',
|
||||||
isCloseable: false,
|
isCloseable: false,
|
||||||
|
@ -12,10 +12,15 @@
|
|||||||
import jasmineEnzyme from 'jasmine-enzyme';
|
import jasmineEnzyme from 'jasmine-enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import QueryHistory from '../../../pgadmin/static/jsx/history/query_history';
|
import QueryHistory from '../../../pgadmin/static/jsx/history/query_history';
|
||||||
import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry';
|
import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry';
|
||||||
|
import QueryHistoryEntryDateGroup from '../../../pgadmin/static/jsx/history/query_history_entry_date_group';
|
||||||
|
import QueryHistoryEntries from '../../../pgadmin/static/jsx/history/query_history_entries';
|
||||||
import QueryHistoryDetail from '../../../pgadmin/static/jsx/history/query_history_detail';
|
import QueryHistoryDetail from '../../../pgadmin/static/jsx/history/query_history_detail';
|
||||||
import HistoryCollection from '../../../pgadmin/static/js/history/history_collection';
|
import HistoryCollection from '../../../pgadmin/static/js/history/history_collection';
|
||||||
|
import clipboard from '../../../pgadmin/static/js/selection/clipboard';
|
||||||
|
|
||||||
import {mount} from 'enzyme';
|
import {mount} from 'enzyme';
|
||||||
|
|
||||||
@ -50,7 +55,7 @@ describe('QueryHistory', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('nothing is displayed on right panel', (done) => {
|
it('nothing is displayed in the history details panel', (done) => {
|
||||||
let foundChildren = historyWrapper.find(QueryHistoryDetail);
|
let foundChildren = historyWrapper.find(QueryHistoryDetail);
|
||||||
expect(foundChildren.length).toBe(1);
|
expect(foundChildren.length).toBe(1);
|
||||||
done();
|
done();
|
||||||
@ -58,254 +63,397 @@ describe('QueryHistory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when there is history', () => {
|
describe('when there is history', () => {
|
||||||
let historyObjects;
|
let queryEntries;
|
||||||
|
let queryDetail;
|
||||||
|
let isInvisibleSpy;
|
||||||
|
|
||||||
beforeEach(function () {
|
describe('when two SQL queries were executed', () => {
|
||||||
historyObjects = [{
|
|
||||||
query: 'first sql statement',
|
|
||||||
start_time: new Date(2017, 5, 3, 14, 3, 15, 150),
|
|
||||||
status: true,
|
|
||||||
row_affected: 12345,
|
|
||||||
total_time: '14 msec',
|
|
||||||
message: 'something important ERROR: message from first sql query',
|
|
||||||
}, {
|
|
||||||
query: 'second sql statement',
|
|
||||||
start_time: new Date(2016, 11, 11, 1, 33, 5, 99),
|
|
||||||
status: false,
|
|
||||||
row_affected: 1,
|
|
||||||
total_time: '234 msec',
|
|
||||||
message: 'something important ERROR: message from second sql query',
|
|
||||||
}];
|
|
||||||
historyCollection = new HistoryCollection(historyObjects);
|
|
||||||
|
|
||||||
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>);
|
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', () => {
|
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>);
|
||||||
describe('when two SQL queries were executed', () => {
|
|
||||||
let foundChildren;
|
|
||||||
let queryDetail;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
const queryHistoryEntriesComponent = historyWrapper.find(QueryHistoryEntries);
|
||||||
spyOn(historyWrapper.node, 'isInvisible').and.returnValue(false);
|
isInvisibleSpy = spyOn(queryHistoryEntriesComponent.node, 'isInvisible')
|
||||||
foundChildren = historyWrapper.find(QueryHistoryEntry);
|
.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('displays the query history entries in order', () => {
|
||||||
it('has two query history entries', () => {
|
expect(queryEntries.at(0).text()).toContain('first sql statement');
|
||||||
expect(foundChildren.length).toBe(2);
|
expect(queryEntries.at(1).text()).toContain('second sql statement');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the query history entries in order', () => {
|
it('displays the formatted timestamp of the queries in chronological order by most recent first', () => {
|
||||||
expect(foundChildren.at(0).text()).toContain('first sql statement');
|
expect(queryEntries.at(0).find('.timestamp').text()).toBe('14:03:15');
|
||||||
expect(foundChildren.at(1).text()).toContain('second sql statement');
|
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', () => {
|
it('renders the most recent query as selected', () => {
|
||||||
expect(foundChildren.at(0).text()).toContain('Jun 3 2017 – 14:03:15');
|
expect(queryEntries.at(0).nodes.length).toBe(1);
|
||||||
expect(foundChildren.at(1).text()).toContain('Dec 11 2016 – 01:33:05');
|
expect(queryEntries.at(0).hasClass('selected')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the most recent query as selected', () => {
|
it('renders the older query as not selected', () => {
|
||||||
expect(foundChildren.at(0).nodes.length).toBe(1);
|
expect(queryEntries.at(1).nodes.length).toBe(1);
|
||||||
expect(foundChildren.at(0).hasClass('selected')).toBeTruthy();
|
expect(queryEntries.at(1).hasClass('selected')).toBeFalsy();
|
||||||
});
|
expect(queryEntries.at(1).hasClass('error')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the older query as not selected', () => {
|
describe('when the selected query is the most recent', () => {
|
||||||
expect(foundChildren.at(1).nodes.length).toBe(1);
|
describe('when we press arrow down', () => {
|
||||||
expect(foundChildren.at(1).hasClass('selected')).toBeFalsy();
|
beforeEach(() => {
|
||||||
expect(foundChildren.at(1).hasClass('error')).toBeTruthy();
|
pressArrowDownKey(queryEntries.parent().at(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the selected query is the most recent', () => {
|
it('should select the next query', () => {
|
||||||
describe('when we press arrow down', () => {
|
expect(queryEntries.at(1).nodes.length).toBe(1);
|
||||||
beforeEach(() => {
|
expect(queryEntries.at(1).hasClass('selected')).toBeTruthy();
|
||||||
pressArrowDownKey(foundChildren.parent().at(0));
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should select the next query', () => {
|
it('should display the corresponding detail on the right pane', () => {
|
||||||
expect(foundChildren.at(1).nodes.length).toBe(1);
|
expect(queryDetail.at(0).text()).toContain('message from second sql query');
|
||||||
expect(foundChildren.at(1).hasClass('selected')).toBeTruthy();
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should display the corresponding detail on the right pane', () => {
|
describe('when arrow down pressed again', () => {
|
||||||
expect(queryDetail.at(0).text()).toContain('message from second sql query');
|
it('should not change the selected query', () => {
|
||||||
});
|
pressArrowDownKey(queryEntries.parent().at(0));
|
||||||
|
|
||||||
describe('when arrow down pressed again', () => {
|
expect(queryEntries.at(1).nodes.length).toBe(1);
|
||||||
it('should not change the selected query', () => {
|
expect(queryEntries.at(1).hasClass('selected')).toBeTruthy();
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when arrow up is pressed', () => {
|
describe('when arrow up is pressed', () => {
|
||||||
it('should not change the selected query', () => {
|
it('should select the most recent query', () => {
|
||||||
pressArrowUpKey(foundChildren.parent().at(0));
|
pressArrowUpKey(queryEntries.parent().at(0));
|
||||||
expect(foundChildren.at(0).nodes.length).toBe(1);
|
|
||||||
expect(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(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', () => {
|
describe('when the query failed', () => {
|
||||||
it('displays the formatted timestamp', () => {
|
let failedEntry;
|
||||||
expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date');
|
|
||||||
|
beforeEach(() => {
|
||||||
|
failedEntry = queryEntries.at(1);
|
||||||
|
failedEntry.simulate('click');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the number of rows affected', () => {
|
it('displays the error message on top of the details pane', () => {
|
||||||
if (/PhantomJS/.test(window.navigator.userAgent)) {
|
expect(queryDetail.at(0).text()).toContain('Error Message message from second sql query');
|
||||||
expect(queryDetail.at(0).text()).toContain('12345Rows Affected');
|
|
||||||
} else {
|
|
||||||
expect(queryDetail.at(0).text()).toContain('12,345Rows Affected');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('displays the total time', () => {
|
describe('when the older query is clicked on', () => {
|
||||||
expect(queryDetail.at(0).text()).toContain('14 msecDuration');
|
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', () => {
|
describe('when the first query is the selected query', () => {
|
||||||
expect(queryDetail.at(0).text()).toContain('message from first sql query');
|
describe('when refocus function is called', () => {
|
||||||
});
|
let selectedListItem;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
failedEntry = foundChildren.at(1);
|
selectedListItem = ReactDOM.findDOMNode(historyWrapper.node)
|
||||||
failedEntry.simulate('click');
|
.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(() => {
|
describe('when a third SQL query is executed', () => {
|
||||||
firstEntry = foundChildren.at(0);
|
beforeEach(() => {
|
||||||
secondEntry = foundChildren.at(1);
|
historyCollection.add({
|
||||||
secondEntry.simulate('click');
|
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', () => {
|
queryEntries = historyWrapper.find(QueryHistoryEntry);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the user clicks inside the main pane but not in any history entry', () => {
|
it('displays third query SQL in the right pane', () => {
|
||||||
let queryList;
|
expect(queryDetail.at(0).text()).toContain('third sql statement');
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the first query is outside the visible area', () => {
|
describe('when several days of queries were executed', () => {
|
||||||
|
let queryEntryDateGroups;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(historyWrapper.node, 'isInvisible').and.callFake((element) => {
|
jasmine.clock().install();
|
||||||
return element.textContent.contains('first sql statement');
|
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', () => {
|
afterEach(() => {
|
||||||
describe('when refocus function is called', () => {
|
jasmine.clock().uninstall();
|
||||||
let selectedListItem;
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
describe('the history entries panel', () => {
|
||||||
selectedListItem = ReactDOM.findDOMNode(historyWrapper.node)
|
it('has three query history entry data groups', () => {
|
||||||
.getElementsByClassName('list-item')[0];
|
expect(queryEntryDateGroups.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
spyOn(selectedListItem, 'focus');
|
it('has title above', () => {
|
||||||
historyWrapper.node.refocus();
|
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');
|
||||||
it('the first query scrolls into view', function () {
|
|
||||||
expect(selectedListItem.focus).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3438,11 +3438,11 @@ hyphenate-style-name@^1.0.2:
|
|||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
|
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
|
||||||
|
|
||||||
iconv-lite@0.4.15, iconv-lite@~0.4.13:
|
iconv-lite@0.4.15:
|
||||||
version "0.4.15"
|
version "0.4.15"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
|
||||||
|
|
||||||
iconv-lite@^0.4.15:
|
iconv-lite@^0.4.15, iconv-lite@~0.4.13:
|
||||||
version "0.4.18"
|
version "0.4.18"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
|
||||||
|
|
||||||
@ -5848,7 +5848,7 @@ read-pkg@^2.0.0:
|
|||||||
normalize-package-data "^2.3.2"
|
normalize-package-data "^2.3.2"
|
||||||
path-type "^2.0.0"
|
path-type "^2.0.0"
|
||||||
|
|
||||||
"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@^1.0.33, readable-stream@~1.0.2:
|
"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.2:
|
||||||
version "1.0.34"
|
version "1.0.34"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5857,6 +5857,15 @@ read-pkg@^2.0.0:
|
|||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
|
readable-stream@^1.0.33, readable-stream@~1.1.9:
|
||||||
|
version "1.1.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||||
|
dependencies:
|
||||||
|
core-util-is "~1.0.0"
|
||||||
|
inherits "~2.0.1"
|
||||||
|
isarray "0.0.1"
|
||||||
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.6:
|
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.6:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.1.tgz#84e26965bb9e785535ed256e8d38e92c69f09d10"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.1.tgz#84e26965bb9e785535ed256e8d38e92c69f09d10"
|
||||||
@ -5869,15 +5878,6 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable
|
|||||||
string_decoder "~1.0.0"
|
string_decoder "~1.0.0"
|
||||||
util-deprecate "~1.0.1"
|
util-deprecate "~1.0.1"
|
||||||
|
|
||||||
readable-stream@~1.1.9:
|
|
||||||
version "1.1.14"
|
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
|
||||||
dependencies:
|
|
||||||
core-util-is "~1.0.0"
|
|
||||||
inherits "~2.0.1"
|
|
||||||
isarray "0.0.1"
|
|
||||||
string_decoder "~0.10.x"
|
|
||||||
|
|
||||||
readable-stream@~2.0.0:
|
readable-stream@~2.0.0:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
|
||||||
|
Loading…
Reference in New Issue
Block a user