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 69195f8  Introduce Javascript controls (#4076)
69195f8 is described below

commit 69195f8d2d44d5b5fdf2ee338e4489b826fa5363
Author: Maxime Beauchemin <[email protected]>
AuthorDate: Wed Dec 20 21:24:35 2017 -0800

    Introduce Javascript controls (#4076)
    
    * Introduce Javascript controls
    
    This allows power-users to perform intricate transformations on data and
    objects using javascript code.
    
    The operations allowed are "sanboxed" or limited using node's vm
    `runInNewContext`
    https://nodejs.org/api/vm.html#vm_vm_runinnewcontext_code_sandbox_options
    
    For now I'm only enabling in the line chart visualization, but the plan
    would be to go towards offering more power to people who can write some
    JS moving forward.
    
    * Not applied
---
 superset/assets/javascripts/chart/Chart.jsx        | 10 ++++++++-
 .../components/controls/TextAreaControl.jsx        | 20 +++++++++++------
 .../assets/javascripts/explore/stores/controls.jsx | 22 +++++++++++++++++++
 .../assets/javascripts/explore/stores/visTypes.js  |  1 +
 superset/assets/javascripts/modules/sandbox.js     | 25 ++++++++++++++++++++++
 .../spec/javascripts/modules/sandbox_spec.jsx      | 17 +++++++++++++++
 6 files changed, 87 insertions(+), 8 deletions(-)

diff --git a/superset/assets/javascripts/chart/Chart.jsx 
b/superset/assets/javascripts/chart/Chart.jsx
index 958d353..bd7e4f8 100644
--- a/superset/assets/javascripts/chart/Chart.jsx
+++ b/superset/assets/javascripts/chart/Chart.jsx
@@ -8,6 +8,7 @@ import ChartBody from './ChartBody';
 import Loading from '../components/Loading';
 import StackTraceMessage from '../components/StackTraceMessage';
 import visMap from '../../visualizations/main';
+import sandboxedEval from '../modules/sandbox';
 
 const propTypes = {
   annotationData: PropTypes.object,
@@ -141,8 +142,15 @@ class Chart extends React.PureComponent {
 
   renderViz() {
     const viz = visMap[this.props.vizType];
+    const fd = this.props.formData;
+    const qr = this.props.queryResponse;
     try {
-      viz(this, this.props.queryResponse, this.props.setControlValue);
+      // Executing user-defined data mutator function
+      if (fd.js_data) {
+        qr.data = sandboxedEval(fd.js_data)(qr.data);
+      }
+      // [re]rendering the visualization
+      viz(this, qr, this.props.setControlValue);
     } catch (e) {
       this.props.actions.chartRenderingFailed(e, this.props.chartKey);
     }
diff --git 
a/superset/assets/javascripts/explore/components/controls/TextAreaControl.jsx 
b/superset/assets/javascripts/explore/components/controls/TextAreaControl.jsx
index 47b5454..3e96854 100644
--- 
a/superset/assets/javascripts/explore/components/controls/TextAreaControl.jsx
+++ 
b/superset/assets/javascripts/explore/components/controls/TextAreaControl.jsx
@@ -7,6 +7,7 @@ import 'brace/mode/sql';
 import 'brace/mode/json';
 import 'brace/mode/html';
 import 'brace/mode/markdown';
+import 'brace/mode/javascript';
 
 import 'brace/theme/textmate';
 
@@ -16,24 +17,21 @@ import { t } from '../../../locales';
 
 const propTypes = {
   name: PropTypes.string.isRequired,
-  label: PropTypes.string,
-  description: PropTypes.string,
   onChange: PropTypes.func,
   value: PropTypes.string,
   height: PropTypes.number,
-  language: PropTypes.oneOf([null, 'json', 'html', 'sql', 'markdown']),
   minLines: PropTypes.number,
   maxLines: PropTypes.number,
   offerEditInModal: PropTypes.bool,
+  language: PropTypes.oneOf([null, 'json', 'html', 'sql', 'markdown', 
'javascript']),
+  aboveEditorSection: PropTypes.node,
 };
 
 const defaultProps = {
-  label: null,
-  description: null,
   onChange: () => {},
   value: '',
   height: 250,
-  minLines: 10,
+  minLines: 3,
   maxLines: 10,
   offerEditInModal: true,
 };
@@ -73,6 +71,14 @@ export default class TextAreaControl extends React.Component 
{
         />
       </FormGroup>);
   }
+  renderModalBody() {
+    return (
+      <div>
+        <div>{this.props.aboveEditorSection}</div>
+        {this.renderEditor(true)}
+      </div>
+    );
+  }
   render() {
     const controlHeader = <ControlHeader {...this.props} />;
     return (
@@ -88,7 +94,7 @@ export default class TextAreaControl extends React.Component {
                 {t('Edit')} <strong>{this.props.language}</strong> {t('in 
modal')}
               </Button>
             }
-            modalBody={this.renderEditor(true)}
+            modalBody={this.renderModalBody(true)}
           />}
       </div>
     );
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx 
b/superset/assets/javascripts/explore/stores/controls.jsx
index 7117712..95d4813 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -46,6 +46,15 @@ const sortAxisChoices = [
   ['value_desc', 'sum(value) descending'],
 ];
 
+const sandboxUrl = 
'https://github.com/apache/incubator-superset/blob/master/superset/assets/javascripts/modules/sandbox.js';
+const sandboxedEvalInfo = (
+  <span>
+    {t('While this runs in a ')}
+    <a 
href="https://nodejs.org/api/vm.html#vm_script_runinnewcontext_sandbox_options";>sandboxed
 vm</a>
+    , {t('a set of')}<a href={sandboxUrl}> useful objects are in context </a>
+    {t('to be used where necessary.')}
+  </span>);
+
 const groupByControl = {
   type: 'SelectControl',
   multi: true,
@@ -1759,5 +1768,18 @@ export const controls = {
     default: false,
   },
 
+  js_data: {
+    type: 'TextAreaControl',
+    label: t('Javascript data mutator'),
+    description: t('Define a function that receives intercepts the data 
objects and can mutate it'),
+    language: 'javascript',
+    default: '',
+    height: 100,
+    aboveEditorSection: (
+      <p>
+        Define a function that intercepts the <code>data</code> object passed 
to the visualization
+        and returns a similarly shaped object. {sandboxedEvalInfo}
+      </p>),
+  },
 };
 export default controls;
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js 
b/superset/assets/javascripts/explore/stores/visTypes.js
index 2c5f2a6..88f6710 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -607,6 +607,7 @@ export const visTypes = {
     controlPanelSections: [
       {
         label: t('Code'),
+        expanded: true,
         controlSetRows: [
           ['markup_type'],
           ['code'],
diff --git a/superset/assets/javascripts/modules/sandbox.js 
b/superset/assets/javascripts/modules/sandbox.js
new file mode 100644
index 0000000..24473ad
--- /dev/null
+++ b/superset/assets/javascripts/modules/sandbox.js
@@ -0,0 +1,25 @@
+// A safe alternative to JS's eval
+import vm from 'vm';
+import _ from 'underscore';
+
+// Objects exposed here should be treated like a public API
+// if `underscore` had backwards incompatible changes in a future release, we'd
+// have to be careful about bumping the library as those changes could break 
user charts
+const GLOBAL_CONTEXT = {
+  console,
+  _,
+};
+
+// Copied/modified from 
https://github.com/hacksparrow/safe-eval/blob/master/index.js
+export default function sandboxedEval(code, context, opts) {
+  const sandbox = {};
+  const resultKey = 'SAFE_EVAL_' + Math.floor(Math.random() * 1000000);
+  sandbox[resultKey] = {};
+  const codeToEval = resultKey + '=' + code;
+  const sandboxContext = { ...GLOBAL_CONTEXT, ...context };
+  Object.keys(sandboxContext).forEach(function (key) {
+    sandbox[key] = sandboxContext[key];
+  });
+  vm.runInNewContext(codeToEval, sandbox, opts);
+  return sandbox[resultKey];
+}
diff --git a/superset/assets/spec/javascripts/modules/sandbox_spec.jsx 
b/superset/assets/spec/javascripts/modules/sandbox_spec.jsx
new file mode 100644
index 0000000..85b3647
--- /dev/null
+++ b/superset/assets/spec/javascripts/modules/sandbox_spec.jsx
@@ -0,0 +1,17 @@
+import { it, describe } from 'mocha';
+import { expect } from 'chai';
+
+import sandboxedEval from '../../../javascripts/modules/sandbox';
+
+describe('sandboxedEval', () => {
+  it('works like a basic eval', () => {
+    expect(sandboxedEval('100')).to.equal(100);
+    expect(sandboxedEval('v => v * 2')(5)).to.equal(10);
+  });
+  it('d3 is in context and works', () => {
+    expect(sandboxedEval("l => _.find(l, s => s === 'bar')")(['foo', 
'bar'])).to.equal('bar');
+  });
+  it('passes context as expected', () => {
+    expect(sandboxedEval('foo', { foo: 'bar' })).to.equal('bar');
+  });
+});

-- 
To stop receiving notification emails like this one, please contact
['"[email protected]" <[email protected]>'].

Reply via email to