diff --git a/superset/assets/javascripts/chart/Chart.jsx b/superset/assets/javascripts/chart/Chart.jsx
index 958d353f95540..bd7e4f80c2b7b 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 47b5454f4ccd5..3e968546a5a3b 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 {
/>
);
}
+ renderModalBody() {
+ return (
+
+
{this.props.aboveEditorSection}
+ {this.renderEditor(true)}
+
+ );
+ }
render() {
const controlHeader = ;
return (
@@ -88,7 +94,7 @@ export default class TextAreaControl extends React.Component {
{t('Edit')} {this.props.language} {t('in modal')}
}
- modalBody={this.renderEditor(true)}
+ modalBody={this.renderModalBody(true)}
/>}
);
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index 7117712d2100e..95d4813f15af8 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 = (
+
+ {t('While this runs in a ')}
+ sandboxed vm
+ , {t('a set of')} useful objects are in context
+ {t('to be used where necessary.')}
+ );
+
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: (
+
+ Define a function that intercepts the data
object passed to the visualization
+ and returns a similarly shaped object. {sandboxedEvalInfo}
+
),
+ },
};
export default controls;
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index 2c5f2a61e1889..88f6710a9daff 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 0000000000000..24473adb3d644
--- /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 0000000000000..85b36472fca65
--- /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');
+ });
+});