You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by gr...@apache.org on 2017/11/08 18:46:23 UTC
[incubator-superset] branch master updated: Dashboard refactory
(#3581)
This is an automated email from the ASF dual-hosted git repository.
graceguo 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 4fa1f0a Dashboard refactory (#3581)
4fa1f0a is described below
commit 4fa1f0ab17e239e95826a020653f7bd21df46493
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Wed Nov 8 10:46:21 2017 -0800
Dashboard refactory (#3581)
Create Chart component for all chart fetching and rendering, and apply redux architecture in dashboard view.
---
superset/assets/javascripts/chart/Chart.jsx | 184 +++++++++++
superset/assets/javascripts/chart/ChartBody.jsx | 54 +++
.../assets/javascripts/chart/ChartContainer.jsx | 28 ++
superset/assets/javascripts/chart/chartAction.js | 91 ++++++
superset/assets/javascripts/chart/chartReducer.js | 100 ++++++
.../assets/javascripts/components/FaveStar.jsx | 9 +-
.../javascripts/components/StackTraceMessage.jsx | 59 ++++
.../assets/javascripts/dashboard/Dashboard.jsx | 4 +-
superset/assets/javascripts/dashboard/actions.js | 112 +++++++
.../javascripts/dashboard/components/Controls.jsx | 26 +-
.../javascripts/dashboard/components/Dashboard.jsx | 349 ++++++++++++++++++++
.../dashboard/components/DashboardAlert.jsx | 21 ++
.../dashboard/components/DashboardContainer.jsx | 29 ++
.../javascripts/dashboard/components/GridCell.jsx | 132 ++++++++
.../dashboard/components/GridLayout.jsx | 218 +++++++------
.../javascripts/dashboard/components/Header.jsx | 44 ++-
.../javascripts/dashboard/components/SaveModal.jsx | 21 +-
.../dashboard/components/SliceAdder.jsx | 26 +-
.../javascripts/dashboard/components/SliceCell.jsx | 117 -------
.../dashboard/components/SliceHeader.jsx | 142 ++++++++
superset/assets/javascripts/dashboard/index.jsx | 29 ++
superset/assets/javascripts/dashboard/reducers.js | 188 +++++++++++
.../javascripts/explore/actions/chartActions.js | 74 -----
.../javascripts/explore/actions/exploreActions.js | 11 +-
.../explore/components/ChartContainer.jsx | 362 ---------------------
.../explore/components/EmbedCodeButton.jsx | 4 +-
.../explore/components/ExploreChartHeader.jsx | 140 ++++++++
.../explore/components/ExploreChartPanel.jsx | 79 +++++
.../explore/components/ExploreViewContainer.jsx | 47 +--
.../javascripts/explore/components/SaveModal.jsx | 6 +-
superset/assets/javascripts/explore/index.jsx | 22 +-
.../javascripts/explore/reducers/chartReducer.js | 80 -----
.../javascripts/explore/reducers/exploreReducer.js | 8 -
.../assets/javascripts/explore/reducers/index.js | 4 +-
superset/assets/javascripts/modules/utils.js | 8 +
superset/assets/javascripts/reduxUtils.js | 19 +-
.../spec/javascripts/dashboard/SliceCell_spec.jsx | 24 --
.../assets/spec/javascripts/dashboard/fixtures.jsx | 2 +-
.../spec/javascripts/explore/chartActions_spec.js | 2 +-
...Container_spec.js => ExploreChartPanel_spec.js} | 0
.../explore/components/SaveModal_spec.jsx | 2 +-
.../javascripts/explore/exploreActions_spec.js | 3 +-
superset/assets/stylesheets/dashboard.css | 19 +-
superset/assets/stylesheets/superset.less | 19 +-
superset/assets/visualizations/markup.js | 2 +-
superset/assets/webpack.config.js | 2 +-
superset/templates/superset/dashboard.html | 6 -
47 files changed, 2050 insertions(+), 878 deletions(-)
diff --git a/superset/assets/javascripts/chart/Chart.jsx b/superset/assets/javascripts/chart/Chart.jsx
new file mode 100644
index 0000000..f775e89
--- /dev/null
+++ b/superset/assets/javascripts/chart/Chart.jsx
@@ -0,0 +1,184 @@
+/* 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';
+
+const propTypes = {
+ 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.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() {
+ this.runQuery();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ 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.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) {
+ this.props.removeFilter(col, vals);
+ }
+
+ 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);
+ }
+
+ runQuery() {
+ this.props.actions.runQuery(this.props.formData, true,
+ this.props.timeout,
+ this.props.chartKey,
+ );
+ }
+
+ render_template(s) {
+ const context = {
+ width: this.width(),
+ height: this.height(),
+ };
+ return Mustache.render(s, context);
+ }
+
+ renderViz() {
+ const viz = visMap[this.props.vizType];
+ try {
+ viz(this, this.props.queryResponse, this.props.actions.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;
diff --git a/superset/assets/javascripts/chart/ChartBody.jsx b/superset/assets/javascripts/chart/ChartBody.jsx
new file mode 100644
index 0000000..89352f5
--- /dev/null
+++ b/superset/assets/javascripts/chart/ChartBody.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+
+const propTypes = {
+ containerId: PropTypes.string.isRequired,
+ vizType: PropTypes.string.isRequired,
+ height: PropTypes.func.isRequired,
+ width: PropTypes.func.isRequired,
+};
+
+class ChartBody extends React.PureComponent {
+ html(data) {
+ this.el.innerHTML = data;
+ }
+
+ css(property, value) {
+ this.el.style[property] = value;
+ }
+
+ get(n) {
+ return $(this.el).get(n);
+ }
+
+ find(classname) {
+ return $(this.el).find(classname);
+ }
+
+ show() {
+ return $(this.el).show();
+ }
+
+ height() {
+ return this.props.height();
+ }
+
+ width() {
+ return this.props.width();
+ }
+
+ render() {
+ return (
+ <div
+ id={this.props.containerId}
+ className={`slice_container ${this.props.vizType}`}
+ ref={(el) => { this.el = el; }}
+ />
+ );
+ }
+}
+
+ChartBody.propTypes = propTypes;
+
+export default ChartBody;
diff --git a/superset/assets/javascripts/chart/ChartContainer.jsx b/superset/assets/javascripts/chart/ChartContainer.jsx
new file mode 100644
index 0000000..11c4322
--- /dev/null
+++ b/superset/assets/javascripts/chart/ChartContainer.jsx
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import * as Actions from './chartAction';
+import Chart from './Chart';
+
+function mapStateToProps({ charts }, ownProps) {
+ const chart = charts[ownProps.chartKey];
+ return {
+ chartAlert: chart.chartAlert,
+ chartStatus: chart.chartStatus,
+ chartUpdateEndTime: chart.chartUpdateEndTime,
+ chartUpdateStartTime: chart.chartUpdateStartTime,
+ latestQueryFormData: chart.latestQueryFormData,
+ queryResponse: chart.queryResponse,
+ queryRequest: chart.queryRequest,
+ triggerQuery: chart.triggerQuery,
+ triggerRender: chart.triggerRender,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(Actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Chart);
diff --git a/superset/assets/javascripts/chart/chartAction.js b/superset/assets/javascripts/chart/chartAction.js
new file mode 100644
index 0000000..17205a4
--- /dev/null
+++ b/superset/assets/javascripts/chart/chartAction.js
@@ -0,0 +1,91 @@
+import { getExploreUrl } from '../explore/exploreUtils';
+import { t } from '../locales';
+
+const $ = window.$ = require('jquery');
+
+export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
+export function chartUpdateStarted(queryRequest, key) {
+ return { type: CHART_UPDATE_STARTED, queryRequest, key };
+}
+
+export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
+export function chartUpdateSucceeded(queryResponse, key) {
+ return { type: CHART_UPDATE_SUCCEEDED, queryResponse, key };
+}
+
+export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
+export function chartUpdateStopped(queryRequest, key) {
+ if (queryRequest) {
+ queryRequest.abort();
+ }
+ return { type: CHART_UPDATE_STOPPED, key };
+}
+
+export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
+export function chartUpdateTimeout(statusText, timeout, key) {
+ return { type: CHART_UPDATE_TIMEOUT, statusText, timeout, key };
+}
+
+export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
+export function chartUpdateFailed(queryResponse, key) {
+ return { type: CHART_UPDATE_FAILED, queryResponse, key };
+}
+
+export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
+export function chartRenderingFailed(error, key) {
+ return { type: CHART_RENDERING_FAILED, error, key };
+}
+
+export const REMOVE_CHART = 'REMOVE_CHART';
+export function removeChart(key) {
+ return { type: REMOVE_CHART, key };
+}
+
+export const TRIGGER_QUERY = 'TRIGGER_QUERY';
+export function triggerQuery(value = true, key) {
+ return { type: TRIGGER_QUERY, value, key };
+}
+
+// this action is used for forced re-render without fetch data
+export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
+export function renderTriggered(value, key) {
+ return { type: RENDER_TRIGGERED, value, key };
+}
+
+export const RUN_QUERY = 'RUN_QUERY';
+export function runQuery(formData, force = false, timeout = 60, key) {
+ return (dispatch) => {
+ const url = getExploreUrl(formData, 'json', force);
+ const queryRequest = $.ajax({
+ url,
+ dataType: 'json',
+ timeout: timeout * 1000,
+ success: (queryResponse =>
+ dispatch(chartUpdateSucceeded(queryResponse, key))
+ ),
+ error: ((xhr) => {
+ if (xhr.statusText === 'timeout') {
+ dispatch(chartUpdateTimeout(xhr.statusText, timeout, key));
+ } else {
+ let error = '';
+ if (!xhr.responseText) {
+ const status = xhr.status;
+ if (status === 0) {
+ // This may happen when the worker in gunicorn times out
+ error += (
+ t('The server could not be reached. You may want to ' +
+ 'verify your connection and try again.'));
+ } else {
+ error += (t('An unknown error occurred. (Status: %s )', status));
+ }
+ }
+ const errorResponse = Object.assign({}, xhr.responseJSON, error);
+ dispatch(chartUpdateFailed(errorResponse, key));
+ }
+ }),
+ });
+
+ dispatch(chartUpdateStarted(queryRequest, key));
+ dispatch(triggerQuery(false, key));
+ };
+}
diff --git a/superset/assets/javascripts/chart/chartReducer.js b/superset/assets/javascripts/chart/chartReducer.js
new file mode 100644
index 0000000..2adb904
--- /dev/null
+++ b/superset/assets/javascripts/chart/chartReducer.js
@@ -0,0 +1,100 @@
+/* eslint camelcase: 0 */
+import PropTypes from 'prop-types';
+
+import { now } from '../modules/dates';
+import * as actions from './chartAction';
+import { t } from '../locales';
+
+export const chartPropType = {
+ chartKey: PropTypes.string.isRequired,
+ chartAlert: PropTypes.string,
+ chartStatus: PropTypes.string,
+ chartUpdateEndTime: PropTypes.number,
+ chartUpdateStartTime: PropTypes.number,
+ latestQueryFormData: PropTypes.object,
+ queryResponse: PropTypes.object,
+ triggerQuery: PropTypes.bool,
+ lastRendered: PropTypes.number,
+};
+
+export const chart = {
+ chartKey: '',
+ chartAlert: null,
+ chartStatus: null,
+ chartUpdateEndTime: null,
+ chartUpdateStartTime: now(),
+ latestQueryFormData: null,
+ queryResponse: null,
+ triggerQuery: true,
+ lastRendered: 0,
+};
+
+export default function chartReducer(charts = {}, action) {
+ const actionHandlers = {
+ [actions.CHART_UPDATE_SUCCEEDED](state) {
+ return { ...state,
+ chartStatus: 'success',
+ queryResponse: action.queryResponse,
+ chartUpdateEndTime: now(),
+ };
+ },
+ [actions.CHART_UPDATE_STARTED](state) {
+ return { ...state,
+ chartStatus: 'loading',
+ chartUpdateEndTime: null,
+ chartUpdateStartTime: now(),
+ queryRequest: action.queryRequest,
+ };
+ },
+ [actions.CHART_UPDATE_STOPPED](state) {
+ return { ...state,
+ chartStatus: 'stopped',
+ chartAlert: t('Updating chart was stopped'),
+ };
+ },
+ [actions.CHART_RENDERING_FAILED](state) {
+ return { ...state,
+ chartStatus: 'failed',
+ chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
+ };
+ },
+ [actions.CHART_UPDATE_TIMEOUT](state) {
+ return { ...state,
+ chartStatus: 'failed',
+ chartAlert: (
+ "<strong>{t('Query timeout')}</strong> - " +
+ t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
+ t('Perhaps your data has grown, your database is under unusual load, ' +
+ 'or you are simply querying a data source that is too large ' +
+ 'to be processed within the timeout range. ' +
+ 'If that is the case, we recommend that you summarize your data further.')),
+ };
+ },
+ [actions.CHART_UPDATE_FAILED](state) {
+ return { ...state,
+ chartStatus: 'failed',
+ chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
+ chartUpdateEndTime: now(),
+ queryResponse: action.queryResponse,
+ };
+ },
+ [actions.TRIGGER_QUERY](state) {
+ return { ...state, triggerQuery: action.value };
+ },
+ [actions.RENDER_TRIGGERED](state) {
+ return { ...state, lastRendered: action.value };
+ },
+ };
+
+ /* eslint-disable no-param-reassign */
+ if (action.type === actions.REMOVE_CHART) {
+ delete charts[action.key];
+ return charts;
+ }
+
+ if (action.type in actionHandlers) {
+ return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) };
+ }
+
+ return charts;
+}
diff --git a/superset/assets/javascripts/components/FaveStar.jsx b/superset/assets/javascripts/components/FaveStar.jsx
index e633247..60de9d1 100644
--- a/superset/assets/javascripts/components/FaveStar.jsx
+++ b/superset/assets/javascripts/components/FaveStar.jsx
@@ -5,19 +5,20 @@ import TooltipWrapper from './TooltipWrapper';
import { t } from '../locales';
const propTypes = {
- sliceId: PropTypes.number.isRequired,
- actions: PropTypes.object.isRequired,
+ itemId: PropTypes.number.isRequired,
+ fetchFaveStar: PropTypes.func,
+ saveFaveStar: PropTypes.func,
isStarred: PropTypes.bool.isRequired,
};
export default class FaveStar extends React.Component {
componentDidMount() {
- this.props.actions.fetchFaveStar(this.props.sliceId);
+ this.props.fetchFaveStar(this.props.itemId);
}
onClick(e) {
e.preventDefault();
- this.props.actions.saveFaveStar(this.props.sliceId, this.props.isStarred);
+ this.props.saveFaveStar(this.props.itemId, this.props.isStarred);
}
render() {
diff --git a/superset/assets/javascripts/components/StackTraceMessage.jsx b/superset/assets/javascripts/components/StackTraceMessage.jsx
new file mode 100644
index 0000000..a950c39
--- /dev/null
+++ b/superset/assets/javascripts/components/StackTraceMessage.jsx
@@ -0,0 +1,59 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Alert, Collapse } from 'react-bootstrap';
+
+const propTypes = {
+ message: PropTypes.string,
+ queryResponse: PropTypes.object,
+ showStackTrace: PropTypes.bool,
+};
+const defaultProps = {
+ showStackTrace: false,
+};
+
+class StackTraceMessage extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showStackTrace: props.showStackTrace,
+ };
+ }
+
+ hasTrace() {
+ return this.props.queryResponse && this.props.queryResponse.stacktrace;
+ }
+
+ render() {
+ const msg = (
+ <div>
+ <p
+ dangerouslySetInnerHTML={{ __html: this.props.message }}
+ />
+ </div>);
+
+ return (
+ <div className={`stack-trace-container${this.hasTrace() ? ' has-trace' : ''}`}>
+ <Alert
+ bsStyle="warning"
+ onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
+ >
+ {msg}
+ </Alert>
+ {this.hasTrace() &&
+ <Collapse in={this.state.showStackTrace}>
+ <pre>
+ {this.props.queryResponse.stacktrace}
+ </pre>
+ </Collapse>
+ }
+ </div>
+ );
+ }
+}
+
+StackTraceMessage.propTypes = propTypes;
+StackTraceMessage.defaultProps = defaultProps;
+
+export default StackTraceMessage;
diff --git a/superset/assets/javascripts/dashboard/Dashboard.jsx b/superset/assets/javascripts/dashboard/Dashboard.jsx
index f133424..9e67647 100644
--- a/superset/assets/javascripts/dashboard/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/Dashboard.jsx
@@ -283,7 +283,7 @@ export function dashboardContainer(dashboard, datasources, userid) {
const refreshAll = () => {
const slices = dash.sliceObjects
.filter(slice => immune.indexOf(slice.data.slice_id) === -1);
- dash.renderSlices(slices, true, interval * 0.2);
+ dash.fetchSlices(slices, true, interval * 0.2);
};
const fetchAndRender = function () {
refreshAll();
@@ -375,7 +375,7 @@ $(document).ready(() => {
const state = getInitialState(dashboardData);
px = superset(state);
- const dashboard = dashboardContainer(state.dashboard, state.datasources, state.user_id);
+ const dashboard = dashboardContainer(state.dashboard, state.datasources, state.userId);
initDashboardView(dashboard);
dashboard.init();
});
diff --git a/superset/assets/javascripts/dashboard/actions.js b/superset/assets/javascripts/dashboard/actions.js
new file mode 100644
index 0000000..6e88ca6
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/actions.js
@@ -0,0 +1,112 @@
+/* global notify */
+import $ from 'jquery';
+import { getExploreUrl } from '../explore/exploreUtils';
+
+export const ADD_FILTER = 'ADD_FILTER';
+export function addFilter(sliceId, col, vals, merge = true, refresh = true) {
+ return { type: ADD_FILTER, sliceId, col, vals, merge, refresh };
+}
+
+export const CLEAR_FILTER = 'CLEAR_FILTER';
+export function clearFilter(sliceId) {
+ return { type: CLEAR_FILTER, sliceId };
+}
+
+export const REMOVE_FILTER = 'REMOVE_FILTER';
+export function removeFilter(sliceId, col, vals) {
+ return { type: REMOVE_FILTER, sliceId, col, vals };
+}
+
+export const UPDATE_DASHBOARD_LAYOUT = 'UPDATE_DASHBOARD_LAYOUT';
+export function updateDashboardLayout(layout) {
+ return { type: UPDATE_DASHBOARD_LAYOUT, layout };
+}
+
+export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
+export function updateDashboardTitle(title) {
+ return { type: UPDATE_DASHBOARD_TITLE, title };
+}
+
+export function addSlicesToDashboard(dashboardId, sliceIds) {
+ return () => (
+ $.ajax({
+ type: 'POST',
+ url: `/superset/add_slices/${dashboardId}/`,
+ data: {
+ data: JSON.stringify({ slice_ids: sliceIds }),
+ },
+ })
+ .done(() => {
+ // Refresh page to allow for slices to re-render
+ window.location.reload();
+ })
+ );
+}
+
+export const REMOVE_SLICE = 'REMOVE_SLICE';
+export function removeSlice(slice) {
+ return { type: REMOVE_SLICE, slice };
+}
+
+export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
+export function updateSliceName(slice, sliceName) {
+ return { type: UPDATE_SLICE_NAME, slice, sliceName };
+}
+export function saveSlice(slice, sliceName) {
+ const oldName = slice.slice_name;
+ return (dispatch) => {
+ const sliceParams = {};
+ sliceParams.slice_id = slice.slice_id;
+ sliceParams.action = 'overwrite';
+ sliceParams.slice_name = sliceName;
+ const saveUrl = getExploreUrl(slice.form_data, 'base', false, null, sliceParams);
+ return $.ajax({
+ url: saveUrl,
+ type: 'GET',
+ success: () => {
+ dispatch(updateSliceName(slice, sliceName));
+ notify.success('This slice name was saved successfully.');
+ },
+ error: () => {
+ // if server-side reject the overwrite action,
+ // revert to old state
+ dispatch(updateSliceName(slice, oldName));
+ notify.error("You don't have the rights to alter this slice");
+ },
+ });
+ };
+}
+
+const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
+export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
+export function toggleFaveStar(isStarred) {
+ return { type: TOGGLE_FAVE_STAR, isStarred };
+}
+
+export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
+export function fetchFaveStar(id) {
+ return function (dispatch) {
+ const url = `${FAVESTAR_BASE_URL}/${id}/count`;
+ return $.get(url)
+ .done((data) => {
+ if (data.count > 0) {
+ dispatch(toggleFaveStar(true));
+ }
+ });
+ };
+}
+
+export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
+export function saveFaveStar(id, isStarred) {
+ return function (dispatch) {
+ const urlSuffix = isStarred ? 'unselect' : 'select';
+ const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
+ $.get(url);
+ dispatch(toggleFaveStar(!isStarred));
+ };
+}
+
+export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
+export function toggleExpandSlice(slice, isExpanded) {
+ return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded };
+}
diff --git a/superset/assets/javascripts/dashboard/components/Controls.jsx b/superset/assets/javascripts/dashboard/components/Controls.jsx
index 5d24055..ecbc907 100644
--- a/superset/assets/javascripts/dashboard/components/Controls.jsx
+++ b/superset/assets/javascripts/dashboard/components/Controls.jsx
@@ -14,6 +14,15 @@ const $ = window.$ = require('jquery');
const propTypes = {
dashboard: PropTypes.object.isRequired,
+ slices: PropTypes.array,
+ userId: PropTypes.string.isRequired,
+ addSlicesToDashboard: PropTypes.func,
+ onSave: PropTypes.func,
+ onChange: PropTypes.func,
+ readFilters: PropTypes.func,
+ renderSlices: PropTypes.func,
+ serialize: PropTypes.func,
+ startPeriodicRender: PropTypes.func,
};
class Controls extends React.PureComponent {
@@ -36,14 +45,16 @@ class Controls extends React.PureComponent {
}
refresh() {
// Force refresh all slices
- this.props.dashboard.renderSlices(this.props.dashboard.sliceObjects, true);
+ this.props.renderSlices(true);
}
changeCss(css) {
this.setState({ css });
- this.props.dashboard.onChange();
+ this.props.onChange();
}
render() {
- const dashboard = this.props.dashboard;
+ const { dashboard, userId,
+ addSlicesToDashboard, startPeriodicRender, readFilters,
+ serialize, onSave } = this.props;
const emailBody = t('Checkout this dashboard: %s', window.location.href);
const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
+ `${dashboard.dashboard_title}&Body=${emailBody}`;
@@ -57,18 +68,20 @@ class Controls extends React.PureComponent {
</Button>
<SliceAdder
dashboard={dashboard}
+ addSlicesToDashboard={addSlicesToDashboard}
+ userId={userId}
triggerNode={
<i className="fa fa-plus" />
}
/>
<RefreshIntervalModal
- onChange={refreshInterval => dashboard.startPeriodicRender(refreshInterval * 1000)}
+ onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
triggerNode={
<i className="fa fa-clock-o" />
}
/>
<CodeModal
- codeCallback={dashboard.readFilters.bind(dashboard)}
+ codeCallback={readFilters}
triggerNode={<i className="fa fa-filter" />}
/>
<CssEditor
@@ -96,6 +109,9 @@ class Controls extends React.PureComponent {
</Button>
<SaveModal
dashboard={dashboard}
+ readFilters={readFilters}
+ serialize={serialize}
+ onSave={onSave}
css={this.state.css}
triggerNode={
<Button disabled={!dashboard.dash_save_perm}>
diff --git a/superset/assets/javascripts/dashboard/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
new file mode 100644
index 0000000..2415e36
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
@@ -0,0 +1,349 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AlertsWrapper from '../../components/AlertsWrapper';
+import GridLayout from './GridLayout';
+import Header from './Header';
+import DashboardAlert from './DashboardAlert';
+import { getExploreUrl } from '../../explore/exploreUtils';
+import { areObjectsEqual } from '../../reduxUtils';
+import { t } from '../../locales';
+
+import '../../../stylesheets/dashboard.css';
+
+const propTypes = {
+ actions: PropTypes.object,
+ initMessages: PropTypes.array,
+ dashboard: PropTypes.object.isRequired,
+ slices: PropTypes.object,
+ datasources: PropTypes.object,
+ filters: PropTypes.object,
+ refresh: PropTypes.bool,
+ timeout: PropTypes.number,
+ userId: PropTypes.string,
+ isStarred: PropTypes.bool,
+};
+
+const defaultProps = {
+ initMessages: [],
+ dashboard: {},
+ slices: {},
+ datasources: {},
+ filters: {},
+ timeout: 60,
+ userId: '',
+ isStarred: false,
+};
+
+class Dashboard extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.refreshTimer = null;
+ this.firstLoad = true;
+
+ // alert for unsaved changes
+ this.state = {
+ alert: null,
+ trigger: false,
+ };
+
+ this.rerenderCharts = this.rerenderCharts.bind(this);
+ this.updateDashboardTitle = this.updateDashboardTitle.bind(this);
+ this.onSave = this.onSave.bind(this);
+ this.onChange = this.onChange.bind(this);
+ this.serialize = this.serialize.bind(this);
+ this.readFilters = this.readFilters.bind(this);
+ this.fetchAllSlices = this.fetchSlices.bind(this, this.getAllSlices());
+ this.startPeriodicRender = this.startPeriodicRender.bind(this);
+ this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this);
+ this.fetchSlice = this.fetchSlice.bind(this);
+ this.getFormDataExtra = this.getFormDataExtra.bind(this);
+ this.props.actions.fetchFaveStar = this.props.actions.fetchFaveStar.bind(this);
+ this.props.actions.saveFaveStar = this.props.actions.saveFaveStar.bind(this);
+ this.props.actions.saveSlice = this.props.actions.saveSlice.bind(this);
+ this.props.actions.removeSlice = this.props.actions.removeSlice.bind(this);
+ this.props.actions.removeChart = this.props.actions.removeChart.bind(this);
+ this.props.actions.updateDashboardLayout = this.props.actions.updateDashboardLayout.bind(this);
+ this.props.actions.toggleExpandSlice = this.props.actions.toggleExpandSlice.bind(this);
+ this.props.actions.addFilter = this.props.actions.addFilter.bind(this);
+ this.props.actions.clearFilter = this.props.actions.clearFilter.bind(this);
+ this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
+ }
+
+ componentDidMount() {
+ this.loadPreSelectFilters();
+ this.firstLoad = false;
+ window.addEventListener('resize', this.rerenderCharts);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ // check filters is changed
+ if (!areObjectsEqual(nextProps.filters, this.props.filters)) {
+ this.renderUnsavedChangeAlert();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (!areObjectsEqual(prevProps.filters, this.props.filters) && this.props.refresh) {
+ Object.keys(this.props.filters).forEach(sliceId => (this.refreshExcept(sliceId)));
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.rerenderCharts);
+ }
+
+ onBeforeUnload(hasChanged) {
+ if (hasChanged) {
+ window.addEventListener('beforeunload', this.unload);
+ } else {
+ window.removeEventListener('beforeunload', this.unload);
+ }
+ }
+
+ onChange() {
+ this.onBeforeUnload(true);
+ this.renderUnsavedChangeAlert();
+ }
+
+ onSave() {
+ this.onBeforeUnload(false);
+ this.setState({
+ alert: '',
+ });
+ }
+
+ // return charts in array
+ getAllSlices() {
+ return Object.values(this.props.slices);
+ }
+
+ getFormDataExtra(slice) {
+ const formDataExtra = Object.assign({}, slice.formData);
+ const extraFilters = this.effectiveExtraFilters(slice.slice_id);
+ formDataExtra.filters = formDataExtra.filters.concat(extraFilters);
+ return formDataExtra;
+ }
+
+ getFilters(sliceId) {
+ return this.props.filters[sliceId];
+ }
+
+ unload() {
+ const message = t('You have unsaved changes.');
+ window.event.returnValue = message; // Gecko + IE
+ return message; // Gecko + Webkit, Safari, Chrome etc.
+ }
+
+ effectiveExtraFilters(sliceId) {
+ const metadata = this.props.dashboard.metadata;
+ const filters = this.props.filters;
+ const f = [];
+ const immuneSlices = metadata.filter_immune_slices || [];
+ if (sliceId && immuneSlices.includes(sliceId)) {
+ // The slice is immune to dashboard filters
+ return f;
+ }
+
+ // Building a list of fields the slice is immune to filters on
+ let immuneToFields = [];
+ if (
+ sliceId &&
+ metadata.filter_immune_slice_fields &&
+ metadata.filter_immune_slice_fields[sliceId]) {
+ immuneToFields = metadata.filter_immune_slice_fields[sliceId];
+ }
+ for (const filteringSliceId in filters) {
+ if (filteringSliceId === sliceId.toString()) {
+ // Filters applied by the slice don't apply to itself
+ continue;
+ }
+ for (const field in filters[filteringSliceId]) {
+ if (!immuneToFields.includes(field)) {
+ f.push({
+ col: field,
+ op: 'in',
+ val: filters[filteringSliceId][field],
+ });
+ }
+ }
+ }
+ return f;
+ }
+
+ jsonEndpoint(data, force = false) {
+ let endpoint = getExploreUrl(data, 'json', force);
+ if (endpoint.charAt(0) !== '/') {
+ // Known issue for IE <= 11:
+ // https://connect.microsoft.com/IE/feedbackdetail/view/1002846/pathname-incorrect-for-out-of-document-elements
+ endpoint = '/' + endpoint;
+ }
+ return endpoint;
+ }
+
+ loadPreSelectFilters() {
+ for (const key in this.props.filters) {
+ for (const col in this.props.filters[key]) {
+ const sliceId = parseInt(key, 10);
+ this.props.actions.addFilter(sliceId, col,
+ this.props.filters[key][col], false, false,
+ );
+ }
+ }
+ }
+
+ refreshExcept(sliceId) {
+ const immune = this.props.dashboard.metadata.filter_immune_slices || [];
+ const slices = this.getAllSlices()
+ .filter(slice => slice.slice_id !== sliceId && immune.indexOf(slice.slice_id) === -1);
+ this.fetchSlices(slices);
+ }
+
+ stopPeriodicRender() {
+ if (this.refreshTimer) {
+ clearTimeout(this.refreshTimer);
+ this.refreshTimer = null;
+ }
+ }
+
+ startPeriodicRender(interval) {
+ this.stopPeriodicRender();
+ const immune = this.props.dashboard.metadata.timed_refresh_immune_slices || [];
+ const refreshAll = () => {
+ const affectedSlices = this.getAllSlices()
+ .filter(slice => immune.indexOf(slice.slice_id) === -1);
+ this.fetchSlices(affectedSlices, true, interval * 0.2);
+ };
+ const fetchAndRender = () => {
+ refreshAll();
+ if (interval > 0) {
+ this.refreshTimer = setTimeout(fetchAndRender, interval);
+ }
+ };
+
+ fetchAndRender();
+ }
+
+ readFilters() {
+ // Returns a list of human readable active filters
+ return JSON.stringify(this.props.filters, null, ' ');
+ }
+
+ updateDashboardTitle(title) {
+ this.props.actions.updateDashboardTitle(title);
+ this.onChange();
+ }
+
+ serialize() {
+ return this.props.dashboard.layout.map(reactPos => ({
+ slice_id: reactPos.i,
+ col: reactPos.x + 1,
+ row: reactPos.y,
+ size_x: reactPos.w,
+ size_y: reactPos.h,
+ }));
+ }
+
+ addSlicesToDashboard(sliceIds) {
+ return this.props.actions.addSlicesToDashboard(this.props.dashboard.id, sliceIds);
+ }
+
+ fetchSlice(slice, force = false) {
+ return this.props.actions.runQuery(
+ this.getFormDataExtra(slice), force, this.props.timeout, slice.chartKey,
+ );
+ }
+
+ // fetch and render an list of slices
+ fetchSlices(slc, force = false, interval = 0) {
+ const slices = slc || this.getAllSlices();
+ if (!interval) {
+ slices.forEach((slice) => { this.fetchSlice(slice, force); });
+ return;
+ }
+
+ const meta = this.props.dashboard.metadata;
+ const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
+ if (typeof meta.stagger_refresh !== 'boolean') {
+ meta.stagger_refresh = meta.stagger_refresh === undefined ?
+ true : meta.stagger_refresh === 'true';
+ }
+ const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0;
+ slices.forEach((slice, i) => {
+ setTimeout(() => { this.fetchSlice(slice, force); }, delay * i);
+ });
+ }
+
+ // re-render chart without fetch
+ rerenderCharts() {
+ this.getAllSlices().forEach((slice) => {
+ setTimeout(() => {
+ this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
+ }, 50);
+ });
+ }
+
+ renderUnsavedChangeAlert() {
+ this.setState({
+ alert: (
+ <span>
+ <strong>{t('You have unsaved changes.')}</strong> {t('Click the')}
+ <i className="fa fa-save" />
+ {t('button on the top right to save your changes.')}
+ </span>
+ ),
+ });
+ }
+
+ render() {
+ return (
+ <div id="dashboard-container">
+ {this.state.alert && <DashboardAlert alertContent={this.state.alert} />}
+ <div id="dashboard-header">
+ <AlertsWrapper initMessages={this.props.initMessages} />
+ <Header
+ dashboard={this.props.dashboard}
+ userId={this.props.userId}
+ isStarred={this.props.isStarred}
+ updateDashboardTitle={this.updateDashboardTitle}
+ onSave={this.onSave}
+ onChange={this.onChange}
+ serialize={this.serialize}
+ readFilters={this.readFilters}
+ fetchFaveStar={this.props.actions.fetchFaveStar}
+ saveFaveStar={this.props.actions.saveFaveStar}
+ renderSlices={this.fetchAllSlices}
+ startPeriodicRender={this.startPeriodicRender}
+ addSlicesToDashboard={this.addSlicesToDashboard}
+ />
+ </div>
+ <div id="grid-container" className="slice-grid gridster">
+ <GridLayout
+ dashboard={this.props.dashboard}
+ datasources={this.props.datasources}
+ filters={this.props.filters}
+ charts={this.props.slices}
+ timeout={this.props.timeout}
+ onChange={this.onChange}
+ getFormDataExtra={this.getFormDataExtra}
+ fetchSlice={this.fetchSlice}
+ saveSlice={this.props.actions.saveSlice}
+ removeSlice={this.props.actions.removeSlice}
+ removeChart={this.props.actions.removeChart}
+ updateDashboardLayout={this.props.actions.updateDashboardLayout}
+ toggleExpandSlice={this.props.actions.toggleExpandSlice}
+ addFilter={this.props.actions.addFilter}
+ getFilters={this.getFilters}
+ clearFilter={this.props.actions.clearFilter}
+ removeFilter={this.props.actions.removeFilter}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+Dashboard.propTypes = propTypes;
+Dashboard.defaultProps = defaultProps;
+
+export default Dashboard;
diff --git a/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx b/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx
new file mode 100644
index 0000000..4579ce8
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Alert } from 'react-bootstrap';
+
+const propTypes = {
+ alertContent: PropTypes.node.isRequired,
+};
+
+const DashboardAlert = ({ alertContent }) => (
+ <div id="alert-container">
+ <div className="container-fluid">
+ <Alert bsStyle="warning">
+ {alertContent}
+ </Alert>
+ </div>
+ </div>
+);
+
+DashboardAlert.propTypes = propTypes;
+
+export default DashboardAlert;
diff --git a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx
new file mode 100644
index 0000000..24127aa
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx
@@ -0,0 +1,29 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import * as dashboardActions from '../actions';
+import * as chartActions from '../../chart/chartAction';
+import Dashboard from './Dashboard';
+
+function mapStateToProps({ charts, dashboard }) {
+ return {
+ initMessages: dashboard.common.flash_messages,
+ timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+ dashboard: dashboard.dashboard,
+ slices: charts,
+ datasources: dashboard.datasources,
+ filters: dashboard.filters,
+ refresh: dashboard.refresh,
+ userId: dashboard.userId,
+ isStarred: !!dashboard.isStarred,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ const actions = { ...chartActions, ...dashboardActions };
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/javascripts/dashboard/components/GridCell.jsx b/superset/assets/javascripts/dashboard/components/GridCell.jsx
new file mode 100644
index 0000000..1a59a92
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/GridCell.jsx
@@ -0,0 +1,132 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import SliceHeader from './SliceHeader';
+import ChartContainer from '../../chart/ChartContainer';
+
+import '../../../stylesheets/dashboard.css';
+
+const propTypes = {
+ timeout: PropTypes.number,
+ datasource: PropTypes.object,
+ isLoading: PropTypes.bool,
+ isExpanded: PropTypes.bool,
+ widgetHeight: PropTypes.number,
+ widgetWidth: PropTypes.number,
+ exploreChartUrl: PropTypes.string,
+ exportCSVUrl: PropTypes.string,
+ slice: PropTypes.object,
+ chartKey: PropTypes.string,
+ formData: PropTypes.object,
+ filters: PropTypes.object,
+ forceRefresh: PropTypes.func,
+ removeSlice: PropTypes.func,
+ updateSliceName: PropTypes.func,
+ toggleExpandSlice: PropTypes.func,
+ addFilter: PropTypes.func,
+ getFilters: PropTypes.func,
+ clearFilter: PropTypes.func,
+ removeFilter: PropTypes.func,
+};
+
+const defaultProps = {
+ forceRefresh: () => ({}),
+ removeSlice: () => ({}),
+ updateSliceName: () => ({}),
+ toggleExpandSlice: () => ({}),
+ addFilter: () => ({}),
+ getFilters: () => ({}),
+ clearFilter: () => ({}),
+ removeFilter: () => ({}),
+};
+
+class GridCell extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ const sliceId = this.props.slice.slice_id;
+ this.addFilter = this.props.addFilter.bind(this, sliceId);
+ this.getFilters = this.props.getFilters.bind(this, sliceId);
+ this.clearFilter = this.props.clearFilter.bind(this, sliceId);
+ this.removeFilter = this.props.removeFilter.bind(this, sliceId);
+ }
+
+ getDescriptionId(slice) {
+ return 'description_' + slice.slice_id;
+ }
+
+ getHeaderId(slice) {
+ return 'header_' + slice.slice_id;
+ }
+
+ width() {
+ return this.props.widgetWidth - 10;
+ }
+
+ height(slice) {
+ const widgetHeight = this.props.widgetHeight;
+ const headerId = this.getHeaderId(slice);
+ const descriptionId = this.getDescriptionId(slice);
+ const headerHeight = this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
+ let descriptionHeight = 0;
+ if (this.props.isExpanded && this.refs[descriptionId]) {
+ descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
+ }
+ return widgetHeight - headerHeight - descriptionHeight;
+ }
+
+ render() {
+ const {
+ exploreChartUrl, exportCSVUrl, isExpanded, isLoading, removeSlice, updateSliceName,
+ toggleExpandSlice, forceRefresh, chartKey, slice, datasource, formData, timeout,
+ } = this.props;
+ return (
+ <div
+ className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
+ id={`${slice.slice_id}-cell`}
+ >
+ <div ref={this.getHeaderId(slice)}>
+ <SliceHeader
+ slice={slice}
+ exploreChartUrl={exploreChartUrl}
+ exportCSVUrl={exportCSVUrl}
+ isExpanded={isExpanded}
+ removeSlice={removeSlice}
+ updateSliceName={updateSliceName}
+ toggleExpandSlice={toggleExpandSlice}
+ forceRefresh={forceRefresh}
+ />
+ </div>
+ <div
+ className="slice_description bs-callout bs-callout-default"
+ style={isExpanded ? {} : { display: 'none' }}
+ ref={this.getDescriptionId(slice)}
+ dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
+ />
+ <div className="row chart-container">
+ <input type="hidden" value="false" />
+ <ChartContainer
+ containerId={`slice-container-${slice.slice_id}`}
+ chartKey={chartKey}
+ datasource={datasource}
+ formData={formData}
+ height={this.height(slice)}
+ width={this.width()}
+ timeout={timeout}
+ vizType={slice.formData.viz_type}
+ addFilter={this.addFilter}
+ getFilters={this.getFilters}
+ clearFilter={this.clearFilter}
+ removeFilter={this.removeFilter}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+GridCell.propTypes = propTypes;
+GridCell.defaultProps = defaultProps;
+
+export default GridCell;
diff --git a/superset/assets/javascripts/dashboard/components/GridLayout.jsx b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
index dc99503..22d4b59 100644
--- a/superset/assets/javascripts/dashboard/components/GridLayout.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
@@ -1,10 +1,8 @@
-/* global notify */
import React from 'react';
import PropTypes from 'prop-types';
import { Responsive, WidthProvider } from 'react-grid-layout';
-import $ from 'jquery';
-import SliceCell from './SliceCell';
+import GridCell from './GridCell';
import { getExploreUrl } from '../../explore/exploreUtils';
require('react-grid-layout/css/styles.css');
@@ -14,119 +12,127 @@ const ResponsiveReactGridLayout = WidthProvider(Responsive);
const propTypes = {
dashboard: PropTypes.object.isRequired,
+ datasources: PropTypes.object,
+ charts: PropTypes.object.isRequired,
+ filters: PropTypes.object,
+ timeout: PropTypes.number,
+ onChange: PropTypes.func,
+ getFormDataExtra: PropTypes.func,
+ fetchSlice: PropTypes.func,
+ saveSlice: PropTypes.func,
+ removeSlice: PropTypes.func,
+ removeChart: PropTypes.func,
+ updateDashboardLayout: PropTypes.func,
+ toggleExpandSlice: PropTypes.func,
+ addFilter: PropTypes.func,
+ getFilters: PropTypes.func,
+ clearFilter: PropTypes.func,
+ removeFilter: PropTypes.func,
+};
+
+const defaultProps = {
+ onChange: () => ({}),
+ getFormDataExtra: () => ({}),
+ fetchSlice: () => ({}),
+ saveSlice: () => ({}),
+ removeSlice: () => ({}),
+ removeChart: () => ({}),
+ updateDashboardLayout: () => ({}),
+ toggleExpandSlice: () => ({}),
+ addFilter: () => ({}),
+ getFilters: () => ({}),
+ clearFilter: () => ({}),
+ removeFilter: () => ({}),
};
class GridLayout extends React.Component {
- componentWillMount() {
- const layout = [];
-
- this.props.dashboard.slices.forEach((slice, index) => {
- const sliceId = slice.slice_id;
- let pos = this.props.dashboard.posDict[sliceId];
- if (!pos) {
- pos = {
- col: (index * 4 + 1) % 12,
- row: Math.floor((index) / 3) * 4,
- size_x: 4,
- size_y: 4,
- };
- }
-
- layout.push({
- i: String(sliceId),
- x: pos.col - 1,
- y: pos.row,
- w: pos.size_x,
- minW: 2,
- h: pos.size_y,
- });
- });
-
- this.setState({
- layout,
- slices: this.props.dashboard.slices,
- });
+ constructor(props) {
+ super(props);
+
+ this.onResizeStop = this.onResizeStop.bind(this);
+ this.onDragStop = this.onDragStop.bind(this);
+ this.forceRefresh = this.forceRefresh.bind(this);
+ this.removeSlice = this.removeSlice.bind(this);
+ this.updateSliceName = this.props.dashboard.dash_edit_perm ?
+ this.updateSliceName.bind(this) : null;
}
- onResizeStop(layout, oldItem, newItem) {
- const newSlice = this.props.dashboard.getSlice(newItem.i);
- if (oldItem.w !== newItem.w || oldItem.h !== newItem.h) {
- this.setState({ layout }, () => newSlice.resize());
- }
- this.props.dashboard.onChange();
+ onResizeStop(layout) {
+ this.props.updateDashboardLayout(layout);
+ this.props.onChange();
}
onDragStop(layout) {
- this.setState({ layout });
- this.props.dashboard.onChange();
+ this.props.updateDashboardLayout(layout);
+ this.props.onChange();
+ }
+
+ getWidgetId(slice) {
+ return 'widget_' + slice.slice_id;
+ }
+
+ getWidgetHeight(slice) {
+ const widgetId = this.getWidgetId(slice);
+ if (!widgetId || !this.refs[widgetId]) {
+ return 400;
+ }
+ return this.refs[widgetId].offsetHeight;
+ }
+
+ getWidgetWidth(slice) {
+ const widgetId = this.getWidgetId(slice);
+ if (!widgetId || !this.refs[widgetId]) {
+ return 400;
+ }
+ return this.refs[widgetId].offsetWidth;
}
- removeSlice(sliceId) {
- $('[data-toggle=tooltip]').tooltip('hide');
- this.setState({
- layout: this.state.layout.filter(function (reactPos) {
- return reactPos.i !== String(sliceId);
- }),
- slices: this.state.slices.filter(function (slice) {
- return slice.slice_id !== sliceId;
- }),
- });
- this.props.dashboard.onChange();
+ findSliceIndexById(sliceId) {
+ return this.props.dashboard.slices
+ .map(slice => (slice.slice_id)).indexOf(sliceId);
+ }
+
+ forceRefresh(sliceId) {
+ return this.props.fetchSlice(this.props.charts['slice_' + sliceId], true);
+ }
+
+ removeSlice(slice) {
+ if (!slice) {
+ return;
+ }
+
+ // remove slice dashbaord and charts
+ this.props.removeSlice(slice);
+ this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey);
+ this.props.onChange();
}
updateSliceName(sliceId, sliceName) {
- const index = this.state.slices.map(slice => (slice.slice_id)).indexOf(sliceId);
+ const index = this.findSliceIndexById(sliceId);
if (index === -1) {
return;
}
- // update slice_name first
- const oldSlices = this.state.slices;
- const currentSlice = this.state.slices[index];
- const updated = Object.assign({},
- this.state.slices[index], { slice_name: sliceName });
- const updatedSlices = this.state.slices.slice();
- updatedSlices[index] = updated;
- this.setState({ slices: updatedSlices });
-
- const sliceParams = {};
- sliceParams.slice_id = currentSlice.slice_id;
- sliceParams.action = 'overwrite';
- sliceParams.slice_name = sliceName;
- const saveUrl = getExploreUrl(currentSlice.form_data, 'base', false, null, sliceParams);
-
- $.ajax({
- url: saveUrl,
- type: 'GET',
- success: () => {
- notify.success('This slice name was saved successfully.');
- },
- error: () => {
- // if server-side reject the overwrite action,
- // revert to old state
- this.setState({ slices: oldSlices });
- notify.error('You don\'t have the rights to alter this slice');
- },
- });
+ const currentSlice = this.props.dashboard.slices[index];
+ if (currentSlice.slice_name === sliceName) {
+ return;
+ }
+
+ this.props.saveSlice(currentSlice, sliceName);
}
- serialize() {
- return this.state.layout.map(reactPos => ({
- slice_id: reactPos.i,
- col: reactPos.x + 1,
- row: reactPos.y,
- size_x: reactPos.w,
- size_y: reactPos.h,
- }));
+ isExpanded(slice) {
+ return this.props.dashboard.metadata.expanded_slices &&
+ this.props.dashboard.metadata.expanded_slices[slice.slice_id];
}
render() {
return (
<ResponsiveReactGridLayout
className="layout"
- layouts={{ lg: this.state.layout }}
- onResizeStop={this.onResizeStop.bind(this)}
- onDragStop={this.onDragStop.bind(this)}
+ layouts={{ lg: this.props.dashboard.layout }}
+ onResizeStop={this.onResizeStop}
+ onDragStop={this.onDragStop}
cols={{ lg: 12, md: 12, sm: 10, xs: 8, xxs: 6 }}
rowHeight={100}
autoSize
@@ -134,19 +140,36 @@ class GridLayout extends React.Component {
useCSSTransforms
draggableHandle=".drag"
>
- {this.state.slices.map(slice => (
+ {this.props.dashboard.slices.map(slice => (
<div
id={'slice_' + slice.slice_id}
key={slice.slice_id}
data-slice-id={slice.slice_id}
className={`widget ${slice.form_data.viz_type}`}
+ ref={this.getWidgetId(slice)}
>
- <SliceCell
+ <GridCell
slice={slice}
- removeSlice={this.removeSlice.bind(this, slice.slice_id)}
- expandedSlices={this.props.dashboard.metadata.expanded_slices}
- updateSliceName={this.props.dashboard.dash_edit_perm ?
- this.updateSliceName.bind(this) : null}
+ chartKey={'slice_' + slice.slice_id}
+ datasource={this.props.datasources[slice.form_data.datasource]}
+ filters={this.props.filters}
+ formData={this.props.getFormDataExtra(slice)}
+ timeout={this.props.timeout}
+ widgetHeight={this.getWidgetHeight(slice)}
+ widgetWidth={this.getWidgetWidth(slice)}
+ exploreChartUrl={getExploreUrl(this.props.getFormDataExtra(slice))}
+ exportCSVUrl={getExploreUrl(this.props.getFormDataExtra(slice), 'csv')}
+ isExpanded={!!this.isExpanded(slice)}
+ isLoading={[undefined, 'loading']
+ .indexOf(this.props.charts['slice_' + slice.slice_id].chartStatus) !== -1}
+ toggleExpandSlice={this.props.toggleExpandSlice}
+ forceRefresh={this.forceRefresh}
+ removeSlice={this.removeSlice}
+ updateSliceName={this.updateSliceName}
+ addFilter={this.props.addFilter}
+ getFilters={this.props.getFilters}
+ clearFilter={this.props.clearFilter}
+ removeFilter={this.props.removeFilter}
/>
</div>
))}
@@ -156,5 +179,6 @@ class GridLayout extends React.Component {
}
GridLayout.propTypes = propTypes;
+GridLayout.defaultProps = defaultProps;
export default GridLayout;
diff --git a/superset/assets/javascripts/dashboard/components/Header.jsx b/superset/assets/javascripts/dashboard/components/Header.jsx
index a1ab0e8..dfba7e8 100644
--- a/superset/assets/javascripts/dashboard/components/Header.jsx
+++ b/superset/assets/javascripts/dashboard/components/Header.jsx
@@ -3,22 +3,32 @@ import PropTypes from 'prop-types';
import Controls from './Controls';
import EditableTitle from '../../components/EditableTitle';
+import FaveStar from '../../components/FaveStar';
const propTypes = {
- dashboard: PropTypes.object,
-};
-const defaultProps = {
+ dashboard: PropTypes.object.isRequired,
+ userId: PropTypes.string.isRequired,
+ isStarred: PropTypes.bool,
+ addSlicesToDashboard: PropTypes.func,
+ onSave: PropTypes.func,
+ onChange: PropTypes.func,
+ fetchFaveStar: PropTypes.func,
+ readFilters: PropTypes.func,
+ renderSlices: PropTypes.func,
+ saveFaveStar: PropTypes.func,
+ serialize: PropTypes.func,
+ startPeriodicRender: PropTypes.func,
+ updateDashboardTitle: PropTypes.func,
};
class Header extends React.PureComponent {
constructor(props) {
super(props);
- this.state = {
- };
+
this.handleSaveTitle = this.handleSaveTitle.bind(this);
}
handleSaveTitle(title) {
- this.props.dashboard.updateDashboardTitle(title);
+ this.props.updateDashboardTitle(title);
}
render() {
const dashboard = this.props.dashboard;
@@ -32,12 +42,29 @@ class Header extends React.PureComponent {
onSaveTitle={this.handleSaveTitle}
noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
/>
- <span is class="favstar" class_name="Dashboard" obj_id={dashboard.id} />
+ <span className="favstar">
+ <FaveStar
+ itemId={dashboard.id}
+ fetchFaveStar={this.props.fetchFaveStar}
+ saveFaveStar={this.props.saveFaveStar}
+ isStarred={this.props.isStarred}
+ />
+ </span>
</h1>
</div>
<div className="pull-right" style={{ marginTop: '35px' }}>
{!this.props.dashboard.standalone_mode &&
- <Controls dashboard={dashboard} />
+ <Controls
+ dashboard={dashboard}
+ userId={this.props.userId}
+ addSlicesToDashboard={this.props.addSlicesToDashboard}
+ onSave={this.props.onSave}
+ onChange={this.props.onChange}
+ readFilters={this.props.readFilters}
+ renderSlices={this.props.renderSlices}
+ serialize={this.props.serialize}
+ startPeriodicRender={this.props.startPeriodicRender}
+ />
}
</div>
<div className="clearfix" />
@@ -46,6 +73,5 @@ class Header extends React.PureComponent {
}
}
Header.propTypes = propTypes;
-Header.defaultProps = defaultProps;
export default Header;
diff --git a/superset/assets/javascripts/dashboard/components/SaveModal.jsx b/superset/assets/javascripts/dashboard/components/SaveModal.jsx
index f35eb63..cc91dae 100644
--- a/superset/assets/javascripts/dashboard/components/SaveModal.jsx
+++ b/superset/assets/javascripts/dashboard/components/SaveModal.jsx
@@ -13,6 +13,9 @@ const propTypes = {
css: PropTypes.string,
dashboard: PropTypes.object.isRequired,
triggerNode: PropTypes.node.isRequired,
+ readFilters: PropTypes.func,
+ serialize: PropTypes.func,
+ onSave: PropTypes.func,
};
class SaveModal extends React.PureComponent {
@@ -45,8 +48,8 @@ class SaveModal extends React.PureComponent {
});
}
saveDashboardRequest(data, url, saveType) {
- const dashboard = this.props.dashboard;
const saveModal = this.modal;
+ const onSaveDashboard = this.props.onSave;
Object.assign(data, { css: this.props.css });
$.ajax({
type: 'POST',
@@ -56,7 +59,7 @@ class SaveModal extends React.PureComponent {
},
success(resp) {
saveModal.close();
- dashboard.onSave();
+ onSaveDashboard();
if (saveType === 'newDashboard') {
window.location = `/superset/dashboard/${resp.id}/`;
} else {
@@ -72,21 +75,13 @@ class SaveModal extends React.PureComponent {
}
saveDashboard(saveType, newDashboardTitle) {
const dashboard = this.props.dashboard;
- const expandedSlices = {};
- $.each($('.slice_info'), function () {
- const widget = $(this).parents('.widget');
- const sliceDescription = widget.find('.slice_description');
- if (sliceDescription.is(':visible')) {
- expandedSlices[$(widget).attr('data-slice-id')] = true;
- }
- });
- const positions = dashboard.reactGridLayout.serialize();
+ const positions = this.props.serialize();
const data = {
positions,
css: this.state.css,
- expanded_slices: expandedSlices,
+ expanded_slices: dashboard.metadata.expanded_slices || {},
dashboard_title: dashboard.dashboard_title,
- default_filters: dashboard.readFilters(),
+ default_filters: this.props.readFilters(),
duplicate_slices: this.state.duplicateSlices,
};
let url = null;
diff --git a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
index 4c5f462..03e0cb8 100644
--- a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
@@ -11,6 +11,8 @@ require('react-bootstrap-table/css/react-bootstrap-table.css');
const propTypes = {
dashboard: PropTypes.object.isRequired,
triggerNode: PropTypes.node.isRequired,
+ userId: PropTypes.string.isRequired,
+ addSlicesToDashboard: PropTypes.func,
};
class SliceAdder extends React.Component {
@@ -43,7 +45,7 @@ class SliceAdder extends React.Component {
}
onEnterModal() {
- const uri = '/sliceaddview/api/read?_flt_0_created_by=' + this.props.dashboard.curUserId;
+ const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
this.slicesRequest = $.ajax({
url: uri,
type: 'GET',
@@ -52,7 +54,7 @@ class SliceAdder extends React.Component {
const slices = response.result.map(slice => ({
id: slice.id,
sliceName: slice.slice_name,
- vizType: slice.viz_type,
+ vizType: slice.vizType,
modified: slice.modified,
}));
@@ -65,14 +67,30 @@ class SliceAdder extends React.Component {
error: (error) => {
this.errored = true;
this.setState({
- errorMsg: this.props.dashboard.getAjaxErrorMsg(error),
+ errorMsg: t('Sorry, there was an error fetching slices to this dashboard: ') +
+ this.getAjaxErrorMsg(error),
});
},
});
}
+ getAjaxErrorMsg(error) {
+ const respJSON = error.responseJSON;
+ return (respJSON && respJSON.message) ? respJSON.message :
+ error.responseText;
+ }
+
addSlices() {
- this.props.dashboard.addSlicesToDashboard(Object.keys(this.state.selectionMap));
+ const adder = this;
+ this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
+ // if successful, page will be reloaded.
+ .fail((error) => {
+ adder.errored = true;
+ adder.setState({
+ errorMsg: t('Sorry, there was an error adding slices to this dashboard: ') +
+ this.getAjaxErrorMsg(error),
+ });
+ });
}
toggleSlice(slice) {
diff --git a/superset/assets/javascripts/dashboard/components/SliceCell.jsx b/superset/assets/javascripts/dashboard/components/SliceCell.jsx
deleted file mode 100644
index 2fbdff3..0000000
--- a/superset/assets/javascripts/dashboard/components/SliceCell.jsx
+++ /dev/null
@@ -1,117 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { t } from '../../locales';
-import { getExploreUrl } from '../../explore/exploreUtils';
-import EditableTitle from '../../components/EditableTitle';
-
-const propTypes = {
- slice: PropTypes.object.isRequired,
- removeSlice: PropTypes.func.isRequired,
- updateSliceName: PropTypes.func,
- expandedSlices: PropTypes.object,
-};
-
-const SliceCell = ({ expandedSlices, removeSlice, slice, updateSliceName }) => {
- const onSaveTitle = (newTitle) => {
- if (updateSliceName) {
- updateSliceName(slice.slice_id, newTitle);
- }
- };
-
- return (
- <div className="slice-cell" id={`${slice.slice_id}-cell`}>
- <div className="row chart-header">
- <div className="col-md-12">
- <div className="header">
- <EditableTitle
- title={slice.slice_name}
- canEdit={!!updateSliceName}
- onSaveTitle={onSaveTitle}
- noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
- />
- </div>
- <div className="chart-controls">
- <div id={'controls_' + slice.slice_id} className="pull-right">
- <a title={t('Move chart')} data-toggle="tooltip">
- <i className="fa fa-arrows drag" />
- </a>
- <a className="refresh" title={t('Force refresh data')} data-toggle="tooltip">
- <i className="fa fa-repeat" />
- </a>
- {slice.description &&
- <a title={t('Toggle chart description')}>
- <i
- className="fa fa-info-circle slice_info"
- title={slice.description}
- data-toggle="tooltip"
- />
- </a>
- }
- <a
- href={slice.edit_url}
- title={t('Edit chart')}
- data-toggle="tooltip"
- >
- <i className="fa fa-pencil" />
- </a>
- <a
- className="exportCSV"
- href={getExploreUrl(slice.form_data, 'csv')}
- title={t('Export CSV')}
- data-toggle="tooltip"
- >
- <i className="fa fa-table" />
- </a>
- <a
- className="exploreChart"
- href={getExploreUrl(slice.form_data)}
- title={t('Explore chart')}
- data-toggle="tooltip"
- >
- <i className="fa fa-share" />
- </a>
- <a
- className="remove-chart"
- title={t('Remove chart from dashboard')}
- data-toggle="tooltip"
- >
- <i
- className="fa fa-close"
- onClick={() => { removeSlice(slice.slice_id); }}
- />
- </a>
- </div>
- </div>
- </div>
- </div>
- <div
- className="slice_description bs-callout bs-callout-default"
- style={
- expandedSlices &&
- expandedSlices[String(slice.slice_id)] ? {} : { display: 'none' }
- }
- dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
- />
- <div className="row chart-container">
- <input type="hidden" value="false" />
- <div id={'token_' + slice.slice_id} className="token col-md-12">
- <img
- src="/static/assets/images/loading.gif"
- className="loading"
- alt="loading"
- />
- <div
- id={'con_' + slice.slice_id}
- className={`slice_container ${slice.form_data.viz_type}`}
- />
- </div>
- </div>
- </div>
- );
-};
-
-SliceCell.propTypes = propTypes;
-
-export default SliceCell;
diff --git a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
new file mode 100644
index 0000000..d1a2d9e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
@@ -0,0 +1,142 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+
+import { t } from '../../locales';
+import EditableTitle from '../../components/EditableTitle';
+import TooltipWrapper from '../../components/TooltipWrapper';
+
+const propTypes = {
+ slice: PropTypes.object.isRequired,
+ exploreChartUrl: PropTypes.string,
+ exportCSVUrl: PropTypes.string,
+ isExpanded: PropTypes.bool,
+ formDataExtra: PropTypes.object,
+ removeSlice: PropTypes.func,
+ updateSliceName: PropTypes.func,
+ toggleExpandSlice: PropTypes.func,
+ forceRefresh: PropTypes.func,
+};
+
+const defaultProps = {
+ forceRefresh: () => ({}),
+ removeSlice: () => ({}),
+ updateSliceName: () => ({}),
+ toggleExpandSlice: () => ({}),
+};
+
+class SliceHeader extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.onSaveTitle = this.onSaveTitle.bind(this);
+ }
+
+ onSaveTitle(newTitle) {
+ if (this.props.updateSliceName) {
+ this.props.updateSliceName(this.props.slice.slice_id, newTitle);
+ }
+ }
+
+ render() {
+ const slice = this.props.slice;
+ const isCached = slice.is_cached;
+ const isExpanded = !!this.props.isExpanded;
+ const cachedWhen = moment.utc(slice.cached_dttm).fromNow();
+ const refreshTooltip = isCached ?
+ t('Served from data cached %s . Click to force refresh.', cachedWhen) :
+ t('Force refresh data');
+
+ return (
+ <div className="row chart-header">
+ <div className="col-md-12">
+ <div className="header">
+ <EditableTitle
+ title={slice.slice_name}
+ canEdit={!!this.props.updateSliceName}
+ onSaveTitle={this.onSaveTitle}
+ noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
+ />
+ </div>
+ <div className="chart-controls">
+ <div id={'controls_' + slice.slice_id} className="pull-right">
+ <a>
+ <TooltipWrapper
+ placement="top"
+ label="move"
+ tooltip={t('Move chart')}
+ >
+ <i className="fa fa-arrows drag" />
+ </TooltipWrapper>
+ </a>
+ <a
+ className={`refresh ${isCached ? 'danger' : ''}`}
+ onClick={() => (this.props.forceRefresh(slice.slice_id))}
+ >
+ <TooltipWrapper
+ placement="top"
+ label="refresh"
+ tooltip={refreshTooltip}
+ >
+ <i className="fa fa-repeat" />
+ </TooltipWrapper>
+ </a>
+ {slice.description &&
+ <a onClick={() => this.props.toggleExpandSlice(slice, !isExpanded)}>
+ <TooltipWrapper
+ placement="top"
+ label="description"
+ tooltip={t('Toggle chart description')}
+ >
+ <i className="fa fa-info-circle slice_info" />
+ </TooltipWrapper>
+ </a>
+ }
+ <a href={slice.edit_url} target="_blank">
+ <TooltipWrapper
+ placement="top"
+ label="edit"
+ tooltip={t('Edit chart')}
+ >
+ <i className="fa fa-pencil" />
+ </TooltipWrapper>
+ </a>
+ <a className="exportCSV" href={this.props.exportCSVUrl}>
+ <TooltipWrapper
+ placement="top"
+ label="exportCSV"
+ tooltip={t('Export CSV')}
+ >
+ <i className="fa fa-table" />
+ </TooltipWrapper>
+ </a>
+ <a className="exploreChart" href={this.props.exploreChartUrl} target="_blank">
+ <TooltipWrapper
+ placement="top"
+ label="exploreChart"
+ tooltip={t('Explore chart')}
+ >
+ <i className="fa fa-share" />
+ </TooltipWrapper>
+ </a>
+ <a className="remove-chart" onClick={() => (this.props.removeSlice(slice))}>
+ <TooltipWrapper
+ placement="top"
+ label="close"
+ tooltip={t('Remove chart from dashboard')}
+ >
+ <i className="fa fa-close" />
+ </TooltipWrapper>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+SliceHeader.propTypes = propTypes;
+SliceHeader.defaultProps = defaultProps;
+
+export default SliceHeader;
diff --git a/superset/assets/javascripts/dashboard/index.jsx b/superset/assets/javascripts/dashboard/index.jsx
new file mode 100644
index 0000000..774e071
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/index.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { createStore, applyMiddleware, compose } from 'redux';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+
+import { initEnhancer } from '../reduxUtils';
+import { appSetup } from '../common';
+import { initJQueryAjax } from '../modules/utils';
+import DashboardContainer from './components/DashboardContainer';
+import rootReducer, { getInitialState } from './reducers';
+
+appSetup();
+initJQueryAjax();
+
+const appContainer = document.getElementById('app');
+const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
+const initState = Object.assign({}, getInitialState(bootstrapData));
+
+const store = createStore(
+ rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
+
+ReactDOM.render(
+ <Provider store={store}>
+ <DashboardContainer />
+ </Provider>,
+ appContainer,
+);
+
diff --git a/superset/assets/javascripts/dashboard/reducers.js b/superset/assets/javascripts/dashboard/reducers.js
new file mode 100644
index 0000000..8d7b7f4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/reducers.js
@@ -0,0 +1,188 @@
+import { combineReducers } from 'redux';
+import d3 from 'd3';
+
+import charts, { chart } from '../chart/chartReducer';
+import * as actions from './actions';
+import { getParam } from '../modules/utils';
+import { alterInArr, removeFromArr } from '../reduxUtils';
+import { applyDefaultFormData } from '../explore/stores/store';
+
+export function getInitialState(bootstrapData) {
+ const { user_id, datasources, common } = bootstrapData;
+ delete common.locale;
+ delete common.language_pack;
+
+ const dashboard = { ...bootstrapData.dashboard_data };
+ const filters = {};
+ try {
+ // allow request parameter overwrite dashboard metadata
+ const filterData = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
+ for (const key in filterData) {
+ const sliceId = parseInt(key, 10);
+ filters[sliceId] = filterData[key];
+ }
+ } catch (e) {
+ //
+ }
+
+ dashboard.posDict = {};
+ dashboard.layout = [];
+ if (dashboard.position_json) {
+ dashboard.position_json.forEach((position) => {
+ dashboard.posDict[position.slice_id] = position;
+ });
+ }
+ dashboard.slices.forEach((slice, index) => {
+ const sliceId = slice.slice_id;
+ let pos = dashboard.posDict[sliceId];
+ if (!pos) {
+ pos = {
+ col: (index * 4 + 1) % 12,
+ row: Math.floor((index) / 3) * 4,
+ size_x: 4,
+ size_y: 4,
+ };
+ }
+
+ dashboard.layout.push({
+ i: String(sliceId),
+ x: pos.col - 1,
+ y: pos.row,
+ w: pos.size_x,
+ minW: 2,
+ h: pos.size_y,
+ });
+ });
+
+ // will use charts action/reducers to handle chart render
+ const initCharts = {};
+ dashboard.slices.forEach((slice) => {
+ const chartKey = 'slice_' + slice.slice_id;
+ initCharts[chartKey] = { ...chart,
+ chartKey,
+ slice_id: slice.slice_id,
+ form_data: slice.form_data,
+ formData: applyDefaultFormData(slice.form_data),
+ };
+ });
+
+ // also need to add formData for dashboard.slices
+ dashboard.slices = dashboard.slices.map(slice =>
+ ({ ...slice, formData: applyDefaultFormData(slice.form_data) }),
+ );
+
+ return {
+ charts: initCharts,
+ dashboard: { filters, dashboard, userId: user_id, datasources, common },
+ };
+}
+
+const dashboard = function (state = {}, action) {
+ const actionHandlers = {
+ [actions.UPDATE_DASHBOARD_TITLE]() {
+ const newDashboard = { ...state.dashboard, dashboard_title: action.title };
+ return { ...state, dashboard: newDashboard };
+ },
+ [actions.UPDATE_DASHBOARD_LAYOUT]() {
+ const newDashboard = { ...state.dashboard, layout: action.layout };
+ return { ...state, dashboard: newDashboard };
+ },
+ [actions.REMOVE_SLICE]() {
+ const newLayout = state.dashboard.layout.filter(function (reactPos) {
+ return reactPos.i !== String(action.slice.slice_id);
+ });
+ const newDashboard = removeFromArr(state.dashboard, 'slices', action.slice, 'slice_id');
+ return { ...state, dashboard: { ...newDashboard, layout: newLayout } };
+ },
+ [actions.TOGGLE_FAVE_STAR]() {
+ return { ...state, isStarred: action.isStarred };
+ },
+ [actions.TOGGLE_EXPAND_SLICE]() {
+ const updatedExpandedSlices = { ...state.dashboard.metadata.expanded_slices };
+ const sliceId = action.slice.slice_id;
+ if (action.isExpanded) {
+ updatedExpandedSlices[sliceId] = true;
+ } else {
+ delete updatedExpandedSlices[sliceId];
+ }
+ const metadata = { ...state.dashboard.metadata, expanded_slices: updatedExpandedSlices };
+ const newDashboard = { ...state.dashboard, metadata };
+ return { ...state, dashboard: newDashboard };
+ },
+
+ // filters
+ [actions.ADD_FILTER]() {
+ const selectedSlice = state.dashboard.slices
+ .find(slice => (slice.slice_id === action.sliceId));
+ if (!selectedSlice) {
+ return state;
+ }
+
+ let filters;
+ const { sliceId, col, vals, merge, refresh } = action;
+ const filterKeys = ['__from', '__to', '__time_col',
+ '__time_grain', '__time_origin', '__granularity'];
+ if (filterKeys.indexOf(col) >= 0 ||
+ selectedSlice.formData.groupby.indexOf(col) !== -1) {
+ if (!(sliceId in state.filters)) {
+ filters = { ...state.filters, [sliceId]: {} };
+ }
+
+ let newFilter = {};
+ if (state.filters[sliceId] && !(col in state.filters[sliceId]) || !merge) {
+ newFilter = { ...state.filters[sliceId], [col]: vals };
+ // d3.merge pass in array of arrays while some value form filter components
+ // from and to filter box require string to be process and return
+ } else if (state.filters[sliceId][col] instanceof Array) {
+ newFilter = d3.merge([state.filters[sliceId][col], vals]);
+ } else {
+ newFilter = d3.merge([[state.filters[sliceId][col]], vals])[0] || '';
+ }
+ filters = { ...state.filters, [sliceId]: newFilter };
+ }
+ return { ...state, filters, refresh };
+ },
+ [actions.CLEAR_FILTER]() {
+ const newFilters = { ...state.filters };
+ delete newFilters[action.sliceId];
+
+ return { ...state.dashboard, filter: newFilters, refresh: true };
+ },
+ [actions.REMOVE_FILTER]() {
+ const newFilters = { ...state.filters };
+ const { sliceId, col, vals } = action;
+
+ if (sliceId in state.filters) {
+ if (col in state.filters[sliceId]) {
+ const a = [];
+ newFilters[sliceId][col].forEach(function (v) {
+ if (vals.indexOf(v) < 0) {
+ a.push(v);
+ }
+ });
+ newFilters[sliceId][col] = a;
+ }
+ }
+ return { ...state.dashboard, filter: newFilters, refresh: true };
+ },
+
+ // slice reducer
+ [actions.UPDATE_SLICE_NAME]() {
+ const newDashboard = alterInArr(
+ state.dashboard, 'slices',
+ action.slice, { slice_name: action.sliceName },
+ 'slice_id');
+ return { ...state.dashboard, dashboard: newDashboard };
+ },
+ };
+
+ if (action.type in actionHandlers) {
+ return actionHandlers[action.type]();
+ }
+ return state;
+};
+
+export default combineReducers({
+ charts,
+ dashboard,
+});
diff --git a/superset/assets/javascripts/explore/actions/chartActions.js b/superset/assets/javascripts/explore/actions/chartActions.js
deleted file mode 100644
index beca6ef..0000000
--- a/superset/assets/javascripts/explore/actions/chartActions.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { getExploreUrl } from '../exploreUtils';
-import { getFormDataFromControls } from '../stores/store';
-import { triggerQuery } from './exploreActions';
-
-const $ = window.$ = require('jquery');
-
-export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
-export function chartUpdateStarted(queryRequest, latestQueryFormData) {
- return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData };
-}
-
-export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
-export function chartUpdateSucceeded(queryResponse) {
- return { type: CHART_UPDATE_SUCCEEDED, queryResponse };
-}
-
-export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
-export function chartUpdateStopped(queryRequest) {
- if (queryRequest) {
- queryRequest.abort();
- }
- return { type: CHART_UPDATE_STOPPED };
-}
-
-export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
-export function chartUpdateTimeout(statusText, timeout) {
- return { type: CHART_UPDATE_TIMEOUT, statusText, timeout };
-}
-
-export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
-export function chartUpdateFailed(queryResponse) {
- return { type: CHART_UPDATE_FAILED, queryResponse };
-}
-
-export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS';
-export function updateChartStatus(status) {
- return { type: UPDATE_CHART_STATUS, status };
-}
-
-export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
-export function chartRenderingFailed(error) {
- return { type: CHART_RENDERING_FAILED, error };
-}
-
-export const REMOVE_CHART_ALERT = 'REMOVE_CHART_ALERT';
-export function removeChartAlert() {
- return { type: REMOVE_CHART_ALERT };
-}
-
-export const RUN_QUERY = 'RUN_QUERY';
-export function runQuery(formData, force = false, timeout = 60) {
- return function (dispatch, getState) {
- const { explore } = getState();
- const lastQueryFormData = getFormDataFromControls(explore.controls);
- const url = getExploreUrl(formData, 'json', force);
- const queryRequest = $.ajax({
- url,
- dataType: 'json',
- success(queryResponse) {
- dispatch(chartUpdateSucceeded(queryResponse));
- },
- error(err) {
- if (err.statusText === 'timeout') {
- dispatch(chartUpdateTimeout(err.statusText, timeout));
- } else if (err.statusText !== 'abort') {
- dispatch(chartUpdateFailed(err.responseJSON));
- }
- },
- timeout: timeout * 1000,
- });
- dispatch(chartUpdateStarted(queryRequest, lastQueryFormData));
- dispatch(triggerQuery(false));
- };
-}
diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js
index dbba7b7..b5be4d3 100644
--- a/superset/assets/javascripts/explore/actions/exploreActions.js
+++ b/superset/assets/javascripts/explore/actions/exploreActions.js
@@ -1,4 +1,5 @@
/* eslint camelcase: 0 */
+import { triggerQuery } from '../../chart/chartAction';
const $ = window.$ = require('jquery');
@@ -54,11 +55,6 @@ export function resetControls() {
return { type: RESET_FIELDS };
}
-export const TRIGGER_QUERY = 'TRIGGER_QUERY';
-export function triggerQuery(value = true) {
- return { type: TRIGGER_QUERY, value };
-}
-
export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) {
return function (dispatch) {
dispatch(fetchDatasourceStarted());
@@ -146,11 +142,6 @@ export function updateChartTitle(slice_name) {
return { type: UPDATE_CHART_TITLE, slice_name };
}
-export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
-export function renderTriggered() {
- return { type: RENDER_TRIGGERED };
-}
-
export const CREATE_NEW_SLICE = 'CREATE_NEW_SLICE';
export function createNewSlice(can_add, can_download, can_overwrite, slice, form_data) {
return { type: CREATE_NEW_SLICE, can_add, can_download, can_overwrite, slice, form_data };
diff --git a/superset/assets/javascripts/explore/components/ChartContainer.jsx b/superset/assets/javascripts/explore/components/ChartContainer.jsx
deleted file mode 100644
index f3c660a..0000000
--- a/superset/assets/javascripts/explore/components/ChartContainer.jsx
+++ /dev/null
@@ -1,362 +0,0 @@
-import $ from 'jquery';
-import React from 'react';
-import PropTypes from 'prop-types';
-import Mustache from 'mustache';
-import { connect } from 'react-redux';
-import { Alert, Collapse, Panel } from 'react-bootstrap';
-import visMap from '../../../visualizations/main';
-import { d3format } from '../../modules/utils';
-import ExploreActionButtons from './ExploreActionButtons';
-import EditableTitle from '../../components/EditableTitle';
-import FaveStar from '../../components/FaveStar';
-import TooltipWrapper from '../../components/TooltipWrapper';
-import Timer from '../../components/Timer';
-import { getExploreUrl } from '../exploreUtils';
-import { getFormDataFromControls } from '../stores/store';
-import CachedLabel from '../../components/CachedLabel';
-import { t } from '../../locales';
-
-const CHART_STATUS_MAP = {
- failed: 'danger',
- loading: 'warning',
- success: 'success',
-};
-
-const propTypes = {
- actions: PropTypes.object.isRequired,
- alert: PropTypes.string,
- can_overwrite: PropTypes.bool.isRequired,
- can_download: PropTypes.bool.isRequired,
- chartStatus: PropTypes.string,
- chartUpdateEndTime: PropTypes.number,
- chartUpdateStartTime: PropTypes.number.isRequired,
- column_formats: PropTypes.object,
- containerId: PropTypes.string.isRequired,
- height: PropTypes.string.isRequired,
- width: PropTypes.string.isRequired,
- isStarred: PropTypes.bool.isRequired,
- slice: PropTypes.object,
- table_name: PropTypes.string,
- viz_type: PropTypes.string.isRequired,
- formData: PropTypes.object,
- latestQueryFormData: PropTypes.object,
- queryResponse: PropTypes.object,
- triggerRender: PropTypes.bool,
- standalone: PropTypes.bool,
- datasourceType: PropTypes.string,
- datasourceId: PropTypes.number,
- timeout: PropTypes.number,
-};
-
-class ChartContainer extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- selector: `#${props.containerId}`,
- showStackTrace: false,
- };
- }
-
- componentDidUpdate(prevProps) {
- if (
- this.props.queryResponse &&
- (
- prevProps.queryResponse !== this.props.queryResponse ||
- prevProps.height !== this.props.height ||
- prevProps.width !== this.props.width ||
- this.props.triggerRender
- ) && !this.props.queryResponse.error
- && this.props.chartStatus !== 'failed'
- && this.props.chartStatus !== 'stopped'
- && this.props.chartStatus !== 'loading'
- ) {
- this.renderViz();
- }
- }
-
- getMockedSliceObject() {
- const props = this.props;
- const getHeight = () => {
- const headerHeight = props.standalone ? 0 : 100;
- return parseInt(props.height, 10) - headerHeight;
- };
- return {
- viewSqlQuery: props.queryResponse.query,
- containerId: props.containerId,
- datasource: props.datasource,
- selector: this.state.selector,
- formData: props.formData,
- container: {
- html: (data) => {
- // this should be a callback to clear the contents of the slice container
- $(this.state.selector).html(data);
- },
- css: (property, value) => {
- $(this.state.selector).css(property, value);
- },
- height: getHeight,
- show: () => { },
- get: n => ($(this.state.selector).get(n)),
- find: classname => ($(this.state.selector).find(classname)),
- },
-
- width: () => this.chartContainerRef.getBoundingClientRect().width,
-
- height: getHeight,
-
- render_template: (s) => {
- const context = {
- width: this.width,
- height: this.height,
- };
- return Mustache.render(s, context);
- },
-
- setFilter: () => {},
-
- getFilters: () => (
- // return filter objects from viz.formData
- {}
- ),
-
- addFilter: () => {},
-
- removeFilter: () => {},
-
- done: () => {},
- clearError: () => {
- // no need to do anything here since Alert is closable
- // query button will also remove Alert
- },
- error() {},
-
- d3format: (col, number) => {
- // mock d3format function in Slice object in superset.js
- const format = props.column_formats[col];
- return d3format(format, number);
- },
-
- data: {
- csv_endpoint: getExploreUrl(props.formData, 'csv'),
- json_endpoint: getExploreUrl(props.formData, 'json'),
- standalone_endpoint: getExploreUrl(props.formData, 'standalone'),
- },
-
- };
- }
-
- removeAlert() {
- this.props.actions.removeChartAlert();
- }
-
- runQuery() {
- this.props.actions.runQuery(this.props.formData, true, this.props.timeout);
- }
-
- updateChartTitleOrSaveSlice(newTitle) {
- const isNewSlice = !this.props.slice;
- const params = {
- slice_name: newTitle,
- action: isNewSlice ? 'saveas' : 'overwrite',
- };
- const saveUrl = getExploreUrl(this.props.formData, 'base', false, null, params);
- this.props.actions.saveSlice(saveUrl)
- .then((data) => {
- if (isNewSlice) {
- this.props.actions.createNewSlice(
- data.can_add, data.can_download, data.can_overwrite,
- data.slice, data.form_data);
- } else {
- this.props.actions.updateChartTitle(newTitle);
- }
- });
- }
-
- renderChartTitle() {
- let title;
- if (this.props.slice) {
- title = this.props.slice.slice_name;
- } else {
- title = t('%s - untitled', this.props.table_name);
- }
- return title;
- }
-
- renderViz() {
- this.props.actions.renderTriggered();
- const mockSlice = this.getMockedSliceObject();
- this.setState({ mockSlice });
- const viz = visMap[this.props.viz_type];
- try {
- viz(mockSlice, this.props.queryResponse, this.props.actions.setControlValue);
- } catch (e) {
- this.props.actions.chartRenderingFailed(e);
- }
- }
-
- renderAlert() {
- /* eslint-disable react/no-danger */
- const msg = (
- <div>
- <i
- className="fa fa-close pull-right"
- onClick={this.removeAlert.bind(this)}
- style={{ cursor: 'pointer' }}
- />
- <p
- dangerouslySetInnerHTML={{ __html: this.props.alert }}
- />
- </div>);
- return (
- <div>
- <Alert
- bsStyle="warning"
- onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
- >
- {msg}
- </Alert>
- {this.props.queryResponse && this.props.queryResponse.stacktrace &&
- <Collapse in={this.state.showStackTrace}>
- <pre>
- {this.props.queryResponse.stacktrace}
- </pre>
- </Collapse>
- }
- </div>);
- }
-
- renderChart() {
- if (this.props.alert) {
- return this.renderAlert();
- }
- const loading = this.props.chartStatus === 'loading';
- return (
- <div>
- {loading &&
- <img
- alt="loading"
- width="25"
- src="/static/assets/images/loading.gif"
- style={{ position: 'absolute' }}
- />
- }
- <div
- id={this.props.containerId}
- ref={(ref) => { this.chartContainerRef = ref; }}
- className={this.props.viz_type}
- style={{
- opacity: loading ? '0.25' : '1',
- }}
- />
- </div>
- );
- }
-
- render() {
- if (this.props.standalone) {
- // dom manipulation hack to get rid of the boostrap theme's body background
- $('body').addClass('background-transparent');
- return this.renderChart();
- }
- const queryResponse = this.props.queryResponse;
- return (
- <div className="chart-container">
- <Panel
- style={{ height: this.props.height }}
- header={
- <div
- id="slice-header"
- className="clearfix panel-title-large"
- >
- <EditableTitle
- title={this.renderChartTitle()}
- canEdit={!this.props.slice || this.props.can_overwrite}
- onSaveTitle={this.updateChartTitleOrSaveSlice.bind(this)}
- />
-
- {this.props.slice &&
- <span>
- <FaveStar
- sliceId={this.props.slice.slice_id}
- actions={this.props.actions}
- isStarred={this.props.isStarred}
- />
-
- <TooltipWrapper
- label="edit-desc"
- tooltip={t('Edit slice properties')}
- >
- <a
- className="edit-desc-icon"
- href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
- >
- <i className="fa fa-edit" />
- </a>
- </TooltipWrapper>
- </span>
- }
-
- <div className="pull-right">
- {this.props.chartStatus === 'success' &&
- this.props.queryResponse &&
- this.props.queryResponse.is_cached &&
- <CachedLabel
- onClick={this.runQuery.bind(this)}
- cachedTimestamp={queryResponse.cached_dttm}
- />
- }
- <Timer
- startTime={this.props.chartUpdateStartTime}
- endTime={this.props.chartUpdateEndTime}
- isRunning={this.props.chartStatus === 'loading'}
- status={CHART_STATUS_MAP[this.props.chartStatus]}
- style={{ fontSize: '10px', marginRight: '5px' }}
- />
- <ExploreActionButtons
- slice={this.state.mockSlice}
- canDownload={this.props.can_download}
- chartStatus={this.props.chartStatus}
- queryResponse={queryResponse}
- queryEndpoint={getExploreUrl(this.props.latestQueryFormData, 'query')}
- />
- </div>
- </div>
- }
- >
- {this.renderChart()}
- </Panel>
- </div>
- );
- }
-}
-
-ChartContainer.propTypes = propTypes;
-
-function mapStateToProps({ explore, chart }) {
- const formData = getFormDataFromControls(explore.controls);
- return {
- alert: chart.chartAlert,
- can_overwrite: !!explore.can_overwrite,
- can_download: !!explore.can_download,
- datasource: explore.datasource,
- column_formats: explore.datasource ? explore.datasource.column_formats : null,
- containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container',
- formData,
- isStarred: explore.isStarred,
- slice: explore.slice,
- standalone: explore.standalone,
- table_name: formData.datasource_name,
- viz_type: formData.viz_type,
- triggerRender: explore.triggerRender,
- datasourceType: explore.datasource.type,
- datasourceId: explore.datasource_id,
- chartStatus: chart.chartStatus,
- chartUpdateEndTime: chart.chartUpdateEndTime,
- chartUpdateStartTime: chart.chartUpdateStartTime,
- latestQueryFormData: chart.latestQueryFormData,
- queryResponse: chart.queryResponse,
- timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
- };
-}
-
-export default connect(mapStateToProps, () => ({}))(ChartContainer);
diff --git a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
index c5615dc..01d59e1 100644
--- a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
+++ b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
@@ -73,7 +73,7 @@ export default class EmbedCodeButton extends React.Component {
<div className="col-md-6 col-sm-12">
<div className="form-group">
<small>
- <label className="control-label" htmlFor="embed-height">t('Height')</label>
+ <label className="control-label" htmlFor="embed-height">{t('Height')}</label>
</small>
<input
className="form-control input-sm"
@@ -87,7 +87,7 @@ export default class EmbedCodeButton extends React.Component {
<div className="col-md-6 col-sm-12">
<div className="form-group">
<small>
- <label className="control-label" htmlFor="embed-width">t('Width')</label>
+ <label className="control-label" htmlFor="embed-width">{t('Width')}</label>
</small>
<input
className="form-control input-sm"
diff --git a/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx
new file mode 100644
index 0000000..8c2e1f3
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { chartPropType } from '../../chart/chartReducer';
+import ExploreActionButtons from './ExploreActionButtons';
+import EditableTitle from '../../components/EditableTitle';
+import FaveStar from '../../components/FaveStar';
+import TooltipWrapper from '../../components/TooltipWrapper';
+import Timer from '../../components/Timer';
+import { getExploreUrl } from '../exploreUtils';
+import CachedLabel from '../../components/CachedLabel';
+import { t } from '../../locales';
+
+const CHART_STATUS_MAP = {
+ failed: 'danger',
+ loading: 'warning',
+ success: 'success',
+};
+
+const propTypes = {
+ actions: PropTypes.object.isRequired,
+ can_overwrite: PropTypes.bool.isRequired,
+ can_download: PropTypes.bool.isRequired,
+ isStarred: PropTypes.bool.isRequired,
+ slice: PropTypes.object,
+ table_name: PropTypes.string,
+ form_data: PropTypes.object,
+ timeout: PropTypes.number,
+ chart: PropTypes.shape(chartPropType),
+};
+
+class ExploreChartHeader extends React.PureComponent {
+ runQuery() {
+ this.props.actions.runQuery(this.props.form_data, true,
+ this.props.timeout, this.props.chart.chartKey);
+ }
+
+ updateChartTitleOrSaveSlice(newTitle) {
+ const isNewSlice = !this.props.slice;
+ const params = {
+ slice_name: newTitle,
+ action: isNewSlice ? 'saveas' : 'overwrite',
+ };
+ const saveUrl = getExploreUrl(this.props.form_data, 'base', false, null, params);
+ this.props.actions.saveSlice(saveUrl)
+ .then((data) => {
+ if (isNewSlice) {
+ this.props.actions.createNewSlice(
+ data.can_add, data.can_download, data.can_overwrite,
+ data.slice, data.form_data);
+ } else {
+ this.props.actions.updateChartTitle(newTitle);
+ }
+ });
+ }
+
+ renderChartTitle() {
+ let title;
+ if (this.props.slice) {
+ title = this.props.slice.slice_name;
+ } else {
+ title = t('%s - untitled', this.props.table_name);
+ }
+ return title;
+ }
+
+ render() {
+ const queryResponse = this.props.chart.queryResponse;
+ const data = {
+ csv_endpoint: getExploreUrl(this.props.form_data, 'csv'),
+ json_endpoint: getExploreUrl(this.props.form_data, 'json'),
+ standalone_endpoint: getExploreUrl(this.props.form_data, 'standalone'),
+ };
+
+ return (
+ <div
+ id="slice-header"
+ className="clearfix panel-title-large"
+ >
+ <EditableTitle
+ title={this.renderChartTitle()}
+ canEdit={!this.props.slice || this.props.can_overwrite}
+ onSaveTitle={this.updateChartTitleOrSaveSlice.bind(this)}
+ />
+
+ {this.props.slice &&
+ <span>
+ <FaveStar
+ itemId={this.props.slice.slice_id}
+ fetchFaveStar={this.props.actions.fetchFaveStar}
+ saveFaveStar={this.props.actions.saveFaveStar}
+ isStarred={this.props.isStarred}
+ />
+
+ <TooltipWrapper
+ label="edit-desc"
+ tooltip={t('Edit slice properties')}
+ >
+ <a
+ className="edit-desc-icon"
+ href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
+ >
+ <i className="fa fa-edit" />
+ </a>
+ </TooltipWrapper>
+ </span>
+ }
+
+ <div className="pull-right">
+ {this.props.chart.chartStatus === 'success' &&
+ queryResponse &&
+ queryResponse.is_cached &&
+ <CachedLabel
+ onClick={this.runQuery.bind(this)}
+ cachedTimestamp={queryResponse.cached_dttm}
+ />
+ }
+ <Timer
+ startTime={this.props.chart.chartUpdateStartTime}
+ endTime={this.props.chart.chartUpdateEndTime}
+ isRunning={this.props.chart.chartStatus === 'loading'}
+ status={CHART_STATUS_MAP[this.props.chart.chartStatus]}
+ style={{ fontSize: '10px', marginRight: '5px' }}
+ />
+ <ExploreActionButtons
+ slice={Object.assign({}, this.props.slice, { data })}
+ canDownload={this.props.can_download}
+ chartStatus={this.props.chart.chartStatus}
+ queryResponse={queryResponse}
+ queryEndpoint={getExploreUrl(this.props.form_data, 'query')}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+ExploreChartHeader.propTypes = propTypes;
+
+export default ExploreChartHeader;
diff --git a/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
new file mode 100644
index 0000000..7834787
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
@@ -0,0 +1,79 @@
+import $ from 'jquery';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Panel } from 'react-bootstrap';
+
+import { chartPropType } from '../../chart/chartReducer';
+import ChartContainer from '../../chart/ChartContainer';
+import ExploreChartHeader from './ExploreChartHeader';
+
+const propTypes = {
+ actions: PropTypes.object.isRequired,
+ can_overwrite: PropTypes.bool.isRequired,
+ can_download: PropTypes.bool.isRequired,
+ datasource: PropTypes.object,
+ column_formats: PropTypes.object,
+ containerId: PropTypes.string.isRequired,
+ height: PropTypes.string.isRequired,
+ width: PropTypes.string.isRequired,
+ isStarred: PropTypes.bool.isRequired,
+ slice: PropTypes.object,
+ table_name: PropTypes.string,
+ vizType: PropTypes.string.isRequired,
+ form_data: PropTypes.object,
+ standalone: PropTypes.bool,
+ timeout: PropTypes.number,
+ chart: PropTypes.shape(chartPropType),
+};
+
+class ExploreChartPanel extends React.PureComponent {
+ getHeight() {
+ const headerHeight = this.props.standalone ? 0 : 100;
+ return parseInt(this.props.height, 10) - headerHeight;
+ }
+
+ render() {
+ if (this.props.standalone) {
+ // dom manipulation hack to get rid of the boostrap theme's body background
+ $('body').addClass('background-transparent');
+ return this.renderChart();
+ }
+
+ const header = (
+ <ExploreChartHeader
+ actions={this.props.actions}
+ can_overwrite={this.props.can_overwrite}
+ can_download={this.props.can_download}
+ isStarred={this.props.isStarred}
+ slice={this.props.slice}
+ table_name={this.props.table_name}
+ form_data={this.props.form_data}
+ timeout={this.props.timeout}
+ chart={this.props.chart}
+ />);
+ return (
+ <div className="chart-container">
+ <Panel
+ style={{ height: this.props.height }}
+ header={header}
+ >
+ <ChartContainer
+ containerId={this.props.containerId}
+ datasource={this.props.datasource}
+ formData={this.props.form_data}
+ height={this.getHeight()}
+ slice={this.props.slice}
+ chartKey={this.props.chart.chartKey}
+ setControlValue={this.props.actions.setControlValue}
+ timeout={this.props.timeout}
+ vizType={this.props.vizType}
+ />
+ </Panel>
+ </div>
+ );
+ }
+}
+
+ExploreChartPanel.propTypes = propTypes;
+
+export default ExploreChartPanel;
diff --git a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
index f696ed6..e3ea7f2 100644
--- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
@@ -3,27 +3,28 @@ import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import ChartContainer from './ChartContainer';
+
+import ExploreChartPanel from './ExploreChartPanel';
import ControlPanelsContainer from './ControlPanelsContainer';
import SaveModal from './SaveModal';
import QueryAndSaveBtns from './QueryAndSaveBtns';
import { getExploreUrl } from '../exploreUtils';
import { getFormDataFromControls } from '../stores/store';
+import { chartPropType } from '../../chart/chartReducer';
import * as exploreActions from '../actions/exploreActions';
import * as saveModalActions from '../actions/saveModalActions';
-import * as chartActions from '../actions/chartActions';
+import * as chartActions from '../../chart/chartAction';
const propTypes = {
actions: PropTypes.object.isRequired,
datasource_type: PropTypes.string.isRequired,
isDatasourceMetaLoading: PropTypes.bool.isRequired,
chartStatus: PropTypes.string,
+ chart: PropTypes.shape(chartPropType).isRequired,
controls: PropTypes.object.isRequired,
forcedHeight: PropTypes.string,
form_data: PropTypes.object.isRequired,
standalone: PropTypes.bool.isRequired,
- triggerQuery: PropTypes.bool.isRequired,
- queryRequest: PropTypes.object,
timeout: PropTypes.number,
};
@@ -39,13 +40,12 @@ class ExploreViewContainer extends React.Component {
componentDidMount() {
window.addEventListener('resize', this.handleResize.bind(this));
- this.triggerQueryIfNeeded();
}
componentWillReceiveProps(np) {
if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
this.props.actions.resetControls();
- this.props.actions.triggerQuery();
+ this.props.actions.triggerQuery(true, this.props.chart.chartKey);
}
if (np.controls.datasource.value !== this.props.controls.datasource.value) {
this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
@@ -63,9 +63,7 @@ class ExploreViewContainer extends React.Component {
onQuery() {
// remove alerts when query
this.props.actions.removeControlPanelAlert();
- this.props.actions.removeChartAlert();
-
- this.props.actions.triggerQuery();
+ this.props.actions.triggerQuery(true, this.props.chart.chartKey);
history.pushState(
{},
@@ -74,7 +72,7 @@ class ExploreViewContainer extends React.Component {
}
onStop() {
- this.props.actions.chartUpdateStopped(this.props.queryRequest);
+ this.props.actions.chartUpdateStopped(this.props.chart.queryRequest);
}
getWidth() {
@@ -90,8 +88,9 @@ class ExploreViewContainer extends React.Component {
}
triggerQueryIfNeeded() {
- if (this.props.triggerQuery && !this.hasErrors()) {
- this.props.actions.runQuery(this.props.form_data, false, this.props.timeout);
+ if (this.props.chart.triggerQuery && !this.hasErrors()) {
+ this.props.actions.runQuery(this.props.form_data, false,
+ this.props.timeout, this.props.chart.chartKey);
}
}
@@ -134,10 +133,10 @@ class ExploreViewContainer extends React.Component {
}
renderChartContainer() {
return (
- <ChartContainer
- actions={this.props.actions}
+ <ExploreChartPanel
width={this.state.width}
height={this.state.height}
+ {...this.props}
/>);
}
@@ -168,7 +167,7 @@ class ExploreViewContainer extends React.Component {
onQuery={this.onQuery.bind(this)}
onSave={this.toggleModal.bind(this)}
onStop={this.onStop.bind(this)}
- loading={this.props.chartStatus === 'loading'}
+ loading={this.props.chart.chartStatus === 'loading'}
errorMessage={this.renderErrorMessage()}
/>
<br />
@@ -191,18 +190,28 @@ class ExploreViewContainer extends React.Component {
ExploreViewContainer.propTypes = propTypes;
-function mapStateToProps({ explore, chart }) {
+function mapStateToProps({ explore, charts }) {
const form_data = getFormDataFromControls(explore.controls);
+ const chartKey = Object.keys(charts)[0];
+ const chart = charts[chartKey];
return {
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
+ datasource: explore.datasource,
datasource_type: explore.datasource.type,
+ datasourceId: explore.datasource_id,
controls: explore.controls,
+ can_overwrite: !!explore.can_overwrite,
+ can_download: !!explore.can_download,
+ column_formats: explore.datasource ? explore.datasource.column_formats : null,
+ containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container',
+ isStarred: explore.isStarred,
+ slice: explore.slice,
form_data,
+ table_name: form_data.datasource_name,
+ vizType: form_data.viz_type,
standalone: explore.standalone,
- triggerQuery: explore.triggerQuery,
forcedHeight: explore.forced_height,
- queryRequest: chart.queryRequest,
- chartStatus: chart.chartStatus,
+ chart,
timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
};
}
diff --git a/superset/assets/javascripts/explore/components/SaveModal.jsx b/superset/assets/javascripts/explore/components/SaveModal.jsx
index 2939f2e..7b375c2 100644
--- a/superset/assets/javascripts/explore/components/SaveModal.jsx
+++ b/superset/assets/javascripts/explore/components/SaveModal.jsx
@@ -13,7 +13,7 @@ const propTypes = {
onHide: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
form_data: PropTypes.object,
- user_id: PropTypes.string.isRequired,
+ userId: PropTypes.string.isRequired,
dashboards: PropTypes.array.isRequired,
alert: PropTypes.string,
slice: PropTypes.object,
@@ -34,7 +34,7 @@ class SaveModal extends React.Component {
};
}
componentDidMount() {
- this.props.actions.fetchDashboards(this.props.user_id);
+ this.props.actions.fetchDashboards(this.props.userId);
}
onChange(name, event) {
switch (name) {
@@ -243,7 +243,7 @@ function mapStateToProps({ explore, saveModal }) {
datasource: explore.datasource,
slice: explore.slice,
can_overwrite: explore.can_overwrite,
- user_id: explore.user_id,
+ userId: explore.userId,
dashboards: saveModal.dashboards,
alert: saveModal.saveModalAlert,
};
diff --git a/superset/assets/javascripts/explore/index.jsx b/superset/assets/javascripts/explore/index.jsx
index 049e731..2247019 100644
--- a/superset/assets/javascripts/explore/index.jsx
+++ b/superset/assets/javascripts/explore/index.jsx
@@ -34,19 +34,23 @@ const bootstrappedState = Object.assign(
filterColumnOpts: [],
isDatasourceMetaLoading: false,
isStarred: false,
- triggerQuery: true,
- triggerRender: false,
},
);
+const chartKey = bootstrappedState.slice ? ('slice_' + bootstrappedState.slice.slice_id) : 'slice';
const initState = {
- chart: {
- chartAlert: null,
- chartStatus: null,
- chartUpdateEndTime: null,
- chartUpdateStartTime: now(),
- latestQueryFormData: getFormDataFromControls(controls),
- queryResponse: null,
+ charts: {
+ [chartKey]: {
+ chartKey,
+ chartAlert: null,
+ chartStatus: null,
+ chartUpdateEndTime: null,
+ chartUpdateStartTime: now(),
+ latestQueryFormData: getFormDataFromControls(controls),
+ queryResponse: null,
+ triggerQuery: true,
+ triggerRender: false,
+ },
},
saveModal: {
dashboards: [],
diff --git a/superset/assets/javascripts/explore/reducers/chartReducer.js b/superset/assets/javascripts/explore/reducers/chartReducer.js
deleted file mode 100644
index 808d884..0000000
--- a/superset/assets/javascripts/explore/reducers/chartReducer.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/* eslint camelcase: 0 */
-import { now } from '../../modules/dates';
-import * as actions from '../actions/chartActions';
-import { t } from '../../locales';
-
-export default function chartReducer(state = {}, action) {
- const actionHandlers = {
- [actions.CHART_UPDATE_SUCCEEDED]() {
- return Object.assign(
- {},
- state,
- {
- chartStatus: 'success',
- queryResponse: action.queryResponse,
- },
- );
- },
- [actions.CHART_UPDATE_STARTED]() {
- return Object.assign({}, state,
- {
- chartStatus: 'loading',
- chartUpdateEndTime: null,
- chartUpdateStartTime: now(),
- queryRequest: action.queryRequest,
- latestQueryFormData: action.latestQueryFormData,
- });
- },
- [actions.CHART_UPDATE_STOPPED]() {
- return Object.assign({}, state,
- {
- chartStatus: 'stopped',
- chartAlert: t('Updating chart was stopped'),
- });
- },
- [actions.CHART_RENDERING_FAILED]() {
- return Object.assign({}, state, {
- chartStatus: 'failed',
- chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
- });
- },
- [actions.CHART_UPDATE_TIMEOUT]() {
- return Object.assign({}, state, {
- chartStatus: 'failed',
- chartAlert: (
- '<strong>Query timeout</strong> - visualization query are set to timeout at ' +
- `${action.timeout} seconds. ` +
- t('Perhaps your data has grown, your database is under unusual load, ' +
- 'or you are simply querying a data source that is to large ' +
- 'to be processed within the timeout range. ' +
- 'If that is the case, we recommend that you summarize your data further.')),
- });
- },
- [actions.CHART_UPDATE_FAILED]() {
- return Object.assign({}, state, {
- chartStatus: 'failed',
- chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
- chartUpdateEndTime: now(),
- queryResponse: action.queryResponse,
- });
- },
- [actions.UPDATE_CHART_STATUS]() {
- const newState = Object.assign({}, state, { chartStatus: action.status });
- if (action.status === 'success' || action.status === 'failed') {
- newState.chartUpdateEndTime = now();
- }
- return newState;
- },
- [actions.REMOVE_CHART_ALERT]() {
- if (state.chartAlert !== null) {
- return Object.assign({}, state, { chartAlert: null });
- }
- return state;
- },
- };
-
- if (action.type in actionHandlers) {
- return actionHandlers[action.type]();
- }
- return state;
-}
diff --git a/superset/assets/javascripts/explore/reducers/exploreReducer.js b/superset/assets/javascripts/explore/reducers/exploreReducer.js
index e37df6e..7b55748 100644
--- a/superset/assets/javascripts/explore/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explore/reducers/exploreReducer.js
@@ -56,11 +56,6 @@ export default function exploreReducer(state = {}, action) {
}
return Object.assign({}, state, changes);
},
- [actions.TRIGGER_QUERY]() {
- return Object.assign({}, state, {
- triggerQuery: action.value,
- });
- },
[actions.UPDATE_CHART_TITLE]() {
const updatedSlice = Object.assign({}, state.slice, { slice_name: action.slice_name });
return Object.assign({}, state, { slice: updatedSlice });
@@ -69,9 +64,6 @@ export default function exploreReducer(state = {}, action) {
const controls = getControlsState(state, getFormDataFromControls(state.controls));
return Object.assign({}, state, { controls });
},
- [actions.RENDER_TRIGGERED]() {
- return Object.assign({}, state, { triggerRender: false });
- },
[actions.CREATE_NEW_SLICE]() {
return Object.assign({}, state, {
slice: action.slice,
diff --git a/superset/assets/javascripts/explore/reducers/index.js b/superset/assets/javascripts/explore/reducers/index.js
index 0d5acb0..22f7e8f 100644
--- a/superset/assets/javascripts/explore/reducers/index.js
+++ b/superset/assets/javascripts/explore/reducers/index.js
@@ -1,11 +1,11 @@
import { combineReducers } from 'redux';
-import chart from './chartReducer';
+import charts from '../../chart/chartReducer';
import saveModal from './saveModalReducer';
import explore from './exploreReducer';
export default combineReducers({
- chart,
+ charts,
saveModal,
explore,
});
diff --git a/superset/assets/javascripts/modules/utils.js b/superset/assets/javascripts/modules/utils.js
index 83ec2c0..e7757d4 100644
--- a/superset/assets/javascripts/modules/utils.js
+++ b/superset/assets/javascripts/modules/utils.js
@@ -240,3 +240,11 @@ export function tryNumify(s) {
}
return n;
}
+
+export function getParam(name) {
+ /* eslint no-useless-escape: 0 */
+ const formattedName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
+ const regex = new RegExp('[\\?&]' + formattedName + '=([^&#]*)');
+ const results = regex.exec(location.search);
+ return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
+}
diff --git a/superset/assets/javascripts/reduxUtils.js b/superset/assets/javascripts/reduxUtils.js
index fc42083..abe2d7f 100644
--- a/superset/assets/javascripts/reduxUtils.js
+++ b/superset/assets/javascripts/reduxUtils.js
@@ -19,10 +19,9 @@ export function alterInObject(state, arrKey, obj, alterations) {
return Object.assign({}, state, { [arrKey]: newObject });
}
-export function alterInArr(state, arrKey, obj, alterations) {
+export function alterInArr(state, arrKey, obj, alterations, idKey = 'id') {
// Finds an item in an array in the state and replaces it with a
// new object with an altered property
- const idKey = 'id';
const newArr = [];
state[arrKey].forEach((arrItem) => {
if (obj[idKey] === arrItem[idKey]) {
@@ -96,19 +95,5 @@ export function areArraysShallowEqual(arr1, arr2) {
}
export function areObjectsEqual(obj1, obj2) {
- if (!obj1 || !obj2) {
- return false;
- }
- if (!Object.keys(obj1).length !== Object.keys(obj2).length) {
- return false;
- }
- for (const id in obj1) {
- if (!obj2.hasOwnProperty(id)) {
- return false;
- }
- if (obj1[id] !== obj2[id]) {
- return false;
- }
- }
- return true;
+ return JSON.stringify(obj1) === JSON.stringify(obj2);
}
diff --git a/superset/assets/spec/javascripts/dashboard/SliceCell_spec.jsx b/superset/assets/spec/javascripts/dashboard/SliceCell_spec.jsx
deleted file mode 100644
index 8dbf661..0000000
--- a/superset/assets/spec/javascripts/dashboard/SliceCell_spec.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import { describe, it } from 'mocha';
-import { expect } from 'chai';
-import { slice } from './fixtures';
-
-import SliceCell from '../../../javascripts/dashboard/components/SliceCell';
-
-describe('SliceCell', () => {
- const mockedProps = {
- slice,
- removeSlice: () => {},
- expandedSlices: {},
- };
- it('is valid', () => {
- expect(
- React.isValidElement(<SliceCell {...mockedProps} />),
- ).to.equal(true);
- });
- it('renders six links', () => {
- const wrapper = mount(<SliceCell {...mockedProps} />);
- expect(wrapper.find('a')).to.have.length(6);
- });
-});
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures.jsx b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
index 7c822d7..be515e3 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures.jsx
+++ b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
@@ -66,5 +66,5 @@ export const contextData = {
dash_save_perm: true,
standalone_mode: false,
dash_edit_perm: true,
- user_id: '1',
+ userId: '1',
};
diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js b/superset/assets/spec/javascripts/explore/chartActions_spec.js
index b2e069a..f88de8f 100644
--- a/superset/assets/spec/javascripts/explore/chartActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js
@@ -3,7 +3,7 @@ import { expect } from 'chai';
import sinon from 'sinon';
import $ from 'jquery';
import * as exploreUtils from '../../../javascripts/explore/exploreUtils';
-import * as actions from '../../../javascripts/explore/actions/chartActions';
+import * as actions from '../../../javascripts/chart/chartAction';
describe('chart actions', () => {
let dispatch;
diff --git a/superset/assets/spec/javascripts/explore/components/ChartContainer_spec.js b/superset/assets/spec/javascripts/explore/components/ExploreChartPanel_spec.js
similarity index 100%
rename from superset/assets/spec/javascripts/explore/components/ChartContainer_spec.js
rename to superset/assets/spec/javascripts/explore/components/ExploreChartPanel_spec.js
diff --git a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
index e548d21..346feda 100644
--- a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
@@ -24,7 +24,7 @@ describe('SaveModal', () => {
},
explore: {
can_overwrite: true,
- user_id: '1',
+ userId: '1',
datasource: {},
slice: {
slice_id: 1,
diff --git a/superset/assets/spec/javascripts/explore/exploreActions_spec.js b/superset/assets/spec/javascripts/explore/exploreActions_spec.js
index 5d2926d..d37fc46 100644
--- a/superset/assets/spec/javascripts/explore/exploreActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/exploreActions_spec.js
@@ -3,6 +3,7 @@ import { it, describe } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';
import $ from 'jquery';
+import * as chartActions from '../../../javascripts/chart/chartAction';
import * as actions from '../../../javascripts/explore/actions/exploreActions';
import { defaultState } from '../../../javascripts/explore/stores/store';
import exploreReducer from '../../../javascripts/explore/reducers/exploreReducer';
@@ -77,7 +78,7 @@ describe('fetching actions', () => {
ajaxStub.yieldsTo('success', { data: '' });
makeRequest(true);
expect(dispatch.callCount).to.equal(5);
- expect(dispatch.getCall(4).args[0].type).to.equal(actions.TRIGGER_QUERY);
+ expect(dispatch.getCall(4).args[0].type).to.equal(chartActions.TRIGGER_QUERY);
});
});
});
diff --git a/superset/assets/stylesheets/dashboard.css b/superset/assets/stylesheets/dashboard.css
index 289ead3..b110311 100644
--- a/superset/assets/stylesheets/dashboard.css
+++ b/superset/assets/stylesheets/dashboard.css
@@ -18,17 +18,26 @@ div.widget .chart-controls {
right: 0;
top: 5px;
padding: 5px 5px;
+ opacity: 0;
+ transition: opacity 0.5s ease-in-out;
+}
+div.widget:hover .chart-controls {
opacity: 0.75;
- display: none;
+ transition: opacity 0.5s ease-in-out;
}
.slice-grid div.widget {
border-radius: 0;
- border: 0px;
+ border: 0;
box-shadow: none;
background-color: #fff;
overflow: visible;
}
+.slice-grid .slice_container {
+ background-color: #fff;
+ padding-left: 5px;
+}
+
.dashboard .slice-grid .dragging,
.dashboard .slice-grid .resizing {
opacity: 0.5;
@@ -84,10 +93,12 @@ div.widget .chart-controls {
.slice-cell {
box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
transition: box-shadow 1s ease-in;
+ height: 100%;
}
.slice-cell-highlight {
box-shadow: 0px 0px 20px 5px rgba(0,0,0,0.2);
+ height: 100%;
}
.slice-cell .editable-title input[type="button"] {
@@ -95,7 +106,7 @@ div.widget .chart-controls {
}
.dashboard .separator.widget .slice_container {
- padding: 0px;
+ padding: 0;
overflow: visible;
}
.dashboard .separator.widget .slice_container hr {
@@ -116,6 +127,8 @@ div.widget .chart-controls {
.dashboard .title .favstar {
font-size: 20px;
+ position: relative;
+ top: -5px;
}
.chart-header .header {
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index ea43e54..a42c8ba 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -189,8 +189,23 @@ div.widget .chart-header a {
display: none;
}
-div.widget .slice_container {
- overflow: hidden;
+div.widget {
+ .slice_container {
+ overflow: hidden;
+ }
+
+ .stack-trace-container.has-trace {
+ .alert-warning:hover {
+ cursor: pointer;
+ }
+ }
+
+ .is-loading {
+ .stack-trace-container,
+ .slice_container {
+ opacity: 0.5;
+ }
+ }
}
.navbar .alert {
diff --git a/superset/assets/visualizations/markup.js b/superset/assets/visualizations/markup.js
index 739e451..8b43716 100644
--- a/superset/assets/visualizations/markup.js
+++ b/superset/assets/visualizations/markup.js
@@ -22,7 +22,7 @@ function markupWidget(slice, payload) {
jqdiv.html(`
<iframe id="${iframeId}"
frameborder="0"
- height="${slice.height()}"
+ height="${slice.height() - 20}"
sandbox="allow-same-origin allow-scripts allow-top-navigation allow-popups">
</iframe>
`);
diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js
index bb1729c..ca1465e 100644
--- a/superset/assets/webpack.config.js
+++ b/superset/assets/webpack.config.js
@@ -19,7 +19,7 @@ const config = {
common: APP_DIR + '/javascripts/common.js',
addSlice: ['babel-polyfill', APP_DIR + '/javascripts/addSlice/index.jsx'],
explore: ['babel-polyfill', APP_DIR + '/javascripts/explore/index.jsx'],
- dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'],
+ dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/index.jsx'],
sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'],
welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'],
profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'],
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index bb0b97c..1a158d9 100644
--- a/superset/templates/superset/dashboard.html
+++ b/superset/templates/superset/dashboard.html
@@ -6,11 +6,5 @@
class="dashboard container-fluid"
data-bootstrap="{{ bootstrap_data }}"
>
- <div id="alert-container"></div>
- <div id="dashboard-header"></div>
-
- <!-- gridster class used for backwards compatibility -->
- <div id="grid-container" class="slice-grid gridster"></div>
-
</div>
{% endblock %}
--
To stop receiving notification emails like this one, please contact
['"commits@superset.apache.org" <co...@superset.apache.org>'].