[sqllab] some frontend tests (#1400)

* [sqllab] some frontend tests

* linting

* Addressing comments

* Addressing unaddressed comments

* Touchups
This commit is contained in:
Maxime Beauchemin 2016-10-25 16:44:32 -07:00 committed by GitHub
parent 7c5933732b
commit 940659bc14
44 changed files with 875 additions and 558 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.pyc
caravel/assets/coverage/*
changelog.sh
.DS_Store
.coverage

View File

@ -5,6 +5,5 @@ dist/*
images/*
node_modules/*
node_modules*/*
spec/*
stylesheets/*
vendor/*

View File

@ -18,8 +18,6 @@ export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE';
export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN';
export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL';
export const SET_DATABASES = 'SET_DATABASES';
export const ADD_WORKSPACE_QUERY = 'ADD_WORKSPACE_QUERY';
export const REMOVE_WORKSPACE_QUERY = 'REMOVE_WORKSPACE_QUERY';
export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR';
export const ADD_ALERT = 'ADD_ALERT';
export const REMOVE_ALERT = 'REMOVE_ALERT';
@ -141,7 +139,8 @@ export function setDatabases(databases) {
}
export function addQueryEditor(queryEditor) {
return { type: ADD_QUERY_EDITOR, queryEditor };
const newQe = Object.assign({}, queryEditor, { id: shortid.generate() });
return { type: ADD_QUERY_EDITOR, queryEditor: newQe };
}
export function cloneQueryToNewTab(query) {
@ -153,7 +152,9 @@ export function setNetworkStatus(networkOn) {
}
export function addAlert(alert) {
return { type: ADD_ALERT, alert };
const o = Object.assign({}, alert);
o.id = shortid.generate();
return { type: ADD_ALERT, o };
}
export function removeAlert(alert) {
@ -208,13 +209,6 @@ export function removeTable(table) {
return { type: REMOVE_TABLE, table };
}
export function addWorkspaceQuery(query) {
return { type: ADD_WORKSPACE_QUERY, query };
}
export function removeWorkspaceQuery(query) {
return { type: REMOVE_WORKSPACE_QUERY, query };
}
export function refreshQueries(alteredQueries) {
return { type: REFRESH_QUERIES, alteredQueries };
}

View File

@ -1,8 +1,5 @@
import React from 'react';
import { Alert } from 'react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
class Alerts extends React.Component {
removeAlert(alert) {
@ -11,6 +8,7 @@ class Alerts extends React.Component {
render() {
const alerts = this.props.alerts.map((alert) =>
<Alert
key={alert.id}
bsStyle={alert.bsStyle}
style={{ width: '500px', textAlign: 'midddle', margin: '10px auto' }}
>
@ -33,9 +31,4 @@ Alerts.propTypes = {
actions: React.PropTypes.object,
};
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(null, mapDispatchToProps)(Alerts);
export default Alerts;

View File

@ -48,7 +48,7 @@ class App extends React.Component {
}
return (
<div className="App SqlLab">
<Alerts alerts={this.props.alerts} />
<Alerts alerts={this.props.alerts} actions={this.props.actions} />
<DataPreviewModal />
<div className="container-fluid">
{content}
@ -60,6 +60,7 @@ class App extends React.Component {
App.propTypes = {
alerts: React.PropTypes.array,
actions: React.PropTypes.object,
};
function mapStateToProps(state) {
@ -73,4 +74,5 @@ function mapDispatchToProps(dispatch) {
};
}
export { App };
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@ -1,48 +0,0 @@
import React from 'react';
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
const ButtonWithTooltip = (props) => {
let tooltip = (
<Tooltip id="tooltip">
{props.tooltip}
</Tooltip>
);
return (
<OverlayTrigger
overlay={tooltip}
delayShow={300}
placement={props.placement}
delayHide={150}
>
<Button
onClick={props.onClick}
bsStyle={props.bsStyle}
bsSize={props.bsSize}
disabled={props.disabled}
className={props.className}
>
{props.children}
</Button>
</OverlayTrigger>
);
};
ButtonWithTooltip.defaultProps = {
onClick: () => {},
disabled: false,
placement: 'top',
bsStyle: 'default',
};
ButtonWithTooltip.propTypes = {
bsSize: React.PropTypes.string,
bsStyle: React.PropTypes.string,
children: React.PropTypes.element,
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
onClick: React.PropTypes.func,
placement: React.PropTypes.string,
tooltip: React.PropTypes.string,
};
export default ButtonWithTooltip;

View File

@ -3,11 +3,11 @@ import CopyToClipboard from '../../components/CopyToClipboard';
import { getShortUrl } from '../../../utils/common';
const propTypes = {
qe: React.PropTypes.object,
queryEditor: React.PropTypes.object,
};
const defaultProps = {
qe: null,
queryEditor: null,
};
export default class CopyQueryTabUrl extends React.Component {
@ -20,7 +20,7 @@ export default class CopyQueryTabUrl extends React.Component {
componentWillMount() {
const params = [];
const qe = this.props.qe;
const qe = this.props.queryEditor;
if (qe.dbId) params.push('dbid=' + qe.dbId);
if (qe.title) params.push('title=' + encodeURIComponent(qe.title));
if (qe.schema) params.push('schema=' + encodeURIComponent(qe.schema));

View File

@ -32,7 +32,7 @@ class DataPreviewModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
<ResultSet query={query} visualize={false} csv={false} />
<ResultSet query={query} visualize={false} csv={false} actions={this.props.actions} />
</Modal.Body>
</Modal>
);

View File

@ -1,9 +1,6 @@
const $ = window.$ = require('jquery');
import React from 'react';
import { bindActionCreators } from 'redux';
import Select from 'react-select';
import { connect } from 'react-redux';
import * as Actions from '../actions';
class DatabaseSelect extends React.Component {
constructor(props) {
@ -53,15 +50,4 @@ DatabaseSelect.propTypes = {
valueRenderer: React.PropTypes.func,
};
DatabaseSelect.defaultProps = {
onChange: () => {},
databaseId: null,
};
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(null, mapDispatchToProps)(DatabaseSelect);
export default DatabaseSelect;

View File

@ -1,6 +1,24 @@
import React from 'react';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
const propTypes = {
children: React.PropTypes.node,
className: React.PropTypes.string,
href: React.PropTypes.string,
onClick: React.PropTypes.func,
placement: React.PropTypes.string,
style: React.PropTypes.object,
tooltip: React.PropTypes.string,
};
const defaultProps = {
className: '',
href: '#',
onClick: () => {},
placement: 'top',
style: {},
tooltip: null,
};
class Link extends React.Component {
render() {
@ -34,21 +52,7 @@ class Link extends React.Component {
return link;
}
}
Link.propTypes = {
children: React.PropTypes.object,
className: React.PropTypes.string,
href: React.PropTypes.string,
onClick: React.PropTypes.func,
placement: React.PropTypes.string,
style: React.PropTypes.object,
tooltip: React.PropTypes.string,
};
Link.defaultProps = {
disabled: false,
href: '#',
tooltip: null,
placement: 'top',
onClick: () => {},
};
Link.propTypes = propTypes;
Link.defaultProps = defaultProps;
export default Link;

View File

@ -30,14 +30,10 @@ class QueryAutoRefresh extends React.Component {
if (Object.keys(data).length > 0) {
this.props.actions.refreshQueries(data);
}
if (!this.props.networkOn) {
this.props.actions.setNetworkStatus(true);
}
this.props.actions.setNetworkStatus(true);
})
.fail(() => {
if (this.props.networkOn) {
this.props.actions.setNetworkStatus(false);
}
this.props.actions.setNetworkStatus(false);
});
}
render() {
@ -47,7 +43,6 @@ class QueryAutoRefresh extends React.Component {
QueryAutoRefresh.propTypes = {
actions: React.PropTypes.object,
queriesLastUpdate: React.PropTypes.number,
networkOn: React.PropTypes.bool,
};
QueryAutoRefresh.defaultProps = {
// queries: null,
@ -56,7 +51,6 @@ QueryAutoRefresh.defaultProps = {
function mapStateToProps(state) {
return {
queriesLastUpdate: state.queriesLastUpdate,
networkOn: state.networkOn,
};
}

View File

@ -1,28 +1,23 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import QueryTable from './QueryTable';
import { Alert } from 'react-bootstrap';
const propTypes = {
queries: React.PropTypes.array.isRequired,
actions: React.PropTypes.object.isRequired,
};
const QueryHistory = (props) => {
const activeQeId = props.tabHistory[props.tabHistory.length - 1];
const queriesArray = [];
for (const id in props.queries) {
if (props.queries[id].sqlEditorId === activeQeId) {
queriesArray.push(props.queries[id]);
}
}
if (queriesArray.length > 0) {
if (props.queries.length > 0) {
return (
<QueryTable
columns={[
'state', 'started', 'duration', 'progress',
'rows', 'sql', 'output', 'actions',
]}
queries={queriesArray}
queries={props.queries}
actions={props.actions}
/>
);
}
@ -32,27 +27,6 @@ const QueryHistory = (props) => {
</Alert>
);
};
QueryHistory.propTypes = propTypes;
QueryHistory.defaultProps = {
queries: {},
};
QueryHistory.propTypes = {
queries: React.PropTypes.object,
tabHistory: React.PropTypes.array,
actions: React.PropTypes.object,
};
function mapStateToProps(state) {
return {
queries: state.queries,
tabHistory: state.tabHistory,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(QueryHistory);
export default QueryHistory;

View File

@ -1,62 +0,0 @@
import React from 'react';
import Link from './Link';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import shortid from 'shortid';
class QueryLink extends React.Component {
popTab() {
const qe = {
id: shortid.generate(),
title: this.props.query.title,
dbId: this.props.query.dbId,
autorun: false,
sql: this.props.query.sql,
};
this.props.actions.addQueryEditor(qe);
}
render() {
return (
<div>
<div className="clearfix">
<div className="pull-left">
<a
href="#"
tooltip="Pop this query in a new tab"
onClick={this.popTab.bind(this)}
>
{this.props.query.title}
</a>
</div>
<div className="pull-right">
<Link
onClick={this.props.actions.removeWorkspaceQuery.bind(this, this.props.query)}
tooltip="Remove query from workspace"
href="#"
>
&times;
</Link>
</div>
</div>
<hr />
</div>
);
}
}
QueryLink.propTypes = {
query: React.PropTypes.object,
actions: React.PropTypes.object,
};
QueryLink.defaultProps = {
};
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(null, mapDispatchToProps)(QueryLink);

View File

@ -7,6 +7,10 @@ import QueryTable from './QueryTable';
import DatabaseSelect from './DatabaseSelect';
import { STATUS_OPTIONS } from '../common';
const propTypes = {
actions: React.PropTypes.object.isRequired,
};
class QuerySearch extends React.Component {
constructor(props) {
super(props);
@ -132,11 +136,11 @@ class QuerySearch extends React.Component {
onUserClicked={this.onUserClicked.bind(this)}
onDbClicked={this.onDbClicked.bind(this)}
queries={this.state.queriesArray}
actions={this.props.actions}
/>
</div>
);
}
}
QuerySearch.propTypes = propTypes;
export default QuerySearch;

View File

@ -1,9 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import moment from 'moment';
import { Table } from 'reactable';
import { Label, ProgressBar } from 'react-bootstrap';
@ -67,6 +63,9 @@ class QueryTable extends React.Component {
clearQueryResults(query) {
this.props.actions.clearQueryResults(query);
}
removeQuery(query) {
this.props.actions.removeQuery(query);
}
render() {
const data = this.props.queries.map((query) => {
@ -111,7 +110,7 @@ class QueryTable extends React.Component {
modalTitle={'Data preview'}
beforeOpen={this.openAsyncResults.bind(this, query)}
onExit={this.clearQueryResults.bind(this, query)}
modalBody={<ResultSet showSql query={query} />}
modalBody={<ResultSet showSql query={query} actions={this.props.actions} />}
/>
);
} else {
@ -163,7 +162,7 @@ class QueryTable extends React.Component {
<Link
className="fa fa-trash m-r-3"
tooltip="Remove query from log"
onClick={this.props.actions.removeQuery.bind(this, query)}
onClick={this.removeQuery.bind(this, query)}
/>
</div>
);
@ -198,13 +197,4 @@ class QueryTable extends React.Component {
QueryTable.propTypes = propTypes;
QueryTable.defaultProps = defaultProps;
function mapStateToProps() {
return {};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export { QueryTable };
export default connect(mapStateToProps, mapDispatchToProps)(QueryTable);
export default QueryTable;

View File

@ -3,10 +3,6 @@ import { Alert, Button, ButtonGroup, ProgressBar } from 'react-bootstrap';
import { Table } from 'reactable';
import shortid from 'shortid';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import VisualizeModal from './VisualizeModal';
import HighlightedSql from './HighlightedSql';
@ -116,7 +112,7 @@ class ResultSet extends React.Component {
if (this.props.showSql) {
sql = <HighlightedSql sql={query.sql} />;
}
if (['running', 'pending', 'fetching'].includes(query.state)) {
if (['running', 'pending', 'fetching'].indexOf(query.state) > -1) {
let progressBar;
if (query.progress > 0 && query.state === 'running') {
progressBar = (
@ -191,12 +187,4 @@ class ResultSet extends React.Component {
ResultSet.propTypes = propTypes;
ResultSet.defaultProps = defaultProps;
function mapStateToProps() {
return {};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ResultSet);
export default ResultSet;

View File

@ -3,16 +3,21 @@ import QueryHistory from './QueryHistory';
import ResultSet from './ResultSet';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import shortid from 'shortid';
const propTypes = {
queries: React.PropTypes.array.isRequired,
actions: React.PropTypes.object.isRequired,
};
const SouthPane = function (props) {
let results = <div />;
const latestQuery = props.latestQuery;
let latestQuery;
if (props.queries.length > 0) {
latestQuery = props.queries[props.queries.length - 1];
}
let results;
if (latestQuery) {
results = <ResultSet showControls search query={latestQuery} />;
results = <ResultSet showControls search query={latestQuery} actions={props.actions} />;
} else {
results = <Alert bsStyle="info">Run a query to display results here</Alert>;
}
@ -25,24 +30,12 @@ const SouthPane = function (props) {
</div>
</Tab>
<Tab title="Query History" eventKey={2}>
<QueryHistory />
<QueryHistory queries={props.queries} actions={props.actions} />
</Tab>
</Tabs>
</div>
);
};
SouthPane.propTypes = propTypes;
SouthPane.propTypes = {
latestQuery: React.PropTypes.object,
actions: React.PropTypes.object,
};
SouthPane.defaultProps = {
};
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(null, mapDispatchToProps)(SouthPane);
export default SouthPane;

View File

@ -18,16 +18,29 @@ import 'brace/mode/sql';
import 'brace/theme/github';
import 'brace/ext/language_tools';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../actions';
import shortid from 'shortid';
import SouthPane from './SouthPane';
import Timer from './Timer';
import SqlEditorLeftBar from './SqlEditorLeftBar';
const propTypes = {
actions: React.PropTypes.object.isRequired,
database: React.PropTypes.object,
latestQuery: React.PropTypes.object,
networkOn: React.PropTypes.bool,
tables: React.PropTypes.array.isRequired,
queries: React.PropTypes.array.isRequired,
queryEditor: React.PropTypes.object.isRequired,
};
const defaultProps = {
networkOn: true,
database: null,
latestQuery: null,
};
class SqlEditor extends React.Component {
constructor(props) {
super(props);
@ -72,15 +85,6 @@ class SqlEditor extends React.Component {
this.setState({ sql: text });
this.props.actions.queryEditorSetSql(this.props.queryEditor, text);
}
addWorkspaceQuery() {
this.props.actions.addWorkspaceQuery({
id: shortid.generate(),
sql: this.state.sql,
dbId: this.props.queryEditor.dbId,
schema: this.props.queryEditor.schema,
title: this.props.queryEditor.title,
});
}
ctasChange() {}
visualize() {}
ctasChanged(event) {
@ -130,7 +134,9 @@ class SqlEditor extends React.Component {
{runButtons}
</ButtonGroup>
);
if (this.props.latestQuery && ['running', 'pending'].includes(this.props.latestQuery.state)) {
if (
this.props.latestQuery &&
['running', 'pending'].indexOf(this.props.latestQuery.state) > -1) {
runButtons = (
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
<Button
@ -202,7 +208,12 @@ class SqlEditor extends React.Component {
<div className="SqlEditor" style={{ minHeight: this.sqlEditorHeight() }}>
<Row>
<Col md={3}>
<SqlEditorLeftBar queryEditor={this.props.queryEditor} />
<SqlEditorLeftBar
queryEditor={this.props.queryEditor}
tables={this.props.tables}
networkOn={this.props.networkOn}
actions={this.props.actions}
/>
</Col>
<Col md={9}>
<AceEditor
@ -220,32 +231,17 @@ class SqlEditor extends React.Component {
/>
{editorBottomBar}
<br />
<SouthPane latestQuery={this.props.latestQuery} sqlEditor={this} />
<SouthPane
queries={this.props.queries}
actions={this.props.actions}
/>
</Col>
</Row>
</div>
);
}
}
SqlEditor.defaultProps = defaultProps;
SqlEditor.propTypes = propTypes;
SqlEditor.propTypes = {
actions: React.PropTypes.object,
database: React.PropTypes.object,
latestQuery: React.PropTypes.object,
queryEditor: React.PropTypes.object,
};
SqlEditor.defaultProps = {
};
function mapStateToProps() {
return {};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditor);
export default SqlEditor;

View File

@ -1,14 +1,23 @@
const $ = window.$ = require('jquery');
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../actions';
import Select from 'react-select';
import { Label, Button } from 'react-bootstrap';
import TableElement from './TableElement';
import DatabaseSelect from './DatabaseSelect';
const propTypes = {
queryEditor: React.PropTypes.object.isRequired,
tables: React.PropTypes.array,
actions: React.PropTypes.object,
networkOn: React.PropTypes.bool,
};
const defaultProps = {
tables: [],
networkOn: true,
actions: {},
};
class SqlEditorLeftBar extends React.Component {
constructor(props) {
@ -115,7 +124,6 @@ class SqlEditorLeftBar extends React.Component {
if (!this.props.networkOn) {
networkAlert = <p><Label bsStyle="danger">OFFLINE</Label></p>;
}
const tables = this.props.tables.filter((t) => (t.queryEditorId === this.props.queryEditor.id));
const shouldShowReset = window.location.search === '?reset=1';
return (
<div className="clearfix sql-toolbar">
@ -124,6 +132,7 @@ class SqlEditorLeftBar extends React.Component {
<DatabaseSelect
onChange={this.onChange.bind(this)}
databaseId={this.props.queryEditor.dbId}
actions={this.props.actions}
valueRenderer={(o) => (
<div>
<span className="text-muted">Database:</span> {o.label}
@ -161,8 +170,13 @@ class SqlEditorLeftBar extends React.Component {
</div>
<hr />
<div className="m-t-5">
{tables.map((table) => (
<TableElement table={table} queryEditor={this.props.queryEditor} key={table.id} />
{this.props.tables.map((table) => (
<TableElement
table={table}
queryEditor={this.props.queryEditor}
key={table.id}
actions={this.props.actions}
/>
))}
</div>
{shouldShowReset &&
@ -174,29 +188,7 @@ class SqlEditorLeftBar extends React.Component {
);
}
}
SqlEditorLeftBar.propTypes = propTypes;
SqlEditorLeftBar.defaultProps = defaultProps;
SqlEditorLeftBar.propTypes = {
queryEditor: React.PropTypes.object,
tables: React.PropTypes.array,
actions: React.PropTypes.object,
networkOn: React.PropTypes.bool,
};
SqlEditorLeftBar.defaultProps = {
tables: [],
};
function mapStateToProps(state) {
return {
tables: state.tables,
networkOn: state.networkOn,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditorLeftBar);
export default SqlEditorLeftBar;

View File

@ -4,20 +4,21 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import SqlEditor from './SqlEditor';
import shortid from 'shortid';
import { getParamFromQuery } from '../../../utils/common';
import CopyQueryTabUrl from './CopyQueryTabUrl';
const propTypes = {
actions: React.PropTypes.object,
databases: React.PropTypes.object,
queries: React.PropTypes.object,
actions: React.PropTypes.object.isRequired,
databases: React.PropTypes.object.isRequired,
queries: React.PropTypes.object.isRequired,
queryEditors: React.PropTypes.array,
tabHistory: React.PropTypes.array,
tabHistory: React.PropTypes.array.isRequired,
tables: React.PropTypes.array.isRequired,
networkOn: React.PropTypes.bool,
};
const defaultProps = {
tabHistory: [],
queryEditors: [],
networkOn: true,
};
let queryCount = 1;
@ -39,7 +40,6 @@ class TabbedSqlEditors extends React.Component {
if (this.state.query) {
queryCount++;
const queryEditorProps = {
id: shortid.generate(),
title: getParamFromQuery(this.state.query, 'title'),
dbId: parseInt(getParamFromQuery(this.state.query, 'dbid'), 10),
schema: getParamFromQuery(this.state.query, 'schema'),
@ -72,7 +72,6 @@ class TabbedSqlEditors extends React.Component {
queryCount++;
const activeQueryEditor = this.activeQueryEditor();
const qe = {
id: shortid.generate(),
title: `Untitled Query ${queryCount}`,
dbId: (activeQueryEditor) ? activeQueryEditor.dbId : null,
schema: (activeQueryEditor) ? activeQueryEditor.schema : null,
@ -88,10 +87,26 @@ class TabbedSqlEditors extends React.Component {
this.props.actions.setActiveQueryEditor({ id: key });
}
}
removeQueryEditor(qe) {
this.props.actions.removeQueryEditor(qe);
}
render() {
const editors = this.props.queryEditors.map((qe, i) => {
let latestQuery = this.props.queries[qe.latestQueryId];
const database = this.props.databases[qe.dbId];
const isSelected = (qe.id === this.activeQueryEditor().id);
const queriesArray = [];
for (const id in this.props.queries) {
if (this.props.queries[id].sqlEditorId === qe.id) {
queriesArray.push(this.props.queries[id]);
}
}
let latestQuery;
if (qe.latestQueryId) {
latestQuery = this.props.queries[qe.latestQueryId];
}
let database;
if (qe.dbId) {
database = this.props.databases[qe.dbId];
}
const state = (latestQuery) ? latestQuery.state : '';
const tabTitle = (
<div>
@ -101,14 +116,14 @@ class TabbedSqlEditors extends React.Component {
id={'ddbtn-tab-' + i}
title=""
>
<MenuItem eventKey="1" onClick={this.props.actions.removeQueryEditor.bind(this, qe)}>
<MenuItem eventKey="1" onClick={this.removeQueryEditor.bind(this, qe)}>
<i className="fa fa-close" /> close tab
</MenuItem>
<MenuItem eventKey="2" onClick={this.renameTab.bind(this, qe)}>
<i className="fa fa-i-cursor" /> rename tab
</MenuItem>
<MenuItem eventKey="3">
<i className="fa fa-clipboard" /> <CopyQueryTabUrl qe={qe} />
<i className="fa fa-clipboard" /> <CopyQueryTabUrl queryEditor={qe} />
</MenuItem>
</DropdownButton>
</div>
@ -121,11 +136,17 @@ class TabbedSqlEditors extends React.Component {
>
<div className="panel panel-default">
<div className="panel-body">
<SqlEditor
queryEditor={qe}
latestQuery={latestQuery}
database={database}
/>
{isSelected &&
<SqlEditor
tables={this.props.tables.filter((t) => (t.queryEditorId === qe.id))}
queryEditor={qe}
queries={queriesArray}
latestQuery={latestQuery}
database={database}
actions={this.props.actions}
networkOn={this.props.networkOn}
/>
}
</div>
</div>
</Tab>);
@ -138,7 +159,13 @@ class TabbedSqlEditors extends React.Component {
id="a11y-query-editor-tabs"
>
{editors}
<Tab title={<div><i className="fa fa-plus-circle" />&nbsp;</div>} eventKey="add_tab" />
<Tab
title={
<div>
<i className="fa fa-plus-circle" />&nbsp;
</div>}
eventKey="add_tab"
/>
</Tabs>
);
}
@ -152,6 +179,8 @@ function mapStateToProps(state) {
queryEditors: state.queryEditors,
queries: state.queries,
tabHistory: state.tabHistory,
networkOn: state.networkOn,
tables: state.tables,
};
}
function mapDispatchToProps(dispatch) {
@ -160,4 +189,5 @@ function mapDispatchToProps(dispatch) {
};
}
export { TabbedSqlEditors };
export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors);

View File

@ -1,7 +1,4 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import { ButtonGroup, Well } from 'react-bootstrap';
import shortid from 'shortid';
@ -187,7 +184,7 @@ class TableElement extends React.Component {
<Link
className="fa fa-trash pull-left m-l-2"
onClick={this.removeTable.bind(this)}
tooltip="Remove from workspace"
tooltip="Remove from panel"
href="#"
/>
</ButtonGroup>
@ -203,10 +200,4 @@ class TableElement extends React.Component {
TableElement.propTypes = propTypes;
TableElement.defaultProps = defaultProps;
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(null, mapDispatchToProps)(TableElement);
export { TableElement };
export default TableElement;

View File

@ -1,23 +0,0 @@
import React from 'react';
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
const TableMetadata = function (props) {
return (
<BootstrapTable
condensed
data={props.table.columns}
>
<TableHeaderColumn dataField="id" isKey hidden>
id
</TableHeaderColumn>
<TableHeaderColumn dataField="name">Name</TableHeaderColumn>
<TableHeaderColumn dataField="type">Type</TableHeaderColumn>
</BootstrapTable>
);
};
TableMetadata.propTypes = {
table: React.PropTypes.object,
};
export default TableMetadata;

View File

@ -1,10 +1,6 @@
import React from 'react';
import { Alert, Button, Col, Modal } from 'react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import Select from 'react-select';
import { Table } from 'reactable';
import shortid from 'shortid';
@ -16,6 +12,17 @@ const CHART_TYPES = [
{ value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true },
];
const propTypes = {
onHide: React.PropTypes.func,
query: React.PropTypes.object,
show: React.PropTypes.bool,
};
const defaultProps = {
show: false,
query: {},
onHide: () => {},
};
class VisualizeModal extends React.Component {
constructor(props) {
super(props);
@ -34,7 +41,10 @@ class VisualizeModal extends React.Component {
this.validate();
}
setStateFromProps() {
if (!this.props.query || !this.props.query.results.columns) {
if (
!this.props.query ||
!this.props.query.results ||
!this.props.query.results.columns) {
return;
}
const columns = {};
@ -204,22 +214,7 @@ class VisualizeModal extends React.Component {
return modal;
}
}
VisualizeModal.propTypes = {
query: React.PropTypes.object,
show: React.PropTypes.bool,
onHide: React.PropTypes.func,
};
VisualizeModal.defaultProps = {
show: false,
onHide: () => {},
};
VisualizeModal.propTypes = propTypes;
VisualizeModal.defaultProps = defaultProps;
function mapStateToProps() {
return {};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(VisualizeModal);
export default VisualizeModal;

View File

@ -4,7 +4,7 @@ import { now } from '../modules/dates';
import { addToObject, alterInObject, alterInArr, removeFromArr, getFromArr, addToArr }
from '../reduxUtils.js';
const defaultQueryEditor = {
export const defaultQueryEditor = {
id: shortid.generate(),
title: 'Untitled Query',
sql: 'SELECT *\nFROM\nWHERE',
@ -23,7 +23,6 @@ export const initialState = {
queryEditors: [defaultQueryEditor],
tabHistory: [defaultQueryEditor.id],
tables: [],
workspaceQueries: [],
queriesLastUpdate: 0,
};
@ -56,12 +55,12 @@ export const sqlLabReducer = function (state, action) {
const queries = {};
Object.keys(state.queries).forEach((k) => {
const query = state.queries[k];
if (qeIds.includes(query.sqlEditorId)) {
if (qeIds.indexOf(query.sqlEditorId) > -1) {
queries[k] = query;
}
});
let tabHistory = state.tabHistory.slice();
tabHistory = tabHistory.filter((id) => qeIds.includes(id));
tabHistory = tabHistory.filter((id) => qeIds.indexOf(id) > -1);
newState = Object.assign({}, newState, { tabHistory, queries });
return newState;
},
@ -153,7 +152,7 @@ export const sqlLabReducer = function (state, action) {
},
[actions.SET_ACTIVE_QUERY_EDITOR]() {
const qeIds = state.queryEditors.map((qe) => qe.id);
if (qeIds.includes(action.queryEditor.id)) {
if (qeIds.indexOf(action.queryEditor.id) > -1) {
const tabHistory = state.tabHistory.slice();
tabHistory.push(action.queryEditor.id);
return Object.assign({}, state, { tabHistory });

View File

@ -8,7 +8,7 @@
"test": "spec"
},
"scripts": {
"test": "npm run lint && mocha --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/*_spec.*",
"test": "mocha --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/*_spec.*",
"cover": "babel-node ./node_modules/.bin/istanbul cover _mocha -- --require spec/helpers/browser.js --recursive spec/**/*_spec.*",
"dev": "NODE_ENV=dev webpack -d --watch --colors --progress",
"prod": "NODE_ENV=production node --max_old_space_size=8192 ./node_modules/webpack/bin/webpack.js -p --colors --progress",

View File

@ -17,4 +17,6 @@ Object.keys(document.defaultView).forEach((property) => {
global.navigator = {
userAgent: 'node.js',
platform: 'linux',
appName: 'Netscape',
};

View File

@ -1,13 +1,14 @@
import React from 'react';
import { beforeEach, describe, it } from 'mocha';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import QueryAndSaveButtons from '../../../../javascripts/explore/components/QueryAndSaveBtns';
describe('QueryAndSaveButtons', () => {
let defaultProps = {
const defaultProps = {
canAdd: 'True',
onQuery: () => {}
onQuery: () => {},
};
// It must render

View File

@ -0,0 +1,24 @@
import React from 'react';
import Alerts from '../../../javascripts/SqlLab/components/Alerts';
import { Alert } from 'react-bootstrap';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { alert } from './fixtures';
describe('Alerts', () => {
const mockedProps = {
alerts: [alert],
};
it('is valid', () => {
expect(React.isValidElement(<Alerts />)).to.equal(true);
});
it('is valid with props', () => {
expect(React.isValidElement(<Alerts {...mockedProps} />)).to.equal(true);
});
it('renders an Alert', () => {
const wrapper = shallow(<Alerts {...mockedProps} />);
expect(wrapper.find(Alert)).to.have.length(1);
});
});

View File

@ -0,0 +1,11 @@
import React from 'react';
import App from '../../../javascripts/SqlLab/components/App';
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('App', () => {
it('is valid', () => {
expect(React.isValidElement(<App />)).to.equal(true);
});
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import CopyQueryTabUrl from '../../../javascripts/SqlLab/components/CopyQueryTabUrl';
import CopyToClipboard from '../../../javascripts/components/CopyToClipboard';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { initialState } from './fixtures';
describe('CopyQueryTabUrl', () => {
const mockedProps = {
queryEditor: initialState.queryEditors[0],
};
it('should be valid', () => {
expect(React.isValidElement(<CopyQueryTabUrl />)).to.equal(true);
});
it('renders with props', () => {
expect(
React.isValidElement(<CopyQueryTabUrl {...mockedProps} />)
).to.equal(true);
});
it('renders a CopyToClipboard', () => {
const wrapper = shallow(<CopyQueryTabUrl {...mockedProps} />);
expect(wrapper.find(CopyToClipboard)).to.have.length(1);
});
});

View File

@ -0,0 +1,26 @@
import React from 'react';
import HighlightedSql from '../../../javascripts/SqlLab/components/HighlightedSql';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('HighlightedSql', () => {
const sql = "SELECT * FROM test WHERE something='fkldasjfklajdslfkjadlskfjkldasjfkladsjfkdjsa'";
it('renders', () => {
expect(React.isValidElement(<HighlightedSql />)).to.equal(true);
});
it('renders with props', () => {
expect(React.isValidElement(<HighlightedSql sql={sql} />))
.to.equal(true);
});
it('renders a SyntaxHighlighter', () => {
const wrapper = shallow(<HighlightedSql sql={sql} />);
expect(wrapper.find(SyntaxHighlighter)).to.have.length(1);
});
it('renders a SyntaxHighlighter while using shrink', () => {
const wrapper = shallow(<HighlightedSql sql={sql} shrink maxWidth={20} />);
expect(wrapper.find(SyntaxHighlighter)).to.have.length(1);
});
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import Link from '../../../javascripts/SqlLab/components/Link';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('Link', () => {
const mockedProps = {
tooltip: 'This is a tooltip',
href: 'http://www.airbnb.com',
};
it('renders', () => {
expect(React.isValidElement(<Link>TEST</Link>)).to.equal(true);
});
it('renders with props', () => {
expect(
React.isValidElement(<Link {...mockedProps} >TEST</Link>)
).to.equal(true);
});
it('renders an anchor tag', () => {
const wrapper = shallow(<Link {...mockedProps} >TEST</Link>);
expect(wrapper.find('a')).to.have.length(1);
});
});

View File

@ -7,24 +7,27 @@ import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('QuerySearch', () => {
it('should render', () => {
const mockedProps = {
actions: {},
};
it('is valid', () => {
expect(
React.isValidElement(<QuerySearch />)
React.isValidElement(<QuerySearch {...mockedProps} />)
).to.equal(true);
});
it('should have two Select', () => {
const wrapper = shallow(<QuerySearch />);
const wrapper = shallow(<QuerySearch {...mockedProps} />);
expect(wrapper.find(Select)).to.have.length(2);
});
it('should have one input for searchText', () => {
const wrapper = shallow(<QuerySearch />);
const wrapper = shallow(<QuerySearch {...mockedProps} />);
expect(wrapper.find('input')).to.have.length(1);
});
it('should have one Button', () => {
const wrapper = shallow(<QuerySearch />);
const wrapper = shallow(<QuerySearch {...mockedProps} />);
expect(wrapper.find(Button)).to.have.length(1);
});
});

View File

@ -0,0 +1,26 @@
import React from 'react';
import QueryTable from '../../../javascripts/SqlLab/components/QueryTable';
import { queries } from './fixtures';
import { mount } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('QueryTable', () => {
const mockedProps = {
queries,
};
it('is valid', () => {
expect(React.isValidElement(<QueryTable />)).to.equal(true);
});
it('is valid with props', () => {
expect(
React.isValidElement(<QueryTable {...mockedProps} />)
).to.equal(true);
});
it('renders a proper table', () => {
const wrapper = mount(<QueryTable {...mockedProps} />);
expect(wrapper.find('table')).to.have.length(1);
expect(wrapper.find('tr')).to.have.length(3);
});
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import ResultSet from '../../../javascripts/SqlLab/components/ResultSet';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { queries } from './fixtures';
import { Table } from 'reactable';
describe('ResultSet', () => {
const mockedProps = {
query: queries[0],
};
it('renders', () => {
expect(React.isValidElement(<ResultSet />)).to.equal(true);
});
it('renders with props', () => {
expect(
React.isValidElement(<ResultSet />)
).to.equal(true);
});
it('renders a Table', () => {
const wrapper = shallow(<ResultSet {...mockedProps} />);
expect(wrapper.find(Table)).to.have.length(1);
});
});

View File

@ -0,0 +1,24 @@
import React from 'react';
import SqlEditorLeftBar from '../../../javascripts/SqlLab/components/SqlEditorLeftBar';
import TableElement from '../../../javascripts/SqlLab/components/TableElement';
import { mount } from 'enzyme';
import { table, defaultQueryEditor } from './fixtures';
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('SqlEditorLeftBar', () => {
const mockedProps = {
tables: [table],
queryEditor: defaultQueryEditor,
};
it('is valid', () => {
expect(
React.isValidElement(<SqlEditorLeftBar {...mockedProps} />)
).to.equal(true);
});
it('renders a TableElement', () => {
const wrapper = mount(<SqlEditorLeftBar {...mockedProps} />);
expect(wrapper.find(TableElement)).to.have.length(1);
});
});

View File

@ -0,0 +1,27 @@
import React from 'react';
import SqlEditor from '../../../javascripts/SqlLab/components/SqlEditor';
import SqlEditorLeftBar from '../../../javascripts/SqlLab/components/SqlEditorLeftBar';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { initialState, queries, table } from './fixtures';
describe('SqlEditor', () => {
const mockedProps = {
actions: {},
database: {},
queryEditor: initialState.queryEditors[0],
latestQuery: queries[0],
tables: [table],
queries,
};
it('is valid', () => {
expect(
React.isValidElement(<SqlEditor {...mockedProps} />)
).to.equal(true);
});
it('render a SqlEditorLeftBar', () => {
const wrapper = shallow(<SqlEditor {...mockedProps} />);
expect(wrapper.find(SqlEditorLeftBar)).to.have.length(1);
});
});

View File

@ -0,0 +1,27 @@
import React from 'react';
import { TabbedSqlEditors } from '../../../javascripts/SqlLab/components/TabbedSqlEditors';
import { Tab } from 'react-bootstrap';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { initialState } from './fixtures';
describe('TabbedSqlEditors', () => {
const mockedProps = {
actions: {},
databases: {},
tables: [],
queries: {},
queryEditors: initialState.queryEditors,
tabHistory: initialState.tabHistory,
};
it('is valid', () => {
expect(
React.isValidElement(<TabbedSqlEditors {...mockedProps} />)
).to.equal(true);
});
it('shallow mounts', () => {
const wrapper = shallow(<TabbedSqlEditors {...mockedProps} />);
expect(wrapper.find(Tab)).to.have.length(2);
});
});

View File

@ -1,148 +1,22 @@
import React from 'react';
import Link from '../../../javascripts/SqlLab/components/Link';
import { Button } from 'react-bootstrap';
import { TableElement } from '../../../javascripts/SqlLab/components/TableElement';
import { shallow } from 'enzyme';
import TableElement from '../../../javascripts/SqlLab/components/TableElement';
import { table } from './fixtures';
import { mount, shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('TableElement', () => {
const mockedProps = {
'table': {
"dbId": 1,
"queryEditorId": "rJ-KP47a",
"schema": "caravel",
"name": "ab_user",
"id": "r11Vgt60",
"indexes": [
{
"unique": true,
"column_names": [
"username"
],
"type": "UNIQUE",
"name": "username"
},
{
"unique": true,
"column_names": [
"email"
],
"type": "UNIQUE",
"name": "email"
},
{
"unique": false,
"column_names": [
"created_by_fk"
],
"name": "created_by_fk"
},
{
"unique": false,
"column_names": [
"changed_by_fk"
],
"name": "changed_by_fk"
}
],
"columns": [
{
"indexed": false,
"longType": "INTEGER(11)",
"type": "INTEGER",
"name": "id"
},
{
"indexed": false,
"longType": "VARCHAR(64)",
"type": "VARCHAR",
"name": "first_name"
},
{
"indexed": false,
"longType": "VARCHAR(64)",
"type": "VARCHAR",
"name": "last_name"
},
{
"indexed": true,
"longType": "VARCHAR(64)",
"type": "VARCHAR",
"name": "username"
},
{
"indexed": false,
"longType": "VARCHAR(256)",
"type": "VARCHAR",
"name": "password"
},
{
"indexed": false,
"longType": "TINYINT(1)",
"type": "TINYINT",
"name": "active"
},
{
"indexed": true,
"longType": "VARCHAR(64)",
"type": "VARCHAR",
"name": "email"
},
{
"indexed": false,
"longType": "DATETIME",
"type": "DATETIME",
"name": "last_login"
},
{
"indexed": false,
"longType": "INTEGER(11)",
"type": "INTEGER",
"name": "login_count"
},
{
"indexed": false,
"longType": "INTEGER(11)",
"type": "INTEGER",
"name": "fail_login_count"
},
{
"indexed": false,
"longType": "DATETIME",
"type": "DATETIME",
"name": "created_on"
},
{
"indexed": false,
"longType": "DATETIME",
"type": "DATETIME",
"name": "changed_on"
},
{
"indexed": true,
"longType": "INTEGER(11)",
"type": "INTEGER",
"name": "created_by_fk"
},
{
"indexed": true,
"longType": "INTEGER(11)",
"type": "INTEGER",
"name": "changed_by_fk"
}
],
"expanded": true
}
}
it('should just render', () => {
table,
};
it('renders', () => {
expect(
React.isValidElement(<TableElement />)
).to.equal(true);
});
it('should render with props', () => {
it('renders with props', () => {
expect(
React.isValidElement(<TableElement {...mockedProps} />)
).to.equal(true);
@ -155,4 +29,7 @@ describe('TableElement', () => {
const wrapper = shallow(<TableElement {...mockedProps} />);
expect(wrapper.find('div.table-column')).to.have.length(14);
});
it('mounts', () => {
mount(<TableElement {...mockedProps} />);
});
});

View File

@ -0,0 +1,24 @@
import React from 'react';
import Timer from '../../../javascripts/SqlLab/components/Timer';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { queries } from './fixtures';
describe('Timer', () => {
const mockedProps = {
query: queries[0],
};
it('renders', () => {
expect(React.isValidElement(<Timer />)).to.equal(true);
});
it('renders with props', () => {
expect(React.isValidElement(<Timer {...mockedProps} />))
.to.equal(true);
});
it('renders a span', () => {
const wrapper = shallow(<Timer {...mockedProps} />);
expect(wrapper.find('span')).to.have.length(1);
});
});

View File

@ -0,0 +1,27 @@
import React from 'react';
import VisualizeModal from '../../../javascripts/SqlLab/components/VisualizeModal';
import { queries } from './fixtures';
import { Modal } from 'react-bootstrap';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('VisualizeModal', () => {
const mockedProps = {
show: true,
query: queries[0],
};
it('renders', () => {
expect(React.isValidElement(<VisualizeModal />)).to.equal(true);
});
it('renders with props', () => {
expect(
React.isValidElement(<VisualizeModal {...mockedProps} />)
).to.equal(true);
});
it('renders a Modal', () => {
const wrapper = shallow(<VisualizeModal {...mockedProps} />);
expect(wrapper.find(Modal)).to.have.length(1);
});
});

View File

@ -0,0 +1,229 @@
export const alert = { bsStyle: 'danger', msg: 'Ooops', id: 'lksvmcx32' };
export const table = {
dbId: 1,
selectStar: 'SELECT * FROM ab_user',
queryEditorId: 'rJ-KP47a',
schema: 'caravel',
name: 'ab_user',
id: 'r11Vgt60',
indexes: [
{
unique: true,
column_names: [
'username',
],
type: 'UNIQUE',
name: 'username',
},
{
unique: true,
column_names: [
'email',
],
type: 'UNIQUE',
name: 'email',
},
{
unique: false,
column_names: [
'created_by_fk',
],
name: 'created_by_fk',
},
{
unique: false,
column_names: [
'changed_by_fk',
],
name: 'changed_by_fk',
},
],
columns: [
{
indexed: false,
longType: 'INTEGER(11)',
type: 'INTEGER',
name: 'id',
},
{
indexed: false,
longType: 'VARCHAR(64)',
type: 'VARCHAR',
name: 'first_name',
},
{
indexed: false,
longType: 'VARCHAR(64)',
type: 'VARCHAR',
name: 'last_name',
},
{
indexed: true,
longType: 'VARCHAR(64)',
type: 'VARCHAR',
name: 'username',
},
{
indexed: false,
longType: 'VARCHAR(256)',
type: 'VARCHAR',
name: 'password',
},
{
indexed: false,
longType: 'TINYINT(1)',
type: 'TINYINT',
name: 'active',
},
{
indexed: true,
longType: 'VARCHAR(64)',
type: 'VARCHAR',
name: 'email',
},
{
indexed: false,
longType: 'DATETIME',
type: 'DATETIME',
name: 'last_login',
},
{
indexed: false,
longType: 'INTEGER(11)',
type: 'INTEGER',
name: 'login_count',
},
{
indexed: false,
longType: 'INTEGER(11)',
type: 'INTEGER',
name: 'fail_login_count',
},
{
indexed: false,
longType: 'DATETIME',
type: 'DATETIME',
name: 'created_on',
},
{
indexed: false,
longType: 'DATETIME',
type: 'DATETIME',
name: 'changed_on',
},
{
indexed: true,
longType: 'INTEGER(11)',
type: 'INTEGER',
name: 'created_by_fk',
},
{
indexed: true,
longType: 'INTEGER(11)',
type: 'INTEGER',
name: 'changed_by_fk',
},
],
expanded: true,
};
export const defaultQueryEditor = {
id: 'dfsadfs',
title: 'Untitled Query',
sql: 'SELECT *\nFROM\nWHERE',
latestQueryId: null,
autorun: false,
dbId: null,
};
export const queries = [
{
dbId: 1,
sql: 'SELECT *FROM caravel.slices',
sqlEditorId: 'SJ8YO72R',
tab: 'Demo',
runAsync: false,
ctas: false,
id: 'BkA1CLrJg',
progress: 100,
startDttm: 1476910566092.96,
state: 'success',
changedOn: 1476910566000,
tempTable: null,
userId: 1,
executedSql: null,
changed_on: '2016-10-19T20:56:06',
rows: 42,
endDttm: 1476910566798,
limit_reached: false,
schema: null,
errorMessage: null,
db: 'main',
user: 'admin',
limit: 1000,
serverId: 141,
resultsKey: null,
results: {
columns: ['col1', 'col2'],
data: [
{ col1: 0, col2: 1 },
{ col1: 2, col2: 3 },
],
},
},
{
dbId: 1,
sql: 'SELECT *FROM caravel.slices',
sqlEditorId: 'SJ8YO72R',
tab: 'Demo',
runAsync: true,
ctas: false,
id: 'S1zeAISkx',
progress: 100,
startDttm: 1476910570802.2,
state: 'success',
changedOn: 1476910572000,
tempTable: null,
userId: 1,
executedSql: (
'SELECT * \nFROM (SELECT created_on, changed_on, id, slice_name, ' +
'druid_datasource_id, table_id, datasource_type, datasource_name, ' +
'viz_type, params, created_by_fk, changed_by_fk, description, ' +
'cache_timeout, perm\nFROM caravel.slices) AS inner_qry \n LIMIT 1000'
),
changed_on: '2016-10-19T20:56:12',
rows: 42,
endDttm: 1476910579693,
limit_reached: false,
schema: null,
errorMessage: null,
db: 'main',
user: 'admin',
limit: 1000,
serverId: 142,
resultsKey: '417149f4-cd27-4f80-91f3-c45c871003f7',
results: null,
},
];
export const initialState = {
alerts: [],
showDataPreviewModal: false,
dataPreviewQueryId: null,
networkOn: true,
queries: {},
databases: {},
queryEditors: [defaultQueryEditor],
tabHistory: [defaultQueryEditor.id],
tables: [],
workspaceQueries: [],
queriesLastUpdate: 0,
};
export const query = {
dbId: 1,
sql: 'SELECT * FROM something',
sqlEditorId: defaultQueryEditor.id,
tab: 'unimportant',
tempTableName: null,
runAsync: false,
ctas: false,
};

View File

@ -1,18 +1,19 @@
import * as r from '../../../javascripts/SqlLab/reducers';
import * as actions from '../../../javascripts/SqlLab/actions';
import { describe, it } from 'mocha';
import { beforeEach, describe, it } from 'mocha';
import { expect } from 'chai';
import { alert, table, initialState } from './fixtures';
describe('sqlLabReducer', () => {
describe('CLONE_QUERY_TO_NEW_TAB', () => {
const testQuery = { sql: 'SELECT * FROM...', dbId: 1, id: 1 };
const state = Object.assign(r.initialState, { queries: [testQuery] });
const newState = r.sqlLabReducer(state, actions.cloneQueryToNewTab(testQuery));
const testQuery = { sql: 'SELECT * FROM...', dbId: 1, id: 'flasj233' };
let newState = Object.assign({}, initialState, { queries: { [testQuery.id]: testQuery } });
newState = r.sqlLabReducer(newState, actions.cloneQueryToNewTab(testQuery));
it('should have at most one more tab', () => {
expect(newState.queryEditors).have.length(2);
});
it('should have the same SQL as the cloned query', () => {
expect(newState.queryEditors[1].sql).to.equal(testQuery.sql);
});
@ -25,4 +26,124 @@ describe('sqlLabReducer', () => {
expect(newState.tabHistory[1]).to.eq(newState.queryEditors[1].id);
});
});
describe('Alerts', () => {
const state = Object.assign({}, initialState);
let newState;
it('should add one alert', () => {
newState = r.sqlLabReducer(state, actions.addAlert(alert));
expect(newState.alerts).to.have.lengthOf(1);
});
it('should remove one alert', () => {
newState = r.sqlLabReducer(newState, actions.removeAlert(newState.alerts[0]));
expect(newState.alerts).to.have.lengthOf(0);
});
});
describe('Query editors actions', () => {
let newState;
let defaultQueryEditor;
let qe;
beforeEach(() => {
newState = Object.assign({}, initialState);
defaultQueryEditor = newState.queryEditors[0];
qe = Object.assign({}, defaultQueryEditor);
newState = r.sqlLabReducer(newState, actions.addQueryEditor(qe));
qe = newState.queryEditors[newState.queryEditors.length - 1];
});
it('should add a query editor', () => {
expect(newState.queryEditors).to.have.lengthOf(2);
});
it('should remove a query editor', () => {
expect(newState.queryEditors).to.have.lengthOf(2);
newState = r.sqlLabReducer(newState, actions.removeQueryEditor(qe));
expect(newState.queryEditors).to.have.lengthOf(1);
});
it('should set q query editor active', () => {
newState = r.sqlLabReducer(newState, actions.addQueryEditor(qe));
newState = r.sqlLabReducer(newState, actions.setActiveQueryEditor(defaultQueryEditor));
expect(newState.tabHistory[newState.tabHistory.length - 1]).equals(defaultQueryEditor.id);
});
it('should not fail while setting DB', () => {
const dbId = 9;
newState = r.sqlLabReducer(newState, actions.queryEditorSetDb(qe, dbId));
expect(newState.queryEditors[1].dbId).to.equal(dbId);
});
it('should not fail while setting schema', () => {
const schema = 'foo';
newState = r.sqlLabReducer(newState, actions.queryEditorSetSchema(qe, schema));
expect(newState.queryEditors[1].schema).to.equal(schema);
});
it('should not fail while setting autorun ', () => {
newState = r.sqlLabReducer(newState, actions.queryEditorSetAutorun(qe, false));
expect(newState.queryEditors[1].autorun).to.equal(false);
newState = r.sqlLabReducer(newState, actions.queryEditorSetAutorun(qe, true));
expect(newState.queryEditors[1].autorun).to.equal(true);
});
it('should not fail while setting title', () => {
const title = 'a new title';
newState = r.sqlLabReducer(newState, actions.queryEditorSetTitle(qe, title));
expect(newState.queryEditors[1].title).to.equal(title);
});
it('should not fail while setting Sql', () => {
const sql = 'SELECT nothing from dev_null';
newState = r.sqlLabReducer(newState, actions.queryEditorSetSql(qe, sql));
expect(newState.queryEditors[1].sql).to.equal(sql);
});
});
describe('Tables', () => {
let newState;
let newTable;
beforeEach(() => {
newTable = Object.assign({}, table);
newState = r.sqlLabReducer(initialState, actions.mergeTable(newTable));
newTable = newState.tables[0];
});
it('should add a table', () => {
// Testing that beforeEach actually added the table
expect(newState.tables).to.have.lengthOf(1);
});
it('should merge the table attributes', () => {
// Merging the extra attribute
newTable.extra = true;
newState = r.sqlLabReducer(newState, actions.mergeTable(newTable));
expect(newState.tables).to.have.lengthOf(1);
expect(newState.tables[0].extra).to.equal(true);
});
it('should expand and collapse a table', () => {
newState = r.sqlLabReducer(newState, actions.collapseTable(newTable));
expect(newState.tables[0].expanded).to.equal(false);
newState = r.sqlLabReducer(newState, actions.expandTable(newTable));
expect(newState.tables[0].expanded).to.equal(true);
});
it('should remove a table', () => {
newState = r.sqlLabReducer(newState, actions.removeTable(newTable));
expect(newState.tables).to.have.lengthOf(0);
});
});
describe('Run Query', () => {
let newState;
let query;
let newQuery;
beforeEach(() => {
newState = Object.assign({}, initialState);
newQuery = Object.assign({}, query);
});
it('should start a query', () => {
newState = r.sqlLabReducer(newState, actions.startQuery(newQuery));
expect(Object.keys(newState.queries)).to.have.lengthOf(1);
});
it('should stop the query', () => {
newState = r.sqlLabReducer(newState, actions.startQuery(newQuery));
newState = r.sqlLabReducer(newState, actions.stopQuery(newQuery));
const q = newState.queries[Object.keys(newState.queries)[0]];
expect(q.state).to.equal('stopped');
});
it('should remove a query', () => {
newState = r.sqlLabReducer(newState, actions.startQuery(newQuery));
newState = r.sqlLabReducer(newState, actions.removeQuery(newQuery));
expect(Object.keys(newState.queries)).to.have.lengthOf(0);
});
it('should refresh queries when polling returns empty', () => {
newState = r.sqlLabReducer(newState, actions.refreshQueries({}));
});
});
});

1
caravel/static/coverage Symbolic link
View File

@ -0,0 +1 @@
assets/coverage/lcov-report/