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]>'].

Reply via email to