Skip to content

Commit

Permalink
Introduce Javascript controls (apache#4076)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mistercrunch authored and michellethomas committed May 23, 2018
1 parent 16d3ac9 commit 4f98f3f
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 8 deletions.
10 changes: 9 additions & 1 deletion superset/assets/javascripts/chart/Chart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
};
Expand Down Expand Up @@ -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 (
Expand All @@ -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>
);
Expand Down
22 changes: 22 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
1 change: 1 addition & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ export const visTypes = {
controlPanelSections: [
{
label: t('Code'),
expanded: true,
controlSetRows: [
['markup_type'],
['code'],
Expand Down
25 changes: 25 additions & 0 deletions superset/assets/javascripts/modules/sandbox.js
Original file line number Diff line number Diff line change
@@ -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];
}
17 changes: 17 additions & 0 deletions superset/assets/spec/javascripts/modules/sandbox_spec.jsx
Original file line number Diff line number Diff line change
@@ -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');
});
});

0 comments on commit 4f98f3f

Please sign in to comment.