Skip to content

Commit

Permalink
[sql lab] allow users to save their queries (#2528)
Browse files Browse the repository at this point in the history
* Allow users to save their queries

Fixing tests .

* Adding placeholder for Query Description

* initJQueryCSRF -> initJQueryAjaxCSRF
  • Loading branch information
mistercrunch authored Apr 5, 2017
1 parent c1d9918 commit 122891c
Show file tree
Hide file tree
Showing 31 changed files with 656 additions and 279 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
[BASIC]

# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_,d,e,v,o,l,x
good-names=i,j,k,ex,Run,_,d,e,v,o,l,x,ts

# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
Expand Down
3 changes: 3 additions & 0 deletions superset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from flask_appbuilder import SQLA, AppBuilder, IndexView
from flask_appbuilder.baseviews import expose
from flask_migrate import Migrate
from flask_wtf.csrf import CSRFProtect
from werkzeug.contrib.fixers import ProxyFix

from superset.connectors.connector_registry import ConnectorRegistry
Expand Down Expand Up @@ -50,6 +51,8 @@

db = SQLA(app)

if conf.get('WTF_CSRF_ENABLED'):
csrf = CSRFProtect(app)

utils.pessimistic_connection_handling(db.engine.pool)

Expand Down
36 changes: 36 additions & 0 deletions superset/assets/javascripts/SqlLab/actions.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global notify */
import shortid from 'shortid';
import { now } from '../modules/dates';
const $ = require('jquery');
Expand Down Expand Up @@ -33,11 +34,25 @@ export const QUERY_FAILED = 'QUERY_FAILED';
export const CLEAR_QUERY_RESULTS = 'CLEAR_QUERY_RESULTS';
export const REMOVE_DATA_PREVIEW = 'REMOVE_DATA_PREVIEW';
export const CHANGE_DATA_PREVIEW_ID = 'CHANGE_DATA_PREVIEW_ID';
export const SAVE_QUERY = 'SAVE_QUERY';

export function resetState() {
return { type: RESET_STATE };
}

export function saveQuery(query) {
const url = '/savedqueryviewapi/api/create';
$.ajax({
type: 'POST',
url,
data: query,
success: () => notify.success('Your query was saved'),
error: () => notify.error('Your query could not be saved'),
dataType: 'json',
});
return { type: SAVE_QUERY };
}

export function startQuery(query) {
Object.assign(query, {
id: query.id ? query.id : shortid.generate(),
Expand Down Expand Up @@ -328,6 +343,27 @@ export function popStoredQuery(urlId) {
};
dispatch(addQueryEditor(queryEditorProps));
},
error: () => notify.error("The query couldn't be loaded"),
});
};
}
export function popSavedQuery(saveQueryId) {
return function (dispatch) {
$.ajax({
type: 'GET',
url: `/savedqueryviewapi/api/get/${saveQueryId}`,
success: (data) => {
const sq = data.result;
const queryEditorProps = {
title: sq.label,
dbId: sq.db_id,
schema: sq.schema,
autorun: false,
sql: sq.sql,
};
dispatch(addQueryEditor(queryEditorProps));
},
error: () => notify.error("The query couldn't be loaded"),
});
};
}
34 changes: 0 additions & 34 deletions superset/assets/javascripts/SqlLab/components/Alerts.jsx

This file was deleted.

18 changes: 18 additions & 0 deletions superset/assets/javascripts/SqlLab/components/AlertsWrapper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import AlertContainer from 'react-alert';

export default class AlertsWrapper extends React.PureComponent {
render() {
return (
<AlertContainer
ref={ref => {
global.notify = ref;
}}
offset={14}
position="top right"
theme="dark"
time={5000}
transition="fade"
/>);
}
}
4 changes: 2 additions & 2 deletions superset/assets/javascripts/SqlLab/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import TabbedSqlEditors from './TabbedSqlEditors';
import QueryAutoRefresh from './QueryAutoRefresh';
import QuerySearch from './QuerySearch';
import Alerts from './Alerts';
import AlertsWrapper from './AlertsWrapper';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
Expand Down Expand Up @@ -64,7 +64,7 @@ class App extends React.PureComponent {
}
return (
<div className="App SqlLab">
<Alerts id="sqllab-alerts" alerts={this.props.alerts} actions={this.props.actions} />
<AlertsWrapper />
<div className="container-fluid">
{content}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function RunQueryActionButton(props) {
onClick={() => props.runQuery(false)}
key="run-btn"
>
<i className="fa fa-table" /> {runBtnText}
<i className="fa fa-refresh" /> {runBtnText}
</Button>
);

Expand Down
131 changes: 131 additions & 0 deletions superset/assets/javascripts/SqlLab/components/SaveQuery.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* global notify */
import React from 'react';
import { FormControl, FormGroup, Overlay, Popover, Row, Col } from 'react-bootstrap';
import Button from '../../components/Button';

const propTypes = {
defaultLabel: React.PropTypes.string,
sql: React.PropTypes.string,
schema: React.PropTypes.string,
dbId: React.PropTypes.number,
animation: React.PropTypes.bool,
onSave: React.PropTypes.func,
};
const defaultProps = {
defaultLabel: 'Undefined',
animation: true,
onSave: () => {},
};

class SaveQuery extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
description: '',
label: props.defaultLabel,
showSave: false,
};
this.toggleSave = this.toggleSave.bind(this);
this.onSave = this.onSave.bind(this);
this.onCancel = this.onCancel.bind(this);
this.onLabelChange = this.onLabelChange.bind(this);
this.onDescriptionChange = this.onDescriptionChange.bind(this);
}
onSave() {
const query = {
label: this.state.label,
description: this.state.description,
db_id: this.props.dbId,
schema: this.props.schema,
sql: this.props.sql,
};
this.props.onSave(query);
this.setState({ showSave: false });
}
onCancel() {
this.setState({ showSave: false });
}
onLabelChange(e) {
this.setState({ label: e.target.value });
}
onDescriptionChange(e) {
this.setState({ description: e.target.value });
}
renderPopover() {
return (
<Popover id="embed-code-popover">
<FormGroup bsSize="small" style={{ width: '350px' }}>
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">
Label
</label>
</small>
<FormControl
type="text"
placeholder="Label for your query"
value={this.state.label}
onChange={this.onLabelChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">Description</label>
</small>
<FormControl
componentClass="textarea"
placeholder="Write a description for your query"
value={this.state.description}
onChange={this.onDescriptionChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<Button
bsStyle="primary"
onClick={this.onSave}
className="m-r-3"
>
Save
</Button>
<Button onClick={this.onCancel} className="cancelQuery">
Cancel
</Button>
</Col>
</Row>
</FormGroup>
</Popover>
);
}
toggleSave(e) {
this.setState({ target: e.target, showSave: !this.state.showSave });
}
render() {
return (
<span className="SaveQuery">
<Overlay
trigger="click"
target={this.state.target}
show={this.state.showSave}
placement="bottom"
animation={this.props.animation}
>
{this.renderPopover()}
</Overlay>
<Button bsSize="small" className="toggleSave" onClick={this.toggleSave}>
<i className="fa fa-save" /> Save Query
</Button>
</span>
);
}
}
SaveQuery.propTypes = propTypes;
SaveQuery.defaultProps = defaultProps;

export default SaveQuery;
13 changes: 11 additions & 2 deletions superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import Button from '../../components/Button';

import SouthPane from './SouthPane';
import SaveQuery from './SaveQuery';
import Timer from '../../components/Timer';
import SqlEditorLeftBar from './SqlEditorLeftBar';
import AceEditorWrapper from './AceEditorWrapper';
Expand Down Expand Up @@ -101,6 +102,7 @@ class SqlEditor extends React.PureComponent {
}

render() {
const qe = this.props.queryEditor;
let limitWarning = null;
if (this.props.latestQuery && this.props.latestQuery.limit_reached) {
const tooltip = (
Expand Down Expand Up @@ -149,12 +151,19 @@ class SqlEditor extends React.PureComponent {
<Form inline>
<RunQueryActionButton
allowAsync={this.props.database ? this.props.database.allow_run_async : false}
dbId={this.props.queryEditor.dbId}
dbId={qe.dbId}
queryState={this.props.latestQuery && this.props.latestQuery.state}
runQuery={this.runQuery.bind(this)}
selectedText={this.props.queryEditor.selectedText}
selectedText={qe.selectedText}
stopQuery={this.stopQuery.bind(this)}
/>
<SaveQuery
defaultLabel={qe.title}
sql={qe.sql}
onSave={this.props.actions.saveQuery}
schema={qe.schema}
dbId={qe.dbId}
/>
{ctasControls}
</Form>
</div>
Expand Down
28 changes: 14 additions & 14 deletions superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as Actions from '../actions';
import SqlEditor from './SqlEditor';
import CopyQueryTabUrl from './CopyQueryTabUrl';
import { areArraysShallowEqual } from '../../reduxUtils';
import { getParamFromQuery } from '../../../utils/common';
import URI from 'urijs';

const propTypes = {
actions: React.PropTypes.object.isRequired,
Expand Down Expand Up @@ -35,19 +35,19 @@ class TabbedSqlEditors extends React.PureComponent {
};
}
componentDidMount() {
const search = window.location.search;
if (search) {
const queryString = search.substring(1);
const urlId = getParamFromQuery(queryString, 'id');
if (urlId) {
this.props.actions.popStoredQuery(urlId);
} else {
let dbId = getParamFromQuery(queryString, 'dbid');
const query = URI(window.location).search(true);
if (query.id || query.sql || query.savedQueryId) {
if (query.id) {
this.props.actions.popStoredQuery(query.id);
} else if (query.savedQueryId) {
this.props.actions.popSavedQuery(query.savedQueryId);
} else if (query.sql) {
let dbId = query.dbid;
if (dbId) {
dbId = parseInt(dbId, 10);
} else {
const databases = this.props.databases;
const dbName = getParamFromQuery(queryString, 'dbname');
const dbName = query.dbname;
if (dbName) {
Object.keys(databases).forEach((db) => {
if (databases[db].database_name === dbName) {
Expand All @@ -57,11 +57,11 @@ class TabbedSqlEditors extends React.PureComponent {
}
}
const newQueryEditor = {
title: getParamFromQuery(queryString, 'title'),
title: query.title,
dbId,
schema: getParamFromQuery(queryString, 'schema'),
autorun: getParamFromQuery(queryString, 'autorun'),
sql: getParamFromQuery(queryString, 'sql'),
schema: query.schema,
autorun: query.autorun,
sql: query.sql,
};
this.props.actions.addQueryEditor(newQueryEditor);
}
Expand Down
Loading

0 comments on commit 122891c

Please sign in to comment.