Skip to content

Commit

Permalink
[sql lab] deeper support for templating (apache#3996)
Browse files Browse the repository at this point in the history
* [sql lab] deeper support for templating

* Fixing py tests

* Fix typo
  • Loading branch information
mistercrunch committed Dec 19, 2017
1 parent 6ed060c commit 9a1ed40
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 8 deletions.
12 changes: 10 additions & 2 deletions docs/sqllab.rst
Expand Up @@ -48,17 +48,25 @@ Available macros

We expose certain modules from Python's standard library in
Superset's Jinja context:

- ``time``: ``time``
- ``datetime``: ``datetime.datetime``
- ``uuid``: ``uuid``
- ``random``: ``random``
- ``relativedelta``: ``dateutil.relativedelta.relativedelta``
- more to come!

`Jinja's builtin filters <http://jinja.pocoo.org/docs/dev/templates/>`_ can be also be applied where needed.


.. autoclass:: superset.jinja_context.PrestoTemplateProcessor
:members:

.. autofunction:: superset.jinja_context.url_param

Extending macros
''''''''''''''''

As mentioned in the `Installation & Configuration`_ documentation,
it's possible for administrators to expose more more macros in their
environment using the configuration variable ``JINJA_CONTEXT_ADDONS``.
All objects referenced in this dictionary will become available for users
to integrate in their queries in **SQL Lab**.
6 changes: 6 additions & 0 deletions superset/assets/javascripts/SqlLab/actions.js
Expand Up @@ -20,6 +20,7 @@ export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA';
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 QUERY_EDITOR_SET_TEMPLATE_PARAMS = 'QUERY_EDITOR_SET_TEMPLATE_PARAMS';
export const QUERY_EDITOR_SET_SELECTED_TEXT = 'QUERY_EDITOR_SET_SELECTED_TEXT';
export const QUERY_EDITOR_PERSIST_HEIGHT = 'QUERY_EDITOR_PERSIST_HEIGHT';

Expand Down Expand Up @@ -132,6 +133,7 @@ export function runQuery(query) {
tab: query.tab,
tmp_table_name: query.tempTableName,
select_as_cta: query.ctas,
templateParams: query.templateParams,
};
const sqlJsonUrl = '/superset/sql_json/' + location.search;
$.ajax({
Expand Down Expand Up @@ -248,6 +250,10 @@ export function queryEditorSetSql(queryEditor, sql) {
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
}

export function queryEditorSetTemplateParams(queryEditor, templateParams) {
return { type: QUERY_EDITOR_SET_TEMPLATE_PARAMS, queryEditor, templateParams };
}

export function queryEditorSetSelectedText(queryEditor, sql) {
return { type: QUERY_EDITOR_SET_SELECTED_TEXT, queryEditor, sql };
}
Expand Down
10 changes: 10 additions & 0 deletions superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
Expand Up @@ -15,6 +15,7 @@ import {
import SplitPane from 'react-split-pane';

import Button from '../../components/Button';
import TemplateParamsEditor from './TemplateParamsEditor';
import SouthPane from './SouthPane';
import SaveQuery from './SaveQuery';
import Timer from '../../components/Timer';
Expand All @@ -24,6 +25,7 @@ import { STATE_BSSTYLE_MAP } from '../constants';
import RunQueryActionButton from './RunQueryActionButton';
import { t } from '../../locales';


const propTypes = {
actions: PropTypes.object.isRequired,
height: PropTypes.string.isRequired,
Expand Down Expand Up @@ -95,6 +97,7 @@ class SqlEditor extends React.PureComponent {
tab: qe.title,
schema: qe.schema,
tempTableName: ctas ? this.state.ctas : '',
templateParams: qe.templateParams,
runAsync,
ctas,
};
Expand Down Expand Up @@ -189,6 +192,13 @@ class SqlEditor extends React.PureComponent {
</Form>
</div>
<div className="pull-right">
<TemplateParamsEditor
language="json"
onChange={(params) => {
this.props.actions.queryEditorSetTemplateParams(qe, params);
}}
code={qe.templateParams}
/>
{limitWarning}
{this.props.latestQuery &&
<Timer
Expand Down
129 changes: 129 additions & 0 deletions superset/assets/javascripts/SqlLab/components/TemplateParamsEditor.jsx
@@ -0,0 +1,129 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Badge } from 'react-bootstrap';

import AceEditor from 'react-ace';
import 'brace/mode/sql';
import 'brace/mode/json';
import 'brace/mode/html';
import 'brace/mode/markdown';
import 'brace/theme/textmate';

import ModalTrigger from '../../components/ModalTrigger';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
import Button from '../../components/Button';
import { t } from '../../locales';

const propTypes = {
onChange: PropTypes.func,
code: PropTypes.string,
language: PropTypes.oneOf(['yaml', 'json']),
};

const defaultProps = {
label: null,
description: null,
onChange: () => {},
code: '{}',
};

export default class TemplateParamsEditor extends React.Component {
constructor(props) {
super(props);
const codeText = props.code || '{}';
this.state = {
codeText,
parsedJSON: null,
isValid: true,
};
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
this.onChange(this.state.codeText);
}
onChange(value) {
const codeText = value;
let isValid;
let parsedJSON = {};
try {
parsedJSON = JSON.parse(value);
isValid = true;
} catch (e) {
isValid = false;
}
this.setState({ parsedJSON, isValid, codeText });
if (isValid) {
this.props.onChange(codeText);
} else {
this.props.onChange('{}');
}
}
renderDoc() {
return (
<p>
Assign a set of parameters as <code>JSON</code> below
(example: <code>{'{"my_table": "foo"}'}</code>),
and they become available
in your SQL (example: <code>SELECT * FROM {'{{ my_table }}'} </code>)
by using
<a
href="http://superset.apache.org/sqllab.html#templating-with-jinja"
target="_blank"
rel="noopener noreferrer"
>
Jinja templating
</a> syntax.
</p>
);
}
renderModalBody() {
return (
<div>
{this.renderDoc()}
<AceEditor
mode={this.props.language}
theme="textmate"
style={{ border: '1px solid #CCC' }}
minLines={25}
maxLines={50}
onChange={this.onChange}
width="100%"
editorProps={{ $blockScrolling: true }}
enableLiveAutocompletion
value={this.state.codeText}
/>
</div>
);
}
render() {
const paramCount = this.state.parsedJSON ? Object.keys(this.state.parsedJSON).length : 0;
return (
<ModalTrigger
modalTitle={t('Template Parameters')}
triggerNode={
<Button
className="m-r-5"
tooltip={t('Edit template parameters')}
>
{`${t('parameters')} `}
{paramCount > 0 &&
<Badge>{paramCount}</Badge>
}
{!this.state.isValid &&
<InfoTooltipWithTrigger
icon="exclamation-triangle"
bsStyle="danger"
tooltip={t('Invalid JSON')}
label="invalid-json"
/>
}
</Button>
}
modalBody={this.renderModalBody(true)}
/>
);
}
}

TemplateParamsEditor.propTypes = propTypes;
TemplateParamsEditor.defaultProps = defaultProps;
3 changes: 3 additions & 0 deletions superset/assets/javascripts/SqlLab/reducers.js
Expand Up @@ -211,6 +211,9 @@ export const sqlLabReducer = function (state, action) {
[actions.QUERY_EDITOR_SET_SQL]() {
return alterInArr(state, 'queryEditors', action.queryEditor, { sql: action.sql });
},
[actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS]() {
return alterInArr(state, 'queryEditors', action.queryEditor, { templateParams: action.templateParams });
},
[actions.QUERY_EDITOR_SET_SELECTED_TEXT]() {
return alterInArr(state, 'queryEditors', action.queryEditor, { selectedText: action.sql });
},
Expand Down
Expand Up @@ -21,7 +21,7 @@ const tooltipStyle = { wordWrap: 'break-word' };

export default function InfoTooltipWithTrigger({
label, tooltip, icon, className, onClick, placement, bsStyle }) {
const iconClass = `fa fa-${icon} ${className} ${bsStyle ? 'text-' + bsStyle : ''}`;
const iconClass = `fa fa-${icon} ${className} ${bsStyle ? `text-${bsStyle}` : ''}`;
const iconEl = (
<i
className={iconClass}
Expand Down
11 changes: 8 additions & 3 deletions superset/sql_lab.py
Expand Up @@ -87,11 +87,13 @@ def get_session(nullpool):

@celery_app.task(bind=True, soft_time_limit=SQLLAB_TIMEOUT)
def get_sql_results(
ctask, query_id, return_results=True, store_results=False, user_name=None):
ctask, query_id, return_results=True, store_results=False,
user_name=None, template_params=None):
"""Executes the sql query returns the results."""
try:
return execute_sql(
ctask, query_id, return_results, store_results, user_name)
ctask, query_id, return_results, store_results, user_name,
template_params)
except Exception as e:
logging.exception(e)
stats_logger.incr('error_sqllab_unhandled')
Expand All @@ -106,6 +108,7 @@ def get_sql_results(

def execute_sql(
ctask, query_id, return_results=True, store_results=False, user_name=None,
template_params=None,
):
"""Executes the sql query returns the results."""
session = get_session(not ctask.request.called_directly)
Expand Down Expand Up @@ -161,7 +164,9 @@ def handle_error(msg):
try:
template_processor = get_template_processor(
database=database, query=query)
executed_sql = template_processor.process_template(executed_sql)
tp = template_params or {}
executed_sql = template_processor.process_template(
executed_sql, **tp)
except Exception as e:
logging.exception(e)
msg = 'Template rendering failed: ' + utils.error_msg_from_exception(e)
Expand Down
9 changes: 7 additions & 2 deletions superset/views/core.py
Expand Up @@ -2215,6 +2215,8 @@ def sql_json(self):
sql = request.form.get('sql')
database_id = request.form.get('database_id')
schema = request.form.get('schema') or None
template_params = json.loads(
request.form.get('templateParams') or '{}')

session = db.session()
mydb = session.query(models.Database).filter_by(id=database_id).first()
Expand Down Expand Up @@ -2266,7 +2268,9 @@ def sql_json(self):
try:
sql_lab.get_sql_results.delay(
query_id=query_id, return_results=False,
store_results=not query.select_as_cta, user_name=g.user.username)
store_results=not query.select_as_cta,
user_name=g.user.username,
template_params=template_params)
except Exception as e:
logging.exception(e)
msg = (
Expand Down Expand Up @@ -2295,7 +2299,8 @@ def sql_json(self):
error_message=timeout_msg):
# pylint: disable=no-value-for-parameter
data = sql_lab.get_sql_results(
query_id=query_id, return_results=True)
query_id=query_id, return_results=True,
template_params=template_params)
except Exception as e:
logging.exception(e)
return json_error_response('{}'.format(e))
Expand Down

0 comments on commit 9a1ed40

Please sign in to comment.