Files
superset2/superset/assets/javascripts/chart/Chart.jsx
Maxime Beauchemin 69195f8d2d 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
2017-12-20 21:24:35 -08:00

194 lines
5.1 KiB
JavaScript

/* eslint camelcase: 0 */
import React from 'react';
import PropTypes from 'prop-types';
import Mustache from 'mustache';
import { d3format } from '../modules/utils';
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,
actions: PropTypes.object,
chartKey: PropTypes.string.isRequired,
containerId: PropTypes.string.isRequired,
datasource: PropTypes.object.isRequired,
formData: PropTypes.object.isRequired,
height: PropTypes.number,
width: PropTypes.number,
setControlValue: PropTypes.func,
timeout: PropTypes.number,
vizType: PropTypes.string.isRequired,
// state
chartAlert: PropTypes.string,
chartStatus: PropTypes.string,
chartUpdateEndTime: PropTypes.number,
chartUpdateStartTime: PropTypes.number,
latestQueryFormData: PropTypes.object,
queryRequest: PropTypes.object,
queryResponse: PropTypes.object,
lastRendered: PropTypes.number,
triggerQuery: PropTypes.bool,
// dashboard callbacks
addFilter: PropTypes.func,
getFilters: PropTypes.func,
clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
};
const defaultProps = {
addFilter: () => ({}),
getFilters: () => ({}),
clearFilter: () => ({}),
removeFilter: () => ({}),
};
class Chart extends React.PureComponent {
constructor(props) {
super(props);
// these properties are used by visualizations
this.annotationData = props.annotationData;
this.containerId = props.containerId;
this.selector = `#${this.containerId}`;
this.formData = props.formData;
this.datasource = props.datasource;
this.addFilter = this.addFilter.bind(this);
this.getFilters = this.getFilters.bind(this);
this.clearFilter = this.clearFilter.bind(this);
this.removeFilter = this.removeFilter.bind(this);
this.height = this.height.bind(this);
this.width = this.width.bind(this);
}
componentDidMount() {
if (this.props.triggerQuery) {
this.props.actions.runQuery(this.props.formData, false,
this.props.timeout,
this.props.chartKey,
);
}
}
componentWillReceiveProps(nextProps) {
this.annotationData = nextProps.annotationData;
this.containerId = nextProps.containerId;
this.selector = `#${this.containerId}`;
this.formData = nextProps.formData;
this.datasource = nextProps.datasource;
}
componentDidUpdate(prevProps) {
if (
this.props.queryResponse &&
this.props.chartStatus === 'success' &&
!this.props.queryResponse.error && (
prevProps.annotationData !== this.props.annotationData ||
prevProps.queryResponse !== this.props.queryResponse ||
prevProps.height !== this.props.height ||
prevProps.width !== this.props.width ||
prevProps.lastRendered !== this.props.lastRendered)
) {
this.renderViz();
}
}
getFilters() {
return this.props.getFilters();
}
addFilter(col, vals, merge = true, refresh = true) {
this.props.addFilter(col, vals, merge, refresh);
}
clearFilter() {
this.props.clearFilter();
}
removeFilter(col, vals, refresh = true) {
this.props.removeFilter(col, vals, refresh);
}
clearError() {
this.setState({
errorMsg: null,
});
}
width() {
return this.props.width || this.container.el.offsetWidth;
}
height() {
return this.props.height || this.container.el.offsetHeight;
}
d3format(col, number) {
const { datasource } = this.props;
const format = (datasource.column_formats && datasource.column_formats[col]) || '0.3s';
return d3format(format, number);
}
render_template(s) {
const context = {
width: this.width(),
height: this.height(),
};
return Mustache.render(s, context);
}
renderViz() {
const viz = visMap[this.props.vizType];
const fd = this.props.formData;
const qr = this.props.queryResponse;
try {
// 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);
}
}
render() {
const isLoading = this.props.chartStatus === 'loading';
return (
<div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
{isLoading &&
<Loading size={25} />
}
{this.props.chartAlert &&
<StackTraceMessage
message={this.props.chartAlert}
queryResponse={this.props.queryResponse}
/>
}
{!this.props.chartAlert &&
<ChartBody
containerId={this.containerId}
vizType={this.props.formData.viz_type}
height={this.height}
width={this.width}
ref={(inner) => {
this.container = inner;
}}
/>
}
</div>
);
}
}
Chart.propTypes = propTypes;
Chart.defaultProps = defaultProps;
export default Chart;