Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sql lab] deeper support for templating #3996

Merged
merged 3 commits into from
Dec 19, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/sqllab.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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> bellow
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/bellow/below/g

(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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -2161,6 +2161,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 '{}')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get takes a default parameter:

template_params = json.loads(request.form.get('templateParams', '{}'))

Copy link
Member Author

@mistercrunch mistercrunch Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent of the or is to handle an existing key with an empty string value, though I could probably trim


session = db.session()
mydb = session.query(models.Database).filter_by(id=database_id).first()
Expand Down Expand Up @@ -2212,7 +2214,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 @@ -2241,7 +2245,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