This is an automated email from the ASF dual-hosted git repository.
maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 44e753d [sql lab] deeper support for templating (#3996)
44e753d is described below
commit 44e753d94d468c351462df6a893a041474656ddd
Author: Maxime Beauchemin <[email protected]>
AuthorDate: Tue Dec 19 15:55:58 2017 -0800
[sql lab] deeper support for templating (#3996)
* [sql lab] deeper support for templating
* Fixing py tests
* Fix typo
---
docs/sqllab.rst | 12 +-
superset/assets/javascripts/SqlLab/actions.js | 6 +
.../javascripts/SqlLab/components/SqlEditor.jsx | 10 ++
.../SqlLab/components/TemplateParamsEditor.jsx | 129 +++++++++++++++++++++
superset/assets/javascripts/SqlLab/reducers.js | 3 +
.../components/InfoTooltipWithTrigger.jsx | 2 +-
superset/sql_lab.py | 11 +-
superset/views/core.py | 9 +-
8 files changed, 174 insertions(+), 8 deletions(-)
diff --git a/docs/sqllab.rst b/docs/sqllab.rst
index a1da6c7..6b87543 100644
--- a/docs/sqllab.rst
+++ b/docs/sqllab.rst
@@ -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**.
diff --git a/superset/assets/javascripts/SqlLab/actions.js
b/superset/assets/javascripts/SqlLab/actions.js
index 2541ee5..d1fbfea 100644
--- a/superset/assets/javascripts/SqlLab/actions.js
+++ b/superset/assets/javascripts/SqlLab/actions.js
@@ -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';
@@ -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({
@@ -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 };
}
diff --git a/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
b/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
index 4dcaede..4b2e899 100644
--- a/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
+++ b/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
@@ -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';
@@ -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,
@@ -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,
};
@@ -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
diff --git
a/superset/assets/javascripts/SqlLab/components/TemplateParamsEditor.jsx
b/superset/assets/javascripts/SqlLab/components/TemplateParamsEditor.jsx
new file mode 100644
index 0000000..d22b9ab
--- /dev/null
+++ b/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;
diff --git a/superset/assets/javascripts/SqlLab/reducers.js
b/superset/assets/javascripts/SqlLab/reducers.js
index 3a49bd1..f01f2c3 100644
--- a/superset/assets/javascripts/SqlLab/reducers.js
+++ b/superset/assets/javascripts/SqlLab/reducers.js
@@ -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 });
},
diff --git a/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx
b/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx
index d86d051..caacb91 100644
--- a/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx
+++ b/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx
@@ -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}
diff --git a/superset/sql_lab.py b/superset/sql_lab.py
index 399faee..d7b0878 100644
--- a/superset/sql_lab.py
+++ b/superset/sql_lab.py
@@ -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')
@@ -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)
@@ -161,7 +164,9 @@ def execute_sql(
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)
diff --git a/superset/views/core.py b/superset/views/core.py
index 4d55fae..02e6f1d 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -2215,6 +2215,8 @@ class Superset(BaseSupersetView):
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()
@@ -2266,7 +2268,9 @@ class Superset(BaseSupersetView):
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 = (
@@ -2295,7 +2299,8 @@ class Superset(BaseSupersetView):
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))
--
To stop receiving notification emails like this one, please contact
['"[email protected]" <[email protected]>'].