You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/06/22 00:54:21 UTC
[incubator-superset] 06/26: Dashboard builder rebased + linted
(#4849)
This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch dashboard-builder
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit 5ff440b101ae2c3ee41e184fea2c594fb8d2bfec
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Thu Apr 19 15:27:23 2018 -0700
Dashboard builder rebased + linted (#4849)
* define dashboard redux state
* update dashboard state reducer
* dashboard layout converter + grid render
* builder pane + slice adder
* Dashboard header + slice header controls
* fix linting
* 2nd code review comments
---
superset/assets/package.json | 1 +
.../assets/spec/javascripts/chart/Chart_spec.jsx | 2 +-
.../spec/javascripts/dashboard/Dashboard_spec.jsx | 96 ++++--
.../assets/spec/javascripts/dashboard/fixtures.jsx | 10 +-
.../spec/javascripts/dashboard/reducers_spec.js | 23 +-
superset/assets/src/chart/Chart.jsx | 38 +--
superset/assets/src/chart/ChartContainer.jsx | 2 +-
superset/assets/src/chart/chartAction.js | 14 +-
superset/assets/src/chart/chartReducer.js | 35 +--
superset/assets/src/dashboard/actions.js | 127 --------
.../assets/src/dashboard/actions/dashboardState.js | 166 ++++++++++
.../assets/src/dashboard/actions/datasources.js | 35 +++
.../assets/src/dashboard/actions/sliceEntities.js | 93 ++++++
.../src/dashboard/components/ActionMenuItem.jsx | 45 +++
.../assets/src/dashboard/components/Controls.jsx | 133 ++------
.../assets/src/dashboard/components/Dashboard.jsx | 301 +++++++-----------
.../dashboard/components/DashboardContainer.jsx | 52 +++-
.../assets/src/dashboard/components/GridCell.jsx | 49 +--
.../assets/src/dashboard/components/GridLayout.jsx | 210 +++++--------
.../assets/src/dashboard/components/Header.jsx | 164 ++++++----
.../dashboard/components/RefreshIntervalModal.jsx | 7 +-
.../assets/src/dashboard/components/SaveModal.jsx | 34 +-
.../assets/src/dashboard/components/SliceAdder.jsx | 341 ++++++++++-----------
.../dashboard/components/SliceAdderContainer.jsx | 25 ++
.../src/dashboard/components/SliceHeader.jsx | 123 ++------
.../dashboard/components/SliceHeaderControls.jsx | 106 +++++++
superset/assets/src/dashboard/index.jsx | 28 +-
superset/assets/src/dashboard/reducers.js | 214 -------------
.../src/dashboard/reducers/dashboardState.js | 128 ++++++++
.../assets/src/dashboard/reducers/datasources.js | 17 +
.../src/dashboard/reducers/getInitialState.js | 109 +++++++
superset/assets/src/dashboard/reducers/index.js | 22 ++
.../assets/src/dashboard/reducers/sliceEntities.js | 62 ++++
.../assets/src/dashboard/util/dashboardHelper.js | 9 +
.../src/dashboard/util/dashboardLayoutConverter.js | 322 +++++++++++++++++++
.../src/dashboard/v2/actions/messageToasts.js | 1 -
.../v2/components/BuilderComponentPane.jsx | 58 +++-
.../dashboard/v2/components/DashboardBuilder.jsx | 11 +-
.../src/dashboard/v2/components/DashboardGrid.jsx | 3 +-
.../dashboard/v2/components/DashboardHeader.jsx | 2 +-
.../v2/components/dnd/dragDroppableConfig.js | 1 +
.../src/dashboard/v2/components/dnd/handleDrop.js | 1 +
.../gridComponents/{Chart.jsx => ChartHolder.jsx} | 11 +-
.../v2/components/gridComponents/Column.jsx | 29 +-
.../dashboard/v2/components/gridComponents/Row.jsx | 29 +-
.../v2/components/gridComponents/index.js | 6 +-
.../dashboard/v2/containers/DashboardBuilder.jsx | 6 +-
.../dashboard/v2/containers/DashboardComponent.jsx | 13 +-
.../src/dashboard/v2/containers/DashboardGrid.jsx | 9 +-
.../dashboard/v2/containers/DashboardHeader.jsx | 43 ++-
superset/assets/src/dashboard/v2/reducers/index.js | 11 +-
.../dashboard/v2/stylesheets/builder-sidepane.less | 103 +++++++
.../src/dashboard/v2/stylesheets/builder.less | 3 +-
.../dashboard/v2/stylesheets/components/chart.less | 3 +-
superset/assets/src/dashboard/v2/util/constants.js | 2 +-
.../src/dashboard/v2/util/newComponentFactory.js | 3 +-
.../src/dashboard/v2/util/newEntitiesFromDrop.js | 3 +-
.../assets/src/dashboard/v2/util/propShapes.jsx | 50 ++-
.../src/explore/components/ExploreChartHeader.jsx | 6 +-
.../src/explore/components/ExploreChartPanel.jsx | 6 +-
.../explore/components/ExploreViewContainer.jsx | 16 +-
superset/assets/src/explore/exploreUtils.js | 2 +-
superset/assets/src/explore/index.jsx | 2 +-
superset/assets/src/explore/reducers/index.js | 5 +-
superset/assets/src/modules/utils.js | 1 -
superset/assets/src/visualizations/table.css | 9 +-
.../stylesheets/{dashboard.css => dashboard.less} | 116 +++----
superset/assets/stylesheets/superset.less | 16 +-
superset/models/core.py | 13 +-
superset/templates/superset/dashboard.html | 1 +
superset/views/core.py | 27 +-
71 files changed, 2299 insertions(+), 1465 deletions(-)
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 75f9504..d20dad7 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -102,6 +102,7 @@
"react-map-gl": "^3.0.4",
"react-redux": "^5.0.2",
"react-resizable": "^1.3.3",
+ "react-search-input": "^0.11.3",
"react-select": "1.2.1",
"react-select-fast-filter-options": "^0.2.1",
"react-sortable-hoc": "^0.8.3",
diff --git a/superset/assets/spec/javascripts/chart/Chart_spec.jsx b/superset/assets/spec/javascripts/chart/Chart_spec.jsx
index b766d9f..29a2941 100644
--- a/superset/assets/spec/javascripts/chart/Chart_spec.jsx
+++ b/superset/assets/spec/javascripts/chart/Chart_spec.jsx
@@ -20,7 +20,7 @@ describe('Chart', () => {
};
const mockedProps = {
...chart,
- chartKey: 'slice_223',
+ id: 223,
containerId: 'slice-container-223',
datasource: {},
formData: {},
diff --git a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
index c6e94d8..f4def13 100644
--- a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
@@ -1,61 +1,68 @@
+/* eslint camelcase: 0 */
import React from 'react';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';
-import * as dashboardActions from '../../../src/dashboard/actions';
+import * as sliceActions from '../../../src/dashboard/actions/sliceEntities';
+import * as dashboardActions from '../../../src/dashboard/actions/dashboardState';
import * as chartActions from '../../../src/chart/chartAction';
import Dashboard from '../../../src/dashboard/components/Dashboard';
-import { defaultFilters, dashboard, charts } from './fixtures';
+import { defaultFilters, dashboardState, dashboardInfo, dashboardLayout,
+ charts, datasources, sliceEntities } from './fixtures';
describe('Dashboard', () => {
const mockedProps = {
- actions: { ...chartActions, ...dashboardActions },
+ actions: { ...chartActions, ...dashboardActions, ...sliceActions },
initMessages: [],
- dashboard: dashboard.dashboard,
- slices: charts,
- filters: dashboard.filters,
- datasources: dashboard.datasources,
- refresh: false,
+ dashboardState,
+ dashboardInfo,
+ charts,
+ slices: sliceEntities.slices,
+ datasources,
+ layout: dashboardLayout.present,
timeout: 60,
- isStarred: false,
- userId: dashboard.userId,
+ userId: dashboardInfo.userId,
};
it('should render', () => {
const wrapper = shallow(<Dashboard {...mockedProps} />);
expect(wrapper.find('#dashboard-container')).to.have.length(1);
- expect(wrapper.instance().getAllSlices()).to.have.length(3);
+ expect(wrapper.instance().getAllCharts()).to.have.length(3);
});
it('should handle metadata default_filters', () => {
const wrapper = shallow(<Dashboard {...mockedProps} />);
- expect(wrapper.instance().props.filters).deep.equal(defaultFilters);
+ expect(wrapper.instance().props.dashboardState.filters).deep.equal(defaultFilters);
});
describe('getFormDataExtra', () => {
let wrapper;
- let selectedSlice;
+ let selectedChart;
beforeEach(() => {
wrapper = shallow(<Dashboard {...mockedProps} />);
- selectedSlice = wrapper.instance().props.dashboard.slices[1];
+ selectedChart = charts[248];
});
it('should carry default_filters', () => {
- const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
+ const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
expect(extraFilters[0]).to.deep.equal({ col: 'region', op: 'in', val: [] });
expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['United States'] });
});
it('should carry updated filter', () => {
- wrapper.setProps({
+ const newState = {
+ ...wrapper.props('dashboardState'),
filters: {
256: { region: [] },
257: { country_name: ['France'] },
},
+ };
+ wrapper.setProps({
+ dashboardState: newState,
});
- const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
+ const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['France'] });
});
});
@@ -65,7 +72,7 @@ describe('Dashboard', () => {
let spy;
beforeEach(() => {
wrapper = shallow(<Dashboard {...mockedProps} />);
- spy = sinon.spy(wrapper.instance(), 'fetchSlices');
+ spy = sinon.spy(mockedProps.actions, 'runQuery');
});
afterEach(() => {
spy.restore();
@@ -75,13 +82,13 @@ describe('Dashboard', () => {
const filterKey = Object.keys(defaultFilters)[1];
wrapper.instance().refreshExcept(filterKey);
expect(spy.callCount).to.equal(1);
- expect(spy.getCall(0).args[0].length).to.equal(1);
+ const slice_id = spy.getCall(0).args[0].slice_id;
+ expect(slice_id).to.equal(248);
});
it('should refresh all slices', () => {
wrapper.instance().refreshExcept();
- expect(spy.callCount).to.equal(1);
- expect(spy.getCall(0).args[0].length).to.equal(3);
+ expect(spy.callCount).to.equal(3);
});
});
@@ -94,7 +101,7 @@ describe('Dashboard', () => {
wrapper = shallow(<Dashboard {...mockedProps} />);
prevProp = wrapper.instance().props;
refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
- fetchSlicesStub = sinon.stub(wrapper.instance(), 'fetchSlices');
+ fetchSlicesStub = sinon.stub(mockedProps.actions, 'fetchCharts');
});
afterEach(() => {
fetchSlicesStub.restore();
@@ -106,48 +113,63 @@ describe('Dashboard', () => {
refreshExceptSpy.reset();
});
it('no change', () => {
- wrapper.setProps({
- refresh: true,
+ const newState = {
+ ...wrapper.props('dashboardState'),
filters: {
256: { region: [] },
257: { country_name: ['United States'] },
},
+ };
+ wrapper.setProps({
+ dashboardState: newState,
});
wrapper.instance().componentDidUpdate(prevProp);
expect(refreshExceptSpy.callCount).to.equal(0);
});
it('remove filter', () => {
- wrapper.setProps({
+ const newState = {
+ ...wrapper.props('dashboardState'),
refresh: true,
filters: {
256: { region: [] },
},
+ };
+ wrapper.setProps({
+ dashboardState: newState,
});
wrapper.instance().componentDidUpdate(prevProp);
expect(refreshExceptSpy.callCount).to.equal(1);
});
it('change filter', () => {
- wrapper.setProps({
+ const newState = {
+ ...wrapper.props('dashboardState'),
refresh: true,
filters: {
256: { region: [] },
257: { country_name: ['Canada'] },
},
+ };
+ wrapper.setProps({
+ dashboardState: newState,
});
wrapper.instance().componentDidUpdate(prevProp);
expect(refreshExceptSpy.callCount).to.equal(1);
});
it('add filter', () => {
- wrapper.setProps({
+ const newState = {
+ ...wrapper.props('dashboardState'),
refresh: true,
filters: {
256: { region: [] },
257: { country_name: ['Canada'] },
258: { another_filter: ['new'] },
},
+ };
+ wrapper.setProps({
+ dashboardState: newState,
});
wrapper.instance().componentDidUpdate(prevProp);
expect(refreshExceptSpy.callCount).to.equal(1);
@@ -155,28 +177,36 @@ describe('Dashboard', () => {
});
it('should refresh if refresh flag is true', () => {
- wrapper.setProps({
+ const newState = {
+ ...wrapper.props('dashboardState'),
refresh: true,
filters: {
256: { region: ['Asian'] },
},
+ };
+ wrapper.setProps({
+ dashboardState: newState,
});
wrapper.instance().componentDidUpdate(prevProp);
- const fetchArgs = fetchSlicesStub.lastCall.args[0];
- expect(fetchArgs).to.have.length(2);
+ expect(refreshExceptSpy.callCount).to.equal(1);
+ expect(refreshExceptSpy.lastCall.args[0]).to.equal('256');
});
it('should not refresh filter_immune_slices', () => {
- wrapper.setProps({
+ const newState = {
+ ...wrapper.props('dashboardState'),
refresh: true,
filters: {
256: { region: [] },
257: { country_name: ['Canada'] },
},
+ };
+ wrapper.setProps({
+ dashboardState: newState,
});
wrapper.instance().componentDidUpdate(prevProp);
- const fetchArgs = fetchSlicesStub.lastCall.args[0];
- expect(fetchArgs).to.have.length(1);
+ expect(refreshExceptSpy.callCount).to.equal(1);
+ expect(refreshExceptSpy.lastCall.args[0]).to.equal('257');
});
});
});
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures.jsx b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
index 371b02c..1565ccd 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures.jsx
+++ b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
@@ -1,4 +1,4 @@
-import { getInitialState } from '../../../src/dashboard/reducers';
+import getInitialState from '../../../src/dashboard/reducers/getInitialState';
export const defaultFilters = {
256: { region: [] },
@@ -118,7 +118,6 @@ export const slice = {
slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
};
-const datasources = {};
const mockDashboardData = {
css: '',
dash_edit_perm: true,
@@ -152,10 +151,13 @@ const mockDashboardData = {
slices: [regionFilter, slice, countryFilter],
standalone_mode: false,
};
-export const { dashboard, charts } = getInitialState({
+export const {
+ dashboardState, dashboardInfo,
+ charts, datasources, sliceEntities,
+ dashboardLayout } = getInitialState({
common: {},
dashboard_data: mockDashboardData,
- datasources,
+ datasources: {},
user_id: '1',
});
diff --git a/superset/assets/spec/javascripts/dashboard/reducers_spec.js b/superset/assets/spec/javascripts/dashboard/reducers_spec.js
index 6421fec..580a574 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers_spec.js
@@ -1,20 +1,23 @@
import { describe, it } from 'mocha';
import { expect } from 'chai';
-import { dashboard as reducers } from '../../../src/dashboard/reducers';
-import * as actions from '../../../src/dashboard/actions';
-import { defaultFilters, dashboard as initState } from './fixtures';
+import reducers from '../../../src/dashboard/reducers/dashboardState';
+import * as actions from '../../../src/dashboard/actions/dashboardState';
+import { defaultFilters, dashboardState as initState } from './fixtures';
describe('Dashboard reducers', () => {
+ it('should initialized', () => {
+ expect(initState.sliceIds.size).to.equal(3);
+ });
+
it('should remove slice', () => {
const action = {
type: actions.REMOVE_SLICE,
- slice: initState.dashboard.slices[1],
+ sliceId: 248,
};
- expect(initState.dashboard.slices).to.have.length(3);
- const { dashboard, filters, refresh } = reducers(initState, action);
- expect(dashboard.slices).to.have.length(2);
+ const { sliceIds, filters, refresh } = reducers(initState, action);
+ expect(sliceIds.size).to.be.equal(2);
expect(filters).to.deep.equal(defaultFilters);
expect(refresh).to.equal(false);
});
@@ -22,13 +25,13 @@ describe('Dashboard reducers', () => {
it('should remove filter slice', () => {
const action = {
type: actions.REMOVE_SLICE,
- slice: initState.dashboard.slices[0],
+ sliceId: 256,
};
const initFilters = Object.keys(initState.filters);
expect(initFilters).to.have.length(2);
- const { dashboard, filters, refresh } = reducers(initState, action);
- expect(dashboard.slices).to.have.length(2);
+ const { sliceIds, filters, refresh } = reducers(initState, action);
+ expect(sliceIds.size).to.equal(2);
expect(Object.keys(filters)).to.have.length(1);
expect(refresh).to.equal(true);
});
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index f223174..e9f7c63 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -17,7 +17,7 @@ import './chart.css';
const propTypes = {
annotationData: PropTypes.object,
actions: PropTypes.object,
- chartKey: PropTypes.string.isRequired,
+ chartId: PropTypes.number.isRequired,
containerId: PropTypes.string.isRequired,
datasource: PropTypes.object.isRequired,
formData: PropTypes.object.isRequired,
@@ -42,7 +42,6 @@ const propTypes = {
// dashboard callbacks
addFilter: PropTypes.func,
getFilters: PropTypes.func,
- clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
onQuery: PropTypes.func,
onDismissRefreshOverlay: PropTypes.func,
@@ -51,7 +50,6 @@ const propTypes = {
const defaultProps = {
addFilter: () => ({}),
getFilters: () => ({}),
- clearFilter: () => ({}),
removeFilter: () => ({}),
};
@@ -67,7 +65,6 @@ class Chart extends React.PureComponent {
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.headerHeight = this.headerHeight.bind(this);
this.height = this.height.bind(this);
@@ -78,7 +75,7 @@ class Chart extends React.PureComponent {
if (this.props.triggerQuery) {
this.props.actions.runQuery(this.props.formData, false,
this.props.timeout,
- this.props.chartKey,
+ this.props.chartId,
);
}
}
@@ -92,15 +89,14 @@ class Chart extends React.PureComponent {
}
componentDidUpdate(prevProps) {
- if (
- this.props.queryResponse &&
- ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
- !this.props.queryResponse.error && (
- prevProps.annotationData !== this.props.annotationData ||
- prevProps.queryResponse !== this.props.queryResponse ||
- prevProps.height !== this.props.height ||
- prevProps.width !== this.props.width ||
- prevProps.lastRendered !== this.props.lastRendered)
+ if (this.props.queryResponse &&
+ ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
+ !this.props.queryResponse.error && (
+ prevProps.annotationData !== this.props.annotationData ||
+ prevProps.queryResponse !== this.props.queryResponse ||
+ prevProps.height !== this.props.height ||
+ prevProps.width !== this.props.width ||
+ prevProps.lastRendered !== this.props.lastRendered)
) {
this.renderViz();
}
@@ -118,10 +114,6 @@ class Chart extends React.PureComponent {
this.props.addFilter(col, vals, merge, refresh);
}
- clearFilter() {
- this.props.clearFilter();
- }
-
removeFilter(col, vals, refresh = true) {
this.props.removeFilter(col, vals, refresh);
}
@@ -150,7 +142,7 @@ class Chart extends React.PureComponent {
}
error(e) {
- this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+ this.props.actions.chartRenderingFailed(e, this.props.chartId);
}
verboseMetricName(metric) {
@@ -198,21 +190,21 @@ class Chart extends React.PureComponent {
// [re]rendering the visualization
viz(this, qr, this.props.setControlValue);
Logger.append(LOG_ACTIONS_RENDER_EVENT, {
- label: this.props.chartKey,
+ label: 'slice_' + this.props.chartId,
vis_type: this.props.vizType,
start_offset: renderStart,
duration: Logger.getTimestamp() - renderStart,
});
- this.props.actions.chartRenderingSucceeded(this.props.chartKey);
+ this.props.actions.chartRenderingSucceeded(this.props.chartId);
} catch (e) {
- this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+ this.props.actions.chartRenderingFailed(e, this.props.chartId);
}
}
render() {
const isLoading = this.props.chartStatus === 'loading';
return (
- <div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
+ <div className={`${isLoading ? 'is-loading' : ''}`}>
{this.renderTooltip()}
{isLoading &&
<Loading size={25} />
diff --git a/superset/assets/src/chart/ChartContainer.jsx b/superset/assets/src/chart/ChartContainer.jsx
index b731412..e3cb1f9 100644
--- a/superset/assets/src/chart/ChartContainer.jsx
+++ b/superset/assets/src/chart/ChartContainer.jsx
@@ -5,7 +5,7 @@ import * as Actions from './chartAction';
import Chart from './Chart';
function mapStateToProps({ charts }, ownProps) {
- const chart = charts[ownProps.chartKey];
+ const chart = charts[ownProps.chartId];
return {
annotationData: chart.annotationData,
chartAlert: chart.chartAlert,
diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js
index cb24f65..33f49d1 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -117,6 +117,11 @@ export function updateQueryFormData(value, key) {
return { type: UPDATE_QUERY_FORM_DATA, value, key };
}
+export const ADD_CHART = 'ADD_CHART';
+export function addChart(chart, key) {
+ return { type: ADD_CHART, chart, key };
+}
+
export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, force = false, timeout = 60, key) {
return (dispatch) => {
@@ -139,7 +144,7 @@ export function runQuery(formData, force = false, timeout = 60, key) {
.then(() => queryRequest)
.then((queryResponse) => {
Logger.append(LOG_ACTIONS_LOAD_EVENT, {
- label: key,
+ label: 'slice_' + key,
is_cached: queryResponse.is_cached,
row_count: queryResponse.rowcount,
datasource: formData.datasource,
@@ -190,3 +195,10 @@ export function runQuery(formData, force = false, timeout = 60, key) {
]);
};
}
+
+export function refreshChart(chart, force, timeout) {
+ return dispatch => (
+ dispatch(runQuery(chart.latestQueryFormData, force, timeout, chart.id))
+ );
+}
+
diff --git a/superset/assets/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js
index f68a5b8..d57959a 100644
--- a/superset/assets/src/chart/chartReducer.js
+++ b/superset/assets/src/chart/chartReducer.js
@@ -1,25 +1,10 @@
/* 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,
- queryRequest: PropTypes.object,
- queryResponse: PropTypes.object,
- triggerQuery: PropTypes.bool,
- lastRendered: PropTypes.number,
-};
-
export const chart = {
- chartKey: '',
+ id: 0,
chartAlert: null,
chartStatus: 'loading',
chartUpdateEndTime: null,
@@ -33,6 +18,12 @@ export const chart = {
export default function chartReducer(charts = {}, action) {
const actionHandlers = {
+ [actions.ADD_CHART]() {
+ return {
+ ...chart,
+ ...action.chart,
+ };
+ },
[actions.CHART_UPDATE_SUCCEEDED](state) {
return { ...state,
chartStatus: 'success',
@@ -70,12 +61,12 @@ export default function chartReducer(charts = {}, action) {
return { ...state,
chartStatus: 'failed',
chartAlert: (
- `${t('Query timeout')} - ` +
- 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.')),
+ `${t('Query timeout')} - ` +
+ 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) {
diff --git a/superset/assets/src/dashboard/actions.js b/superset/assets/src/dashboard/actions.js
deleted file mode 100644
index c7f1a6a..0000000
--- a/superset/assets/src/dashboard/actions.js
+++ /dev/null
@@ -1,127 +0,0 @@
-/* global notify */
-import $ from 'jquery';
-import { getExploreUrlAndPayload } 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, refresh = true) {
- return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
-}
-
-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 { url, payload } = getExploreUrlAndPayload({
- formData: slice.form_data,
- endpointType: 'base',
- force: false,
- curUrl: null,
- requestParams: sliceParams,
- });
- return $.ajax({
- url,
- type: 'POST',
- data: {
- form_data: JSON.stringify(payload),
- },
- 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 };
-}
-
-export const SET_EDIT_MODE = 'SET_EDIT_MODE';
-export function setEditMode(editMode) {
- return { type: SET_EDIT_MODE, editMode };
-}
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
new file mode 100644
index 0000000..2262729
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -0,0 +1,166 @@
+/* eslint camelcase: 0 */
+import $ from 'jquery';
+
+import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
+import { chart as initChart } from '../../chart/chartReducer';
+import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
+import { applyDefaultFormData } from '../../explore/stores/store';
+
+export const ADD_FILTER = 'ADD_FILTER';
+export function addFilter(chart, col, vals, merge = true, refresh = true) {
+ return { type: ADD_FILTER, chart, col, vals, merge, refresh };
+}
+
+export const REMOVE_FILTER = 'REMOVE_FILTER';
+export function removeFilter(sliceId, col, vals, refresh = true) {
+ return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
+}
+
+export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
+export function updateDashboardTitle(title) {
+ return { type: UPDATE_DASHBOARD_TITLE, title };
+}
+
+export const ADD_SLICE = 'ADD_SLICE';
+export function addSlice(slice) {
+ return { type: ADD_SLICE, slice };
+}
+
+export const REMOVE_SLICE = 'REMOVE_SLICE';
+export function removeSlice(sliceId) {
+ return { type: REMOVE_SLICE, sliceId };
+}
+
+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(sliceId) {
+ return { type: TOGGLE_EXPAND_SLICE, sliceId };
+}
+
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export function setEditMode(editMode) {
+ return { type: SET_EDIT_MODE, editMode };
+}
+
+export const ON_CHANGE = 'ON_CHANGE';
+export function onChange() {
+ return { type: ON_CHANGE };
+}
+
+export const ON_SAVE = 'ON_SAVE';
+export function onSave() {
+ return { type: ON_SAVE };
+}
+
+export function fetchCharts(chartList = [], force = false, interval = 0) {
+ return (dispatch, getState) => {
+ const timeout = getState().dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
+ if (!interval) {
+ chartList.forEach(chart => (dispatch(refreshChart(chart, force, timeout))));
+ return;
+ }
+
+ const { metadata: meta } = getState().dashboardInfo;
+ 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 / (chartList.length - 1) : 0;
+ chartList.forEach((chart, i) => {
+ setTimeout(() => dispatch(refreshChart(chart, force, timeout)), delay * i);
+ });
+ };
+}
+
+let refreshTimer = null;
+export function startPeriodicRender(interval) {
+ const stopPeriodicRender = () => {
+ if (refreshTimer) {
+ clearTimeout(refreshTimer);
+ refreshTimer = null;
+ }
+ };
+
+ return (dispatch, getState) => {
+ stopPeriodicRender();
+
+ const { metadata } = getState().dashboardInfo;
+ const immune = metadata.timed_refresh_immune_slices || [];
+ const refreshAll = () => {
+ const affected =
+ Object.values(getState().charts)
+ .filter(chart => immune.indexOf(chart.id) === -1);
+ return dispatch(fetchCharts(affected, true, interval * 0.2));
+ };
+ const fetchAndRender = () => {
+ refreshAll();
+ if (interval > 0) {
+ refreshTimer = setTimeout(fetchAndRender, interval);
+ }
+ };
+
+ fetchAndRender();
+ };
+}
+
+export const TOGGLE_BUILDER_PANE = 'TOGGLE_BUILDER_PANE';
+export function toggleBuilderPane() {
+ return { type: TOGGLE_BUILDER_PANE };
+}
+
+export function addSliceToDashboard(id) {
+ return (dispatch, getState) => {
+ const { sliceEntities } = getState();
+ const selectedSlice = sliceEntities.slices[id];
+ const form_data = selectedSlice.form_data;
+ const newChart = {
+ ...initChart,
+ id,
+ form_data,
+ formData: applyDefaultFormData(form_data),
+ };
+
+ return Promise
+ .all([
+ dispatch(addChart(newChart, id)),
+ dispatch(fetchDatasourceMetadata(form_data.datasource)),
+ ])
+ .then(() => dispatch(addSlice(selectedSlice)));
+ };
+}
+
+export function removeSliceFromDashboard(chart) {
+ return (dispatch) => {
+ dispatch(removeSlice(chart.id));
+ dispatch(removeChart(chart.id));
+ };
+}
diff --git a/superset/assets/src/dashboard/actions/datasources.js b/superset/assets/src/dashboard/actions/datasources.js
new file mode 100644
index 0000000..a00bb17
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/datasources.js
@@ -0,0 +1,35 @@
+import $ from 'jquery';
+
+export const SET_DATASOURCE = 'SET_DATASOURCE';
+export function setDatasource(datasource, key) {
+ return { type: SET_DATASOURCE, datasource, key };
+}
+
+export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
+export function fetchDatasourceStarted(key) {
+ return { type: FETCH_DATASOURCE_STARTED, key };
+}
+
+export const FETCH_DATASOURCE_FAILED = 'FETCH_DATASOURCE_FAILED';
+export function fetchDatasourceFailed(error, key) {
+ return { type: FETCH_DATASOURCE_FAILED, error, key };
+}
+
+export function fetchDatasourceMetadata(key) {
+ return (dispatch, getState) => {
+ const { datasources } = getState();
+ const datasource = datasources[key];
+
+ if (datasource) {
+ return dispatch(setDatasource(datasource, key));
+ }
+
+ const url = `/superset/fetch_datasource_metadata?datasourceKey=${key}`;
+ return $.ajax({
+ type: 'GET',
+ url,
+ success: data => dispatch(setDatasource(data, key)),
+ error: error => dispatch(fetchDatasourceFailed(error.responseJSON.error, key)),
+ });
+ };
+}
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
new file mode 100644
index 0000000..3a1b1dc
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -0,0 +1,93 @@
+/* eslint camelcase: 0 */
+/* global notify */
+import $ from 'jquery';
+
+export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
+export function updateSliceName(key, sliceName) {
+ return { type: UPDATE_SLICE_NAME, key, sliceName };
+}
+
+export function saveSliceName(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 url = slice.slice_url + '&' +
+ Object.keys(sliceParams)
+ .map(key => (key + '=' + sliceParams[key]))
+ .join('&');
+ const key = slice.slice_id;
+ return $.ajax({
+ url,
+ type: 'POST',
+ success: () => {
+ dispatch(updateSliceName(key, sliceName));
+ notify.success('This slice name was saved successfully.');
+ },
+ error: () => {
+ // if server-side reject the overwrite action,
+ // revert to old state
+ dispatch(updateSliceName(key, oldName));
+ notify.error("You don't have the rights to alter this slice");
+ },
+ });
+ };
+}
+
+export const SET_ALL_SLICES = 'SET_ALL_SLICES';
+export function setAllSlices(slices) {
+ return { type: SET_ALL_SLICES, slices };
+}
+
+export const FETCH_ALL_SLICES_STARTED = 'FETCH_ALL_SLICES_STARTED';
+export function fetchAllSlicesStarted() {
+ return { type: FETCH_ALL_SLICES_STARTED };
+}
+
+export const FETCH_ALL_SLICES_FAILED = 'FETCH_ALL_SLICES_FAILED';
+export function fetchAllSlicesFailed(error) {
+ return { type: FETCH_ALL_SLICES_FAILED, error };
+}
+
+export function fetchAllSlices(userId) {
+ return (dispatch, getState) => {
+ const { sliceEntities } = getState();
+ if (sliceEntities.lastUpdated === 0) {
+ dispatch(fetchAllSlicesStarted());
+
+ const uri = `/sliceaddview/api/read?_flt_0_created_by=${userId}`;
+ return $.ajax({
+ url: uri,
+ type: 'GET',
+ success: (response) => {
+ const slices = {};
+ response.result.forEach((slice) => {
+ const form_data = JSON.parse(slice.params);
+ slices[slice.id] = {
+ slice_id: slice.id,
+ slice_url: slice.slice_url,
+ slice_name: slice.slice_name,
+ edit_url: slice.edit_url,
+ form_data,
+ datasource: form_data.datasource,
+ datasource_name: slice.datasource_name_text,
+ datasource_link: slice.datasource_link,
+ changed_on: new Date(slice.changed_on).getTime(),
+ description: slice.description,
+ description_markdown: slice.description_markeddown,
+ viz_type: slice.viz_type,
+ modified: slice.modified,
+ };
+ });
+ return dispatch(setAllSlices(slices));
+ },
+ error: error => dispatch(fetchAllSlicesFailed(error)),
+ });
+ }
+
+ return dispatch(setAllSlices(sliceEntities.slices));
+ };
+}
diff --git a/superset/assets/src/dashboard/components/ActionMenuItem.jsx b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
new file mode 100644
index 0000000..aaae4df
--- /dev/null
+++ b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { MenuItem } from 'react-bootstrap';
+
+import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+
+export function MenuItemContent({ faIcon, text, tooltip, children }) {
+ return (
+ <span>
+ {faIcon &&
+ <i className={`fa fa-${faIcon}`}> </i>
+ }
+ {text} {''}
+ <InfoTooltipWithTrigger
+ tooltip={tooltip}
+ label={faIcon ? `dash-${faIcon}` : ''}
+ placement="top"
+ />
+ {children}
+ </span>
+ );
+}
+MenuItemContent.propTypes = {
+ faIcon: PropTypes.string,
+ text: PropTypes.string,
+ tooltip: PropTypes.string,
+ children: PropTypes.node,
+};
+
+export function ActionMenuItem(props) {
+ return (
+ <MenuItem
+ onClick={props.onClick}
+ href={props.href}
+ target={props.target}
+ >
+ <MenuItemContent {...props} />
+ </MenuItem>
+ );
+}
+ActionMenuItem.propTypes = {
+ onClick: PropTypes.func,
+ href: PropTypes.string,
+ target: PropTypes.string,
+};
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index baad7bb..8755e8f 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -1,69 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { DropdownButton, MenuItem } from 'react-bootstrap';
+import $ from 'jquery';
+import { DropdownButton } from 'react-bootstrap';
-import CssEditor from './CssEditor';
import RefreshIntervalModal from './RefreshIntervalModal';
import SaveModal from './SaveModal';
-import SliceAdder from './SliceAdder';
+import { ActionMenuItem, MenuItemContent } from './ActionMenuItem';
import { t } from '../../locales';
-import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
-
-const $ = window.$ = require('jquery');
const propTypes = {
- dashboard: PropTypes.object.isRequired,
+ dashboardInfo: PropTypes.object.isRequired,
+ dashboardTitle: PropTypes.string.isRequired,
+ layout: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
+ expandedSlices: PropTypes.object.isRequired,
slices: PropTypes.array,
- userId: PropTypes.string.isRequired,
- addSlicesToDashboard: PropTypes.func,
onSave: PropTypes.func,
onChange: PropTypes.func,
- renderSlices: PropTypes.func,
- serialize: PropTypes.func,
+ forceRefreshAllCharts: PropTypes.func,
startPeriodicRender: PropTypes.func,
editMode: PropTypes.bool,
};
-function MenuItemContent({ faIcon, text, tooltip, children }) {
- return (
- <span>
- <i className={`fa fa-${faIcon}`} /> {text} {''}
- <InfoTooltipWithTrigger
- tooltip={tooltip}
- label={`dash-${faIcon}`}
- placement="top"
- />
- {children}
- </span>
- );
-}
-MenuItemContent.propTypes = {
- faIcon: PropTypes.string.isRequired,
- text: PropTypes.string,
- tooltip: PropTypes.string,
- children: PropTypes.node,
-};
-
-function ActionMenuItem(props) {
- return (
- <MenuItem onClick={props.onClick}>
- <MenuItemContent {...props} />
- </MenuItem>
- );
-}
-ActionMenuItem.propTypes = {
- onClick: PropTypes.func,
-};
-
class Controls extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
- css: props.dashboard.css || '',
+ css: '',
cssTemplates: [],
};
- this.refresh = this.refresh.bind(this);
this.toggleModal = this.toggleModal.bind(this);
this.updateDom = this.updateDom.bind(this);
}
@@ -79,10 +44,6 @@ class Controls extends React.PureComponent {
this.setState({ cssTemplates });
});
}
- refresh() {
- // Force refresh all slices
- this.props.renderSlices(true);
- }
toggleModal(modal) {
let currentModal;
if (modal !== this.state.currentModal) {
@@ -114,12 +75,12 @@ class Controls extends React.PureComponent {
}
}
render() {
- const { dashboard, userId, filters,
- addSlicesToDashboard, startPeriodicRender,
- serialize, onSave, editMode } = this.props;
+ const { dashboardTitle, layout, filters, expandedSlices,
+ startPeriodicRender, forceRefreshAllCharts, onSave,
+ editMode } = this.props;
const emailBody = t('Checkout this dashboard: %s', window.location.href);
const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
- + `${dashboard.dashboard_title}&Body=${emailBody}`;
+ + `${dashboardTitle}&Body=${emailBody}`;
let saveText = t('Save as');
if (editMode) {
saveText = t('Save');
@@ -130,8 +91,7 @@ class Controls extends React.PureComponent {
<ActionMenuItem
text={t('Force Refresh')}
tooltip={t('Force refresh the whole dashboard')}
- faIcon="refresh"
- onClick={this.refresh}
+ onClick={forceRefreshAllCharts}
/>
<RefreshIntervalModal
onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
@@ -139,32 +99,29 @@ class Controls extends React.PureComponent {
<MenuItemContent
text={t('Set autorefresh')}
tooltip={t('Set the auto-refresh interval for this session')}
- faIcon="clock-o"
/>
}
/>
- {dashboard.dash_save_perm &&
- <SaveModal
- dashboard={dashboard}
- filters={filters}
- serialize={serialize}
- onSave={onSave}
- css={this.state.css}
- triggerNode={
- <MenuItemContent
- text={saveText}
- tooltip={t('Save the dashboard')}
- faIcon="save"
- />
- }
- />
- }
+ <SaveModal
+ dashboardId={this.props.dashboardInfo.id}
+ dashboardTitle={dashboardTitle}
+ layout={layout}
+ filters={filters}
+ expandedSlices={expandedSlices}
+ onSave={onSave}
+ css={this.state.css}
+ triggerNode={
+ <MenuItemContent
+ text={saveText}
+ tooltip={t('Save the dashboard')}
+ />
+ }
+ />
{editMode &&
<ActionMenuItem
text={t('Edit properties')}
tooltip={t("Edit the dashboards's properties")}
- faIcon="edit"
- onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }}
+ onClick={() => { window.location = `/dashboardmodelview/edit/${this.props.dashboardInfo.id}`; }}
/>
}
{editMode &&
@@ -172,36 +129,6 @@ class Controls extends React.PureComponent {
text={t('Email')}
tooltip={t('Email a link to this dashboard')}
onClick={() => { window.location = emailLink; }}
- faIcon="envelope"
- />
- }
- {editMode &&
- <SliceAdder
- dashboard={dashboard}
- addSlicesToDashboard={addSlicesToDashboard}
- userId={userId}
- triggerNode={
- <MenuItemContent
- text={t('Add Charts')}
- tooltip={t('Add some charts to this dashboard')}
- faIcon="plus"
- />
- }
- />
- }
- {editMode &&
- <CssEditor
- dashboard={dashboard}
- triggerNode={
- <MenuItemContent
- text={t('Edit CSS')}
- tooltip={t('Change the style of the dashboard using CSS code')}
- faIcon="css3"
- />
- }
- initialCss={this.state.css}
- templates={this.state.cssTemplates}
- onChange={this.changeCss.bind(this)}
/>
}
</DropdownButton>
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 2cb08c3..939476c 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -3,81 +3,69 @@ import PropTypes from 'prop-types';
import AlertsWrapper from '../../components/AlertsWrapper';
import GridLayout from './GridLayout';
-import Header from './Header';
+import {
+ chartPropShape,
+ slicePropShape,
+ dashboardInfoPropShape,
+ dashboardStatePropShape,
+} from '../v2/util/propShapes';
import { exportChart } from '../../explore/exploreUtils';
import { areObjectsEqual } from '../../reduxUtils';
+import { getChartIdsFromLayout } from '../util/dashboardHelper';
import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
import { t } from '../../locales';
-import '../../../stylesheets/dashboard.css';
+import '../../../stylesheets/dashboard.less';
+import '../v2/stylesheets/index.less';
const propTypes = {
- actions: PropTypes.object,
+ actions: PropTypes.object.isRequired,
+ dashboardInfo: dashboardInfoPropShape.isRequired,
+ dashboardState: dashboardStatePropShape.isRequired,
+ charts: PropTypes.objectOf(chartPropShape).isRequired,
+ slices: PropTypes.objectOf(slicePropShape).isRequired,
+ datasources: PropTypes.object.isRequired,
+ layout: PropTypes.object.isRequired,
+ impressionId: PropTypes.string.isRequired,
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,
- editMode: PropTypes.bool,
- impressionId: PropTypes.string,
};
const defaultProps = {
initMessages: [],
- dashboard: {},
- slices: {},
- datasources: {},
- filters: {},
- refresh: false,
timeout: 60,
userId: '',
- isStarred: false,
- editMode: false,
};
class Dashboard extends React.PureComponent {
constructor(props) {
super(props);
- this.refreshTimer = null;
+
this.firstLoad = true;
this.loadingLog = new ActionLog({
impressionId: props.impressionId,
actionType: LOG_ACTIONS_PAGE_LOAD,
source: 'dashboard',
- sourceId: props.dashboard.id,
+ sourceId: props.dashboardInfo.id,
eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
});
Logger.start(this.loadingLog);
- // alert for unsaved changes
- this.state = { unsavedChanges: 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.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.getFilters = this.getFilters.bind(this);
+ this.refreshExcept = this.refreshExcept.bind(this);
this.getFormDataExtra = this.getFormDataExtra.bind(this);
this.exploreChart = this.exploreChart.bind(this);
this.exportCSV = this.exportCSV.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.saveSliceName = this.props.actions.saveSliceName.bind(this);
+ this.props.actions.removeSliceFromDashboard =
+ this.props.actions.removeSliceFromDashboard.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);
}
@@ -87,22 +75,37 @@ class Dashboard extends React.PureComponent {
componentWillReceiveProps(nextProps) {
if (this.firstLoad &&
- Object.values(nextProps.slices)
- .every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
+ Object.values(nextProps.charts)
+ .every(chart => (['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1))
) {
Logger.end(this.loadingLog);
this.firstLoad = false;
}
+
+ const currentChartIds = getChartIdsFromLayout(this.props.layout);
+ const nextChartIds = getChartIdsFromLayout(nextProps.layout);
+ if (currentChartIds.length < nextChartIds.length) {
+ // adding new chart
+ const newChartId = nextChartIds.find(key => (currentChartIds.indexOf(key) === -1));
+ this.props.actions.addSliceToDashboard(newChartId);
+ this.props.actions.onChange();
+ } else if (currentChartIds.length > nextChartIds.length) {
+ // remove chart
+ const removedChartId = currentChartIds.find(key => (nextChartIds.indexOf(key) === -1));
+ this.props.actions.removeSliceFromDashboard(this.props.charts[removedChartId]);
+ this.props.actions.onChange();
+ }
}
componentDidUpdate(prevProps) {
- if (this.props.refresh) {
+ const { refresh, filters, hasUnsavedChanges } = this.props.dashboardState;
+ if (refresh) {
let changedFilterKey;
- const prevFiltersKeySet = new Set(Object.keys(prevProps.filters));
- Object.keys(this.props.filters).some((key) => {
+ const prevFiltersKeySet = new Set(Object.keys(prevProps.dashboardState.filters));
+ Object.keys(filters).some((key) => {
prevFiltersKeySet.delete(key);
- if (prevProps.filters[key] === undefined ||
- !areObjectsEqual(prevProps.filters[key], this.props.filters[key])) {
+ if (prevProps.dashboardState.filters[key] === undefined ||
+ !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])) {
changedFilterKey = key;
return true;
}
@@ -113,6 +116,12 @@ class Dashboard extends React.PureComponent {
this.refreshExcept(changedFilterKey);
}
}
+
+ if (hasUnsavedChanges) {
+ this.onBeforeUnload(true);
+ } else {
+ this.onBeforeUnload(false);
+ }
}
componentWillUnmount() {
@@ -127,29 +136,22 @@ class Dashboard extends React.PureComponent {
}
}
- onChange() {
- this.onBeforeUnload(true);
- this.setState({ unsavedChanges: true });
- }
-
- onSave() {
- this.onBeforeUnload(false);
- this.setState({ unsavedChanges: false });
- }
-
// return charts in array
- getAllSlices() {
- return Object.values(this.props.slices);
+ getAllCharts() {
+ return Object.values(this.props.charts);
}
- getFormDataExtra(slice) {
- const formDataExtra = Object.assign({}, slice.formData);
- formDataExtra.extra_filters = this.effectiveExtraFilters(slice.slice_id);
+ getFormDataExtra(chart) {
+ const extraFilters = this.effectiveExtraFilters(chart.id);
+ const formDataExtra = {
+ ...chart.formData,
+ extra_filters: extraFilters,
+ };
return formDataExtra;
}
getFilters(sliceId) {
- return this.props.filters[sliceId];
+ return this.props.dashboardState.filters[sliceId];
}
unload() {
@@ -159,8 +161,8 @@ class Dashboard extends React.PureComponent {
}
effectiveExtraFilters(sliceId) {
- const metadata = this.props.dashboard.metadata;
- const filters = this.props.filters;
+ const metadata = this.props.dashboardInfo.metadata;
+ const filters = this.props.dashboardState.filters;
const f = [];
const immuneSlices = metadata.filter_immune_slices || [];
if (sliceId && immuneSlices.includes(sliceId)) {
@@ -195,154 +197,75 @@ class Dashboard extends React.PureComponent {
}
refreshExcept(filterKey) {
- const immune = this.props.dashboard.metadata.filter_immune_slices || [];
- let slices = this.getAllSlices();
+ const immune = this.props.dashboardInfo.metadata.filter_immune_slices || [];
+ let charts = this.getAllCharts();
if (filterKey) {
- slices = slices.filter(slice => (
- String(slice.slice_id) !== filterKey &&
- 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();
- }
-
- 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;
+ charts = charts.filter(
+ chart => (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1),
+ );
}
-
- 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);
+ charts.forEach((chart) => {
+ const updatedFormData = this.getFormDataExtra(chart);
+ this.props.actions.runQuery(updatedFormData, false, this.props.timeout, chart.id);
});
}
- exploreChart(slice) {
- const formData = this.getFormDataExtra(slice);
+ exploreChart(chartId) {
+ const chart = this.props.charts[chartId];
+ const formData = this.getFormDataExtra(chart);
exportChart(formData);
}
- exportCSV(slice) {
- const formData = this.getFormDataExtra(slice);
+ exportCSV(chartId) {
+ const chart = this.props.charts[chartId];
+ const formData = this.getFormDataExtra(chart);
exportChart(formData, 'csv');
}
// re-render chart without fetch
rerenderCharts() {
- this.getAllSlices().forEach((slice) => {
+ this.getAllCharts().forEach((chart) => {
setTimeout(() => {
- this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
+ this.props.actions.renderTriggered(new Date().getTime(), chart.id);
}, 50);
});
}
render() {
+ const {
+ expandedSlices = {}, filters, sliceIds,
+ editMode, showBuilderPane,
+ } = this.props.dashboardState;
+
return (
<div id="dashboard-container">
- <div id="dashboard-header">
+ <div>
<AlertsWrapper initMessages={this.props.initMessages} />
- <Header
- dashboard={this.props.dashboard}
- unsavedChanges={this.state.unsavedChanges}
- filters={this.props.filters}
- userId={this.props.userId}
- isStarred={this.props.isStarred}
- updateDashboardTitle={this.updateDashboardTitle}
- onSave={this.onSave}
- onChange={this.onChange}
- serialize={this.serialize}
- fetchFaveStar={this.props.actions.fetchFaveStar}
- saveFaveStar={this.props.actions.saveFaveStar}
- renderSlices={this.fetchAllSlices}
- startPeriodicRender={this.startPeriodicRender}
- addSlicesToDashboard={this.addSlicesToDashboard}
- editMode={this.props.editMode}
- setEditMode={this.props.actions.setEditMode}
- />
- </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}
- exploreChart={this.exploreChart}
- exportCSV={this.exportCSV}
- 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}
- editMode={this.props.editMode}
- />
</div>
+ <GridLayout
+ dashboardInfo={this.props.dashboardInfo}
+ layout={this.props.layout}
+ datasources={this.props.datasources}
+ slices={this.props.slices}
+ sliceIds={sliceIds}
+ expandedSlices={expandedSlices}
+ filters={filters}
+ charts={this.props.charts}
+ timeout={this.props.timeout}
+ onChange={this.onChange}
+ rerenderCharts={this.rerenderCharts}
+ getFormDataExtra={this.getFormDataExtra}
+ exploreChart={this.exploreChart}
+ exportCSV={this.exportCSV}
+ refreshChart={this.props.actions.refreshChart}
+ saveSliceName={this.props.actions.saveSliceName}
+ toggleExpandSlice={this.props.actions.toggleExpandSlice}
+ addFilter={this.props.actions.addFilter}
+ getFilters={this.getFilters}
+ removeFilter={this.props.actions.removeFilter}
+ editMode={editMode}
+ showBuilderPane={showBuilderPane}
+ />
</div>
);
}
diff --git a/superset/assets/src/dashboard/components/DashboardContainer.jsx b/superset/assets/src/dashboard/components/DashboardContainer.jsx
index d429461..31fe035 100644
--- a/superset/assets/src/dashboard/components/DashboardContainer.jsx
+++ b/superset/assets/src/dashboard/components/DashboardContainer.jsx
@@ -1,28 +1,48 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import * as dashboardActions from '../actions';
-import * as chartActions from '../../chart/chartAction';
-import Dashboard from '../v2/components/Dashboard';
+import {
+ toggleExpandSlice,
+ addFilter,
+ removeFilter,
+ addSliceToDashboard,
+ removeSliceFromDashboard,
+ onChange,
+} from '../actions/dashboardState';
+import { saveSliceName } from '../actions/sliceEntities';
+import { refreshChart, runQuery, renderTriggered } from '../../chart/chartAction';
+import Dashboard from './Dashboard';
-function mapStateToProps(/* { charts, dashboard, impressionId } */) {
+function mapStateToProps({ datasources, sliceEntities, charts,
+ dashboardInfo, dashboardState,
+ dashboardLayout, impressionId }) {
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,
- // editMode: dashboard.editMode,
- // impressionId,
+ initMessages: dashboardInfo.common.flash_messages,
+ timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+ userId: dashboardInfo.userId,
+ dashboardInfo,
+ dashboardState,
+ charts,
+ datasources,
+ slices: sliceEntities.slices,
+ layout: dashboardLayout.present,
+ impressionId,
};
}
function mapDispatchToProps(dispatch) {
- const actions = { ...chartActions, ...dashboardActions };
+ const actions = {
+ refreshChart,
+ runQuery,
+ renderTriggered,
+ saveSliceName,
+ toggleExpandSlice,
+ addFilter,
+ removeFilter,
+ addSliceToDashboard,
+ removeSliceFromDashboard,
+ onChange,
+ };
return {
actions: bindActionCreators(actions, dispatch),
};
diff --git a/superset/assets/src/dashboard/components/GridCell.jsx b/superset/assets/src/dashboard/components/GridCell.jsx
index 91fe839..3273272 100644
--- a/superset/assets/src/dashboard/components/GridCell.jsx
+++ b/superset/assets/src/dashboard/components/GridCell.jsx
@@ -4,8 +4,7 @@ import PropTypes from 'prop-types';
import SliceHeader from './SliceHeader';
import ChartContainer from '../../chart/ChartContainer';
-
-import '../../../stylesheets/dashboard.css';
+import { chartPropShape, slicePropShape } from '../v2/util/propShapes';
const propTypes = {
timeout: PropTypes.number,
@@ -16,34 +15,30 @@ const propTypes = {
isExpanded: PropTypes.bool,
widgetHeight: PropTypes.number,
widgetWidth: PropTypes.number,
- slice: PropTypes.object,
- chartKey: PropTypes.string,
+ slice: slicePropShape.isRequired,
+ chart: chartPropShape.isRequired,
formData: PropTypes.object,
filters: PropTypes.object,
- forceRefresh: PropTypes.func,
- removeSlice: PropTypes.func,
+ refreshChart: PropTypes.func,
updateSliceName: PropTypes.func,
toggleExpandSlice: PropTypes.func,
exploreChart: PropTypes.func,
exportCSV: PropTypes.func,
addFilter: PropTypes.func,
getFilters: PropTypes.func,
- clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
editMode: PropTypes.bool,
annotationQuery: PropTypes.object,
};
const defaultProps = {
- forceRefresh: () => ({}),
- removeSlice: () => ({}),
+ refreshChart: () => ({}),
updateSliceName: () => ({}),
toggleExpandSlice: () => ({}),
exploreChart: () => ({}),
exportCSV: () => ({}),
addFilter: () => ({}),
getFilters: () => ({}),
- clearFilter: () => ({}),
removeFilter: () => ({}),
editMode: false,
};
@@ -53,9 +48,9 @@ class GridCell extends React.PureComponent {
super(props);
const sliceId = this.props.slice.slice_id;
- this.addFilter = this.props.addFilter.bind(this, sliceId);
+ this.forceRefresh = this.forceRefresh.bind(this);
+ this.addFilter = this.props.addFilter.bind(this, this.props.chart);
this.getFilters = this.props.getFilters.bind(this, sliceId);
- this.clearFilter = this.props.clearFilter.bind(this, sliceId);
this.removeFilter = this.props.removeFilter.bind(this, sliceId);
}
@@ -68,7 +63,7 @@ class GridCell extends React.PureComponent {
}
width() {
- return this.props.widgetWidth - 10;
+ return this.props.widgetWidth - 32;
}
height(slice) {
@@ -80,7 +75,7 @@ class GridCell extends React.PureComponent {
descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
}
- return widgetHeight - headerHeight - descriptionHeight;
+ return widgetHeight - headerHeight - descriptionHeight - 32;
}
headerHeight(slice) {
@@ -88,13 +83,18 @@ class GridCell extends React.PureComponent {
return this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
}
+ forceRefresh() {
+ return this.props.refreshChart(this.props.chart, true, this.props.timeout);
+ }
+
render() {
const {
isExpanded, isLoading, isCached, cachedDttm,
- removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
- chartKey, slice, datasource, formData, timeout, annotationQuery,
- exploreChart, exportCSV,
+ updateSliceName, toggleExpandSlice,
+ chart, slice, datasource, formData, timeout, annotationQuery,
+ exploreChart, exportCSV, editMode,
} = this.props;
+
return (
<div
className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
@@ -106,11 +106,10 @@ class GridCell extends React.PureComponent {
isExpanded={isExpanded}
isCached={isCached}
cachedDttm={cachedDttm}
- removeSlice={removeSlice}
updateSliceName={updateSliceName}
toggleExpandSlice={toggleExpandSlice}
- forceRefresh={forceRefresh}
- editMode={this.props.editMode}
+ forceRefresh={this.forceRefresh}
+ editMode={editMode}
annotationQuery={annotationQuery}
exploreChart={exploreChart}
exportCSV={exportCSV}
@@ -128,21 +127,23 @@ class GridCell extends React.PureComponent {
ref={this.getDescriptionId(slice)}
dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
/>
- <div className="row chart-container">
+ <div
+ className="chart-container"
+ style={{ width: this.width(), height: this.height(slice) }}
+ >
<input type="hidden" value="false" />
<ChartContainer
containerId={`slice-container-${slice.slice_id}`}
- chartKey={chartKey}
+ chartId={chart.id}
datasource={datasource}
formData={formData}
headerHeight={this.headerHeight(slice)}
height={this.height(slice)}
width={this.width()}
timeout={timeout}
- vizType={slice.formData.viz_type}
+ vizType={slice.viz_type}
addFilter={this.addFilter}
getFilters={this.getFilters}
- clearFilter={this.clearFilter}
removeFilter={this.removeFilter}
/>
</div>
diff --git a/superset/assets/src/dashboard/components/GridLayout.jsx b/superset/assets/src/dashboard/components/GridLayout.jsx
index ef0ec24..fd561e2 100644
--- a/superset/assets/src/dashboard/components/GridLayout.jsx
+++ b/superset/assets/src/dashboard/components/GridLayout.jsx
@@ -1,51 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Responsive, WidthProvider } from 'react-grid-layout';
+import cx from 'classnames';
import GridCell from './GridCell';
-
-require('react-grid-layout/css/styles.css');
-require('react-resizable/css/styles.css');
-
-const ResponsiveReactGridLayout = WidthProvider(Responsive);
+import { slicePropShape, chartPropShape } from '../v2/util/propShapes';
+import DashboardBuilder from '../v2/containers/DashboardBuilder';
const propTypes = {
- dashboard: PropTypes.object.isRequired,
+ dashboardInfo: PropTypes.shape().isRequired,
+ layout: PropTypes.object.isRequired,
datasources: PropTypes.object,
- charts: PropTypes.object.isRequired,
+ charts: PropTypes.objectOf(chartPropShape).isRequired,
+ slices: PropTypes.objectOf(slicePropShape).isRequired,
+ expandedSlices: PropTypes.object.isRequired,
+ sliceIds: PropTypes.object.isRequired,
filters: PropTypes.object,
timeout: PropTypes.number,
onChange: PropTypes.func,
+ rerenderCharts: PropTypes.func,
getFormDataExtra: PropTypes.func,
exploreChart: PropTypes.func,
exportCSV: PropTypes.func,
- fetchSlice: PropTypes.func,
- saveSlice: PropTypes.func,
- removeSlice: PropTypes.func,
- removeChart: PropTypes.func,
- updateDashboardLayout: PropTypes.func,
+ refreshChart: PropTypes.func,
+ saveSliceName: PropTypes.func,
toggleExpandSlice: PropTypes.func,
addFilter: PropTypes.func,
getFilters: PropTypes.func,
- clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
editMode: PropTypes.bool.isRequired,
+ showBuilderPane: PropTypes.bool.isRequired,
};
const defaultProps = {
+ expandedSlices: {},
+ filters: {},
+ timeout: 60,
onChange: () => ({}),
getFormDataExtra: () => ({}),
exploreChart: () => ({}),
exportCSV: () => ({}),
- fetchSlice: () => ({}),
- saveSlice: () => ({}),
- removeSlice: () => ({}),
- removeChart: () => ({}),
- updateDashboardLayout: () => ({}),
+ refreshChart: () => ({}),
+ saveSliceName: () => ({}),
toggleExpandSlice: () => ({}),
addFilter: () => ({}),
getFilters: () => ({}),
- clearFilter: () => ({}),
removeFilter: () => ({}),
};
@@ -53,141 +51,101 @@ class GridLayout extends React.Component {
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 = this.props.dashboardInfo.dash_edit_perm ?
this.updateSliceName.bind(this) : null;
}
- onResizeStop(layout) {
- this.props.updateDashboardLayout(layout);
- this.props.onChange();
- }
-
- onDragStop(layout) {
- this.props.updateDashboardLayout(layout);
- this.props.onChange();
+ componentDidUpdate(prevProps) {
+ if (prevProps.editMode !== this.props.editMode ||
+ prevProps.showBuilderPane !== this.props.showBuilderPane) {
+ this.props.rerenderCharts();
+ }
}
- getWidgetId(slice) {
- return 'widget_' + slice.slice_id;
+ getWidgetId(sliceId) {
+ return 'widget_' + sliceId;
}
- getWidgetHeight(slice) {
- const widgetId = this.getWidgetId(slice);
+ getWidgetHeight(sliceId) {
+ const widgetId = this.getWidgetId(sliceId);
if (!widgetId || !this.refs[widgetId]) {
return 400;
}
- return this.refs[widgetId].offsetHeight;
+ return this.refs[widgetId].parentNode.clientHeight;
}
- getWidgetWidth(slice) {
- const widgetId = this.getWidgetId(slice);
+ getWidgetWidth(sliceId) {
+ const widgetId = this.getWidgetId(sliceId);
if (!widgetId || !this.refs[widgetId]) {
return 400;
}
- return this.refs[widgetId].offsetWidth;
- }
-
- 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 dashboard and charts
- this.props.removeSlice(slice);
- this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey);
- this.props.onChange();
+ return this.refs[widgetId].parentNode.clientWidth;
}
updateSliceName(sliceId, sliceName) {
- const index = this.findSliceIndexById(sliceId);
- if (index === -1) {
- return;
- }
-
- const currentSlice = this.props.dashboard.slices[index];
- if (currentSlice.slice_name === sliceName) {
+ const key = sliceId;
+ const currentSlice = this.props.slices[key];
+ if (!currentSlice || currentSlice.slice_name === sliceName) {
return;
}
- this.props.saveSlice(currentSlice, sliceName);
+ this.props.saveSliceName(currentSlice, sliceName);
}
- isExpanded(slice) {
- return this.props.dashboard.metadata.expanded_slices &&
- this.props.dashboard.metadata.expanded_slices[slice.slice_id];
+ isExpanded(sliceId) {
+ return this.props.expandedSlices[sliceId];
}
render() {
- const cells = this.props.dashboard.slices.map((slice) => {
- const chartKey = `slice_${slice.slice_id}`;
- const currentChart = this.props.charts[chartKey];
- const queryResponse = currentChart.queryResponse || {};
- return (
- <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)}
- >
- <GridCell
- slice={slice}
- chartKey={chartKey}
- 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)}
- exploreChart={this.props.exploreChart}
- exportCSV={this.props.exportCSV}
- isExpanded={!!this.isExpanded(slice)}
- isLoading={currentChart.chartStatus === 'loading'}
- isCached={queryResponse.is_cached}
- cachedDttm={queryResponse.cached_dttm}
- 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}
- editMode={this.props.editMode}
- annotationQuery={currentChart.annotationQuery}
- annotationError={currentChart.annotationError}
- />
- </div>);
+ const cells = {};
+ this.props.sliceIds.forEach((sliceId) => {
+ const key = sliceId;
+ const currentChart = this.props.charts[key];
+ const currentSlice = this.props.slices[key];
+ if (currentChart) {
+ const currentDatasource = this.props.datasources[currentChart.form_data.datasource];
+ const queryResponse = currentChart.queryResponse || {};
+ cells[key] = (
+ <div
+ id={key}
+ key={sliceId}
+ className={cx('widget', `${currentSlice.viz_type}`, { 'is-edit': this.props.editMode })}
+ ref={this.getWidgetId(sliceId)}
+ >
+ <GridCell
+ slice={currentSlice}
+ chart={currentChart}
+ datasource={currentDatasource}
+ filters={this.props.filters}
+ formData={this.props.getFormDataExtra(currentChart)}
+ timeout={this.props.timeout}
+ widgetHeight={this.getWidgetHeight(sliceId)}
+ widgetWidth={this.getWidgetWidth(sliceId)}
+ exploreChart={this.props.exploreChart}
+ exportCSV={this.props.exportCSV}
+ isExpanded={!!this.isExpanded(sliceId)}
+ isLoading={currentChart.chartStatus === 'loading'}
+ isCached={queryResponse.is_cached}
+ cachedDttm={queryResponse.cached_dttm}
+ toggleExpandSlice={this.props.toggleExpandSlice}
+ refreshChart={this.props.refreshChart}
+ updateSliceName={this.updateSliceName}
+ addFilter={this.props.addFilter}
+ getFilters={this.props.getFilters}
+ removeFilter={this.props.removeFilter}
+ editMode={this.props.editMode}
+ annotationQuery={currentChart.annotationQuery}
+ annotationError={currentChart.annotationError}
+ />
+ </div>
+ );
+ }
});
return (
- <ResponsiveReactGridLayout
- className="layout"
- layouts={{ lg: this.props.dashboard.layout }}
- onResizeStop={this.onResizeStop}
- onDragStop={this.onDragStop}
- cols={{ lg: 48, md: 48, sm: 40, xs: 32, xxs: 24 }}
- rowHeight={10}
- autoSize
- margin={[20, 20]}
- useCSSTransforms
- draggableHandle=".drag"
- >
- {cells}
- </ResponsiveReactGridLayout>
+ <DashboardBuilder
+ cells={cells}
+ />
);
}
}
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index eabd3f4..f533506 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,47 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
import Controls from './Controls';
import EditableTitle from '../../components/EditableTitle';
import Button from '../../components/Button';
import FaveStar from '../../components/FaveStar';
-import URLShortLinkButton from '../../components/URLShortLinkButton';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+import { chartPropShape } from '../v2/util/propShapes';
import { t } from '../../locales';
const propTypes = {
- dashboard: PropTypes.object.isRequired,
+ dashboardInfo: PropTypes.object.isRequired,
+ dashboardTitle: PropTypes.string.isRequired,
+ charts: PropTypes.objectOf(chartPropShape).isRequired,
+ layout: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
- userId: PropTypes.string.isRequired,
- isStarred: PropTypes.bool,
- addSlicesToDashboard: PropTypes.func,
- onSave: PropTypes.func,
- onChange: PropTypes.func,
+ expandedSlices: PropTypes.object.isRequired,
+ isStarred: PropTypes.bool.isRequired,
+ onSave: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
fetchFaveStar: PropTypes.func,
- renderSlices: PropTypes.func,
+ fetchCharts: PropTypes.func.isRequired,
saveFaveStar: PropTypes.func,
- serialize: PropTypes.func,
- startPeriodicRender: PropTypes.func,
- updateDashboardTitle: PropTypes.func,
+ startPeriodicRender: PropTypes.func.isRequired,
+ updateDashboardTitle: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
setEditMode: PropTypes.func.isRequired,
- unsavedChanges: PropTypes.bool.isRequired,
+ showBuilderPane: PropTypes.bool.isRequired,
+ toggleBuilderPane: PropTypes.func.isRequired,
+ hasUnsavedChanges: PropTypes.bool.isRequired,
+
+ // redux
+ onUndo: PropTypes.func.isRequired,
+ onRedo: PropTypes.func.isRequired,
+ canUndo: PropTypes.bool.isRequired,
+ canRedo: PropTypes.bool.isRequired,
};
class Header extends React.PureComponent {
constructor(props) {
super(props);
- this.handleSaveTitle = this.handleSaveTitle.bind(this);
+ this.handleChangeText = this.handleChangeText.bind(this);
this.toggleEditMode = this.toggleEditMode.bind(this);
+ this.forceRefresh = this.forceRefresh.bind(this);
+ }
+ forceRefresh() {
+ return this.props.fetchCharts(Object.values(this.props.charts), true);
}
- handleSaveTitle(title) {
- this.props.updateDashboardTitle(title);
+ handleChangeText(nextText) {
+ const { updateDashboardTitle, onChange } = this.props;
+ if (nextText && this.props.dashboardTitle !== nextText) {
+ updateDashboardTitle(nextText);
+ onChange();
+ }
}
toggleEditMode() {
this.props.setEditMode(!this.props.editMode);
}
renderUnsaved() {
- if (!this.props.unsavedChanges) {
+ if (!this.props.hasUnsavedChanges) {
return null;
}
return (
@@ -54,66 +72,86 @@ class Header extends React.PureComponent {
/>
);
}
+ renderInsertButton() {
+ if (!this.props.editMode) {
+ return null;
+ }
+ const btnText = this.props.showBuilderPane ? t('Hide builder pane') : t('Insert components');
+ return (
+ <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
+ {btnText}
+ </Button>
+ );
+ }
renderEditButton() {
- if (!this.props.dashboard.dash_save_perm) {
+ if (!this.props.dashboardInfo.dash_save_perm) {
return null;
}
- const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
+ const btnText = this.props.editMode ? t('Switch to View Mode') : t('Edit Dashboard');
return (
- <Button
- bsStyle="default"
- className="m-r-5"
- style={{ width: '150px' }}
- onClick={this.toggleEditMode}
- >
+ <Button bsSize="small" onClick={this.toggleEditMode}>
{btnText}
- </Button>);
+ </Button>
+ );
}
render() {
- const dashboard = this.props.dashboard;
+ const {
+ dashboardTitle,
+ layout,
+ filters,
+ expandedSlices,
+ onUndo,
+ onRedo,
+ canUndo,
+ canRedo,
+ onChange,
+ onSave,
+ editMode,
+ } = this.props;
+
return (
- <div className="title">
- <div className="pull-left">
- <h1 className="outer-container pull-left">
- <EditableTitle
- title={dashboard.dashboard_title}
- canEdit={dashboard.dash_save_perm && this.props.editMode}
- onSaveTitle={this.handleSaveTitle}
- showTooltip={this.props.editMode}
- />
- <span className="favstar m-r-5">
- <FaveStar
- itemId={dashboard.id}
- fetchFaveStar={this.props.fetchFaveStar}
- saveFaveStar={this.props.saveFaveStar}
- isStarred={this.props.isStarred}
- />
- </span>
- {this.renderUnsaved()}
- </h1>
- </div>
- <div className="pull-right" style={{ marginTop: '35px' }}>
- <span className="m-r-5">
- <URLShortLinkButton
- emailSubject="Superset Dashboard"
- emailContent="Check out this dashboard: "
+ <div className="dashboard-header">
+ <div className="dashboard-component-header header-large">
+ <EditableTitle
+ title={dashboardTitle}
+ canEdit={this.props.dashboardInfo.dash_save_perm && editMode}
+ onSaveTitle={this.handleChangeText}
+ showTooltip={editMode}
+ />
+ <span className="favstar m-r-5">
+ <FaveStar
+ itemId={this.props.dashboardInfo.id}
+ fetchFaveStar={this.props.fetchFaveStar}
+ saveFaveStar={this.props.saveFaveStar}
+ isStarred={this.props.isStarred}
/>
</span>
- {this.renderEditButton()}
+ {this.renderUnsaved()}
+ </div>
+ <ButtonToolbar>
+ <ButtonGroup>
+ <Button bsSize="small" onClick={onUndo} disabled={!canUndo}>
+ Undo
+ </Button>
+ <Button bsSize="small" onClick={onRedo} disabled={!canRedo}>
+ Redo
+ </Button>
+ {this.renderInsertButton()}
+ {this.renderEditButton()}
+ </ButtonGroup>
<Controls
- dashboard={dashboard}
- filters={this.props.filters}
- userId={this.props.userId}
- addSlicesToDashboard={this.props.addSlicesToDashboard}
- onSave={this.props.onSave}
- onChange={this.props.onChange}
- renderSlices={this.props.renderSlices}
- serialize={this.props.serialize}
+ dashboardInfo={this.props.dashboardInfo}
+ dashboardTitle={dashboardTitle}
+ layout={layout}
+ filters={filters}
+ expandedSlices={expandedSlices}
+ onSave={onSave}
+ onChange={onChange}
+ forceRefreshAllCharts={this.forceRefresh}
startPeriodicRender={this.props.startPeriodicRender}
- editMode={this.props.editMode}
+ editMode={editMode}
/>
- </div>
- <div className="clearfix" />
+ </ButtonToolbar>
</div>
);
}
diff --git a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
index 93c4272..c2a5637 100644
--- a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
+++ b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
@@ -48,8 +48,11 @@ class RefreshIntervalModal extends React.PureComponent {
options={options}
value={this.state.refreshFrequency}
onChange={(opt) => {
- this.setState({ refreshFrequency: opt.value });
- this.props.onChange(opt.value);
+ const value = opt ? opt.value : options[0].value;
+ this.setState({
+ refreshFrequency: value,
+ });
+ this.props.onChange(value);
}}
/>
</div>
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index d693385..2e76bf4 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,31 +1,30 @@
/* global notify */
import React from 'react';
import PropTypes from 'prop-types';
+import $ from 'jquery';
+
import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
import { getAjaxErrorMsg } from '../../modules/utils';
import ModalTrigger from '../../components/ModalTrigger';
import { t } from '../../locales';
import Checkbox from '../../components/Checkbox';
-const $ = window.$ = require('jquery');
-
const propTypes = {
- css: PropTypes.string,
- dashboard: PropTypes.object.isRequired,
+ dashboardId: PropTypes.number.isRequired,
+ dashboardTitle: PropTypes.string.isRequired,
+ expandedSlices: PropTypes.object.isRequired,
+ layout: PropTypes.object.isRequired,
triggerNode: PropTypes.node.isRequired,
filters: PropTypes.object.isRequired,
- serialize: PropTypes.func,
- onSave: PropTypes.func,
+ onSave: PropTypes.func.isRequired,
};
class SaveModal extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
- dashboard: props.dashboard,
- css: props.css,
saveType: 'overwrite',
- newDashName: props.dashboard.dashboard_title + ' [copy]',
+ newDashName: props.dashboardTitle + ' [copy]',
duplicateSlices: false,
};
this.modal = null;
@@ -50,7 +49,6 @@ class SaveModal extends React.PureComponent {
saveDashboardRequest(data, url, saveType) {
const saveModal = this.modal;
const onSaveDashboard = this.props.onSave;
- Object.assign(data, { css: this.props.css });
$.ajax({
type: 'POST',
url,
@@ -74,19 +72,17 @@ class SaveModal extends React.PureComponent {
});
}
saveDashboard(saveType, newDashboardTitle) {
- const dashboard = this.props.dashboard;
- const positions = this.props.serialize();
+ const { dashboardTitle, layout: positions, expandedSlices, filters, dashboardId } = this.props;
const data = {
positions,
- css: this.state.css,
- expanded_slices: dashboard.metadata.expanded_slices || {},
- dashboard_title: dashboard.dashboard_title,
- default_filters: JSON.stringify(this.props.filters),
+ expanded_slices: expandedSlices,
+ dashboard_title: dashboardTitle,
+ default_filters: JSON.stringify(filters),
duplicate_slices: this.state.duplicateSlices,
};
let url = null;
if (saveType === 'overwrite') {
- url = `/superset/save_dash/${dashboard.id}/`;
+ url = `/superset/save_dash/${dashboardId}/`;
this.saveDashboardRequest(data, url, saveType);
} else if (saveType === 'newDashboard') {
if (!newDashboardTitle) {
@@ -97,7 +93,7 @@ class SaveModal extends React.PureComponent {
});
} else {
data.dashboard_title = newDashboardTitle;
- url = `/superset/copy_dash/${dashboard.id}/`;
+ url = `/superset/copy_dash/${dashboardId}/`;
this.saveDashboardRequest(data, url, saveType);
}
}
@@ -116,7 +112,7 @@ class SaveModal extends React.PureComponent {
onChange={this.handleSaveTypeChange}
checked={this.state.saveType === 'overwrite'}
>
- {t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
+ {t('Overwrite Dashboard [%s]', this.props.dashboardTitle)}
</Radio>
<hr />
<Radio
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index e99d00f..6477fc4 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -1,219 +1,214 @@
import React from 'react';
-import $ from 'jquery';
import PropTypes from 'prop-types';
-import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+import cx from 'classnames';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+import { List } from 'react-virtualized';
+import SearchInput, { createFilter } from 'react-search-input';
-import ModalTrigger from '../../components/ModalTrigger';
-import { t } from '../../locales';
-
-require('react-bootstrap-table/css/react-bootstrap-table.css');
+import DragDroppable from '../v2/components/dnd/DragDroppable';
+import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE } from '../v2/util/componentTypes';
+import { NEW_CHART_ID, NEW_COMPONENTS_SOURCE_ID } from '../v2/util/constants';
+import { slicePropShape } from '../v2/util/propShapes';
const propTypes = {
- dashboard: PropTypes.object.isRequired,
- triggerNode: PropTypes.node.isRequired,
+ fetchAllSlices: PropTypes.func.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ slices: PropTypes.objectOf(slicePropShape).isRequired,
+ lastUpdated: PropTypes.number.isRequired,
+ errorMessage: PropTypes.string,
userId: PropTypes.string.isRequired,
- addSlicesToDashboard: PropTypes.func,
+ selectedSliceIds: PropTypes.object,
+ editMode: PropTypes.bool,
+};
+
+const defaultProps = {
+ selectedSliceIds: new Set(),
+ editMode: false,
};
+const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
+const KEYS_TO_SORT = [
+ { key: 'slice_name', label: 'Name' },
+ { key: 'viz_type', label: 'Visualization' },
+ { key: 'datasource_name', label: 'Datasource' },
+ { key: 'changed_on', label: 'Recent' },
+];
+
class SliceAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
- slices: [],
- slicesLoaded: false,
- selectionMap: {},
+ filteredSlices: [],
+ searchTerm: '',
+ sortBy: KEYS_TO_SORT.findIndex(item => (item.key === 'changed_on')),
};
- this.options = {
- defaultSortOrder: 'desc',
- defaultSortName: 'modified',
- sizePerPage: 10,
- };
+ this.rowRenderer = this.rowRenderer.bind(this);
+ this.searchUpdated = this.searchUpdated.bind(this);
+ this.handleKeyPress = this.handleKeyPress.bind(this);
+ this.handleSelect = this.handleSelect.bind(this);
+ }
- this.addSlices = this.addSlices.bind(this);
- this.toggleSlice = this.toggleSlice.bind(this);
+ componentDidMount() {
+ this.slicesRequest = this.props.fetchAllSlices(this.props.userId);
+ }
- this.selectRowProp = {
- mode: 'checkbox',
- clickToSelect: true,
- onSelect: this.toggleSlice,
- };
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.lastUpdated !== this.props.lastUpdated) {
+ this.setState({
+ filteredSlices: Object.values(nextProps.slices)
+ .filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
+ .sort(this.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key)),
+ });
+ }
}
componentWillUnmount() {
- if (this.slicesRequest) {
+ if (this.slicesRequest && this.slicesRequest.abort) {
this.slicesRequest.abort();
}
}
- onEnterModal() {
- const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
- this.slicesRequest = $.ajax({
- url: uri,
- type: 'GET',
- success: (response) => {
- // Prepare slice data for table
- const slices = response.result.map(slice => ({
- id: slice.id,
- sliceName: slice.slice_name,
- vizType: slice.viz_type,
- datasourceLink: slice.datasource_link,
- modified: slice.modified,
- }));
-
- this.setState({
- slices,
- selectionMap: {},
- slicesLoaded: true,
- });
- },
- error: (error) => {
- this.errored = true;
- this.setState({
- errorMsg: t('Sorry, there was an error fetching charts to this dashboard: ') +
- this.getAjaxErrorMsg(error),
- });
- },
- });
+ getFilteredSortedSlices(searchTerm, sortBy) {
+ return Object.values(this.props.slices)
+ .filter(createFilter(searchTerm, KEYS_TO_FILTERS))
+ .sort(this.sortByComparator(KEYS_TO_SORT[sortBy].key));
}
- getAjaxErrorMsg(error) {
- const respJSON = error.responseJSON;
- return (respJSON && respJSON.message) ? respJSON.message :
- error.responseText;
+ sortByComparator(attr) {
+ const desc = (attr === 'changed_on') ? -1 : 1;
+
+ return (a, b) => {
+ if (a[attr] < b[attr]) {
+ return -1 * desc;
+ } else if (a[attr] > b[attr]) {
+ return 1 * desc;
+ }
+ return 0;
+ };
}
- addSlices() {
- 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 charts to this dashboard: ') +
- this.getAjaxErrorMsg(error),
- });
- });
+ handleKeyPress(ev) {
+ if (ev.key === 'Enter') {
+ ev.preventDefault();
+
+ this.searchUpdated(ev.target.value);
+ }
}
- toggleSlice(slice) {
- const selectionMap = Object.assign({}, this.state.selectionMap);
- selectionMap[slice.id] = !selectionMap[slice.id];
- this.setState({ selectionMap });
+ searchUpdated(searchTerm) {
+ this.setState({
+ searchTerm,
+ filteredSlices: this.getFilteredSortedSlices(searchTerm, this.state.sortBy),
+ });
}
- modifiedDateComparator(a, b, order) {
- if (order === 'desc') {
- if (a.changed_on > b.changed_on) {
- return -1;
- } else if (a.changed_on < b.changed_on) {
- return 1;
- }
- return 0;
- }
+ handleSelect(sortBy) {
+ this.setState({
+ sortBy,
+ filteredSlices: this.getFilteredSortedSlices(this.state.searchTerm, sortBy),
+ });
+ }
- if (a.changed_on < b.changed_on) {
- return -1;
- } else if (a.changed_on > b.changed_on) {
- return 1;
- }
- return 0;
+ rowRenderer({ key, index, style }) {
+ const cellData = this.state.filteredSlices[index];
+ const duration = cellData.modified ? cellData.modified.replace(/<[^>]*>/g, '') : '';
+ const isSelected = this.props.selectedSliceIds.has(cellData.slice_id);
+ const type = CHART_TYPE;
+ const id = NEW_CHART_ID;
+ const meta = {
+ chartId: cellData.slice_id,
+ };
+
+ return (
+ <DragDroppable
+ component={{ type, id, meta }}
+ parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
+ index={0}
+ depth={0}
+ disableDragDrop={isSelected}
+ editMode={this.props.editMode}
+ >
+ {({ dragSourceRef }) => (
+ <div
+ ref={dragSourceRef}
+ className="chart-card-container"
+ key={key}
+ style={style}
+ >
+ <div className={cx('chart-card', { 'is-selected': isSelected })}>
+ <div className="card-title">{cellData.slice_name}</div>
+ <div className="card-body">
+ <div className="item">
+ <span>Modified </span>
+ <span>{duration}</span>
+ </div>
+ <div className="item">
+ <span>Visualization </span>
+ <span>{cellData.viz_type}</span>
+ </div>
+ <div className="item">
+ <span>Data source </span>
+ <span dangerouslySetInnerHTML={{ __html: cellData.datasource_link }} />
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+ </DragDroppable>
+ );
}
render() {
- const hideLoad = this.state.slicesLoaded || this.errored;
- let enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap);
- if (enableAddSlice) {
- enableAddSlice = enableAddSlice.some(function (key) {
- return this.state.selectionMap[key];
- }, this);
- }
- const modalContent = (
- <div>
- <img
- src="/static/assets/images/loading.gif"
- className={'loading ' + (hideLoad ? 'hidden' : '')}
- alt={hideLoad ? '' : 'loading'}
- />
- <div className={this.errored ? '' : 'hidden'}>
- {this.state.errorMsg}
- </div>
- <div className={this.state.slicesLoaded ? '' : 'hidden'}>
- <BootstrapTable
- ref="table"
- data={this.state.slices}
- selectRow={this.selectRowProp}
- options={this.options}
- hover
- search
- pagination
- condensed
- height="auto"
+ return (
+ <div className="slice-adder-container">
+ <div className="controls">
+ <DropdownButton
+ title={KEYS_TO_SORT[this.state.sortBy].label}
+ onSelect={this.handleSelect}
+ id="slice-adder-sortby"
>
- <TableHeaderColumn
- dataField="id"
- isKey
- dataSort
- hidden
+ {KEYS_TO_SORT.map((item, index) => (
+ <MenuItem key={item.key} eventKey={index}>{item.label}</MenuItem>
+ ))}
+ </DropdownButton>
+
+ <SearchInput
+ onChange={this.searchUpdated}
+ onKeyPress={this.handleKeyPress}
+ />
+ </div>
+
+ {this.props.isLoading &&
+ <img
+ src="/static/assets/images/loading.gif"
+ className="loading"
+ alt="loading"
+ />
+ }
+ <div className={this.props.errorMessage ? '' : 'hidden'}>
+ {this.props.errorMessage}
+ </div>
+ <div className={!this.props.isLoading ? '' : 'hidden'}>
+ {this.state.filteredSlices.length > 0 &&
+ <List
+ width={376}
+ height={500}
+ rowCount={this.state.filteredSlices.length}
+ rowHeight={136}
+ rowRenderer={this.rowRenderer}
+ searchTerm={this.state.searchTerm}
+ sortBy={this.state.sortBy}
+ selectedSliceIds={this.props.selectedSliceIds}
/>
- <TableHeaderColumn
- dataField="sliceName"
- dataSort
- >
- {t('Name')}
- </TableHeaderColumn>
- <TableHeaderColumn
- dataField="vizType"
- dataSort
- >
- {t('Viz')}
- </TableHeaderColumn>
- <TableHeaderColumn
- dataField="datasourceLink"
- dataSort
- // Will cause react-bootstrap-table to interpret the HTML returned
- dataFormat={datasourceLink => datasourceLink}
- >
- {t('Datasource')}
- </TableHeaderColumn>
- <TableHeaderColumn
- dataField="modified"
- dataSort
- sortFunc={this.modifiedDateComparator}
- // Will cause react-bootstrap-table to interpret the HTML returned
- dataFormat={modified => modified}
- >
- {t('Modified')}
- </TableHeaderColumn>
- </BootstrapTable>
- <button
- type="button"
- className="btn btn-default"
- data-dismiss="modal"
- onClick={this.addSlices}
- disabled={!enableAddSlice}
- >
- {t('Add Charts')}
- </button>
+ }
</div>
</div>
);
-
- return (
- <ModalTrigger
- triggerNode={this.props.triggerNode}
- tooltip={t('Add a new chart to the dashboard')}
- beforeOpen={this.onEnterModal.bind(this)}
- isMenuItem
- modalBody={modalContent}
- bsSize="large"
- setModalAsTriggerChildren
- modalTitle={t('Add Charts to Dashboard')}
- />
- );
}
}
SliceAdder.propTypes = propTypes;
+SliceAdder.defaultProps = defaultProps;
export default SliceAdder;
diff --git a/superset/assets/src/dashboard/components/SliceAdderContainer.jsx b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
new file mode 100644
index 0000000..b4f10d9
--- /dev/null
+++ b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
@@ -0,0 +1,25 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import { fetchAllSlices } from '../actions/sliceEntities';
+import SliceAdder from './SliceAdder';
+
+function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
+ return {
+ userId: dashboardInfo.userId,
+ selectedSliceIds: dashboardState.sliceIds,
+ slices: sliceEntities.slices,
+ isLoading: sliceEntities.isLoading,
+ errorMessage: sliceEntities.errorMessage,
+ lastUpdated: sliceEntities.lastUpdated,
+ editMode: dashboardState.editMode,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return bindActionCreators({
+ fetchAllSlices
+ }, dispatch);
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 6db9c68..f126949 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -1,11 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
-import moment from 'moment';
-import { connect } from 'react-redux';
import { t } from '../../locales';
import EditableTitle from '../../components/EditableTitle';
import TooltipWrapper from '../../components/TooltipWrapper';
+import SliceHeaderControls from './SliceHeaderControls';
const propTypes = {
slice: PropTypes.object.isRequired,
@@ -14,7 +13,6 @@ const propTypes = {
isExpanded: PropTypes.bool,
isCached: PropTypes.bool,
cachedDttm: PropTypes.string,
- removeSlice: PropTypes.func,
updateSliceName: PropTypes.func,
toggleExpandSlice: PropTypes.func,
forceRefresh: PropTypes.func,
@@ -40,11 +38,6 @@ class SliceHeader extends React.PureComponent {
super(props);
this.onSaveTitle = this.onSaveTitle.bind(this);
- this.onToggleExpandSlice = this.onToggleExpandSlice.bind(this);
- this.exportCSV = this.props.exportCSV.bind(this, this.props.slice);
- this.exploreChart = this.props.exploreChart.bind(this, this.props.slice);
- this.forceRefresh = this.props.forceRefresh.bind(this, this.props.slice.slice_id);
- this.removeSlice = this.props.removeSlice.bind(this, this.props.slice);
}
onSaveTitle(newTitle) {
@@ -53,17 +46,12 @@ class SliceHeader extends React.PureComponent {
}
}
- onToggleExpandSlice() {
- this.props.toggleExpandSlice(this.props.slice, !this.props.isExpanded);
- }
-
render() {
- const slice = this.props.slice;
- const isCached = this.props.isCached;
- const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
- const refreshTooltip = isCached ?
- t('Served from data cached %s . Click to force refresh.', cachedWhen) :
- t('Force refresh data');
+ const {
+ slice, isExpanded, isCached, cachedDttm,
+ toggleExpandSlice, forceRefresh,
+ exploreChart, exportCSV,
+ } = this.props;
const annoationsLoading = t('Annotation layers are still loading.');
const annoationsError = t('One ore more annotation layers failed loading.');
@@ -96,83 +84,18 @@ class SliceHeader extends React.PureComponent {
<i className="fa fa-exclamation-circle danger" />
</TooltipWrapper>
}
- </div>
- <div className="chart-controls">
- <div id={'controls_' + slice.slice_id} className="pull-right">
- {this.props.editMode &&
- <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.forceRefresh}>
- <TooltipWrapper
- placement="top"
- label="refresh"
- tooltip={refreshTooltip}
- >
- <i className="fa fa-repeat" />
- </TooltipWrapper>
- </a>
- {slice.description &&
- <a onClick={this.onToggleExpandSlice}>
- <TooltipWrapper
- placement="top"
- label="description"
- tooltip={t('Toggle chart description')}
- >
- <i className="fa fa-info-circle slice_info" />
- </TooltipWrapper>
- </a>
- }
- {this.props.sliceCanEdit &&
- <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" onClick={this.exportCSV}>
- <TooltipWrapper
- placement="top"
- label="exportCSV"
- tooltip={t('Export CSV')}
- >
- <i className="fa fa-table" />
- </TooltipWrapper>
- </a>
- {this.props.supersetCanExplore &&
- <a className="exploreChart" onClick={this.exploreChart}>
- <TooltipWrapper
- placement="top"
- label="exploreChart"
- tooltip={t('Explore chart')}
- >
- <i className="fa fa-share" />
- </TooltipWrapper>
- </a>
- }
- {this.props.editMode &&
- <a className="remove-chart" onClick={this.removeSlice}>
- <TooltipWrapper
- placement="top"
- label="close"
- tooltip={t('Remove chart from dashboard')}
- >
- <i className="fa fa-close" />
- </TooltipWrapper>
- </a>
- }
- </div>
+ {!this.props.editMode &&
+ <SliceHeaderControls
+ slice={slice}
+ isCached={isCached}
+ isExpanded={isExpanded}
+ cachedDttm={cachedDttm}
+ toggleExpandSlice={toggleExpandSlice}
+ forceRefresh={forceRefresh}
+ exploreChart={exploreChart}
+ exportCSV={exportCSV}
+ />
+ }
</div>
</div>
</div>
@@ -183,12 +106,4 @@ class SliceHeader extends React.PureComponent {
SliceHeader.propTypes = propTypes;
SliceHeader.defaultProps = defaultProps;
-function mapStateToProps({ dashboard }) {
- return {
- supersetCanExplore: dashboard.dashboard.superset_can_explore,
- sliceCanEdit: dashboard.dashboard.slice_can_edit,
- };
-}
-
-export { SliceHeader };
-export default connect(mapStateToProps, () => ({}))(SliceHeader);
+export default SliceHeader;
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
new file mode 100644
index 0000000..f61e59b
--- /dev/null
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import moment from 'moment';
+import { DropdownButton } from 'react-bootstrap';
+
+import { ActionMenuItem } from './ActionMenuItem';
+import { t } from '../../locales';
+
+const propTypes = {
+ slice: PropTypes.object.isRequired,
+ isCached: PropTypes.bool,
+ isExpanded: PropTypes.bool,
+ cachedDttm: PropTypes.string,
+ toggleExpandSlice: PropTypes.func,
+ forceRefresh: PropTypes.func,
+ exploreChart: PropTypes.func,
+ exportCSV: PropTypes.func,
+};
+
+const defaultProps = {
+ forceRefresh: () => ({}),
+ toggleExpandSlice: () => ({}),
+ exploreChart: () => ({}),
+ exportCSV: () => ({}),
+};
+
+class SliceHeaderControls extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.exportCSV = this.props.exportCSV.bind(this, this.props.slice.slice_id);
+ this.exploreChart = this.props.exploreChart.bind(this, this.props.slice.slice_id);
+ this.toggleExpandSlice = this.props.toggleExpandSlice.bind(this, this.props.slice.slice_id);
+ this.toggleControls = this.toggleControls.bind(this);
+
+ this.state = {
+ showControls: false,
+ };
+ }
+
+ toggleControls() {
+ this.setState({
+ showControls: !this.state.showControls,
+ });
+ }
+
+ render() {
+ const slice = this.props.slice;
+ const isCached = this.props.isCached;
+ const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
+ const refreshTooltip = isCached ?
+ t('Served from data cached %s . Click to force refresh.', cachedWhen) :
+ t('Force refresh data');
+
+ // @TODO account for
+ // dashboard.dashboard.superset_can_explore
+ // dashboard.dashboard.slice_can_edit
+ return (
+ <DropdownButton
+ title=""
+ id={`slice_${slice.slice_id}-controls`}
+ className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', { 'is-cached': isCached })}
+ pullRight
+ noCaret
+ >
+ <ActionMenuItem
+ text={t('Force refresh data')}
+ tooltip={refreshTooltip}
+ onClick={this.props.forceRefresh}
+ />
+
+ {slice.description &&
+ <ActionMenuItem
+ text={t('Toggle chart description')}
+ tooltip={t('Toggle chart description')}
+ onClick={this.toggleExpandSlice}
+ />
+ }
+
+ <ActionMenuItem
+ text={t('Edit chart')}
+ tooltip={t('Edit the chart\'s properties')}
+ href={slice.edit_url}
+ target="_blank"
+ />
+
+ <ActionMenuItem
+ text={t('Export CSV')}
+ tooltip={t('Export CSV')}
+ onClick={this.exportCSV}
+ />
+
+ <ActionMenuItem
+ text={t('Explore chart')}
+ tooltip={t('Explore chart')}
+ onClick={this.exploreChart}
+ />
+ </DropdownButton>
+ );
+ }
+}
+
+SliceHeaderControls.propTypes = propTypes;
+SliceHeaderControls.defaultProps = defaultProps;
+
+export default SliceHeaderControls;
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index 1aadc58..9c00f9e 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -8,36 +8,18 @@ import { initEnhancer } from '../reduxUtils';
import { appSetup } from '../common';
import { initJQueryAjax } from '../modules/utils';
import DashboardContainer from './components/DashboardContainer';
-// import rootReducer, { getInitialState } from './reducers';
-
-import emptyDashboardLayout from './v2/fixtures/emptyDashboardLayout';
-import rootReducer from './v2/reducers/';
+import getInitialState from './reducers/getInitialState';
+import rootReducer from './reducers/index';
appSetup();
initJQueryAjax();
const appContainer = document.getElementById('app');
-// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
-// const initState = Object.assign({}, getInitialState(bootstrapData));
-
-const initState = {
- dashboardLayout: {
- past: [],
- present: emptyDashboardLayout,
- future: [],
- },
- editMode: true,
- messageToasts: [],
-};
+const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
+const initState = getInitialState(bootstrapData);
const store = createStore(
- rootReducer,
- initState,
- compose(
- applyMiddleware(thunk),
- initEnhancer(false),
- ),
-);
+ rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
ReactDOM.render(
<Provider store={store}>
diff --git a/superset/assets/src/dashboard/reducers.js b/superset/assets/src/dashboard/reducers.js
deleted file mode 100644
index 01e6dc2..0000000
--- a/superset/assets/src/dashboard/reducers.js
+++ /dev/null
@@ -1,214 +0,0 @@
-/* eslint-disable camelcase */
-import { combineReducers } from 'redux';
-import d3 from 'd3';
-import shortid from 'shortid';
-
-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/store';
-import { getColorFromScheme } from '../modules/colors';
-
-export function getInitialState(bootstrapData) {
- const { user_id, datasources, common, editMode } = bootstrapData;
- delete common.locale;
- delete common.language_pack;
-
- const dashboard = { ...bootstrapData.dashboard_data };
- let filters = {};
- try {
- // allow request parameter overwrite dashboard metadata
- filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
- } catch (e) {
- //
- }
-
- // Priming the color palette with user's label-color mapping provided in
- // the dashboard's JSON metadata
- if (dashboard.metadata && dashboard.metadata.label_colors) {
- const colorMap = dashboard.metadata.label_colors;
- for (const label in colorMap) {
- getColorFromScheme(label, null, colorMap[label]);
- }
- }
-
- dashboard.posDict = {};
- dashboard.layout = [];
- if (Array.isArray(dashboard.position_json)) {
- dashboard.position_json.forEach((position) => {
- dashboard.posDict[position.slice_id] = position;
- });
- } else {
- dashboard.position_json = [];
- }
-
- const lastRowId = Math.max(0, Math.max.apply(null,
- dashboard.position_json.map(pos => (pos.row + pos.size_y))));
- let newSliceCounter = 0;
- dashboard.slices.forEach((slice) => {
- const sliceId = slice.slice_id;
- let pos = dashboard.posDict[sliceId];
- if (!pos) {
- // append new slices to dashboard bottom, 3 slices per row
- pos = {
- col: (newSliceCounter % 3) * 16 + 1,
- row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
- size_x: 16,
- size_y: 16,
- };
- newSliceCounter++;
- }
-
- 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, editMode },
- };
-}
-
-export 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 key = String(action.slice.slice_id);
- const newLayout = state.dashboard.layout.filter(reactPos => (reactPos.i !== key));
- const newDashboard = removeFromArr(state.dashboard, 'slices', action.slice, 'slice_id');
- // if this slice is a filter
- const newFilter = { ...state.filters };
- let refresh = false;
- if (state.filters[key]) {
- delete newFilter[key];
- refresh = true;
- }
- return {
- ...state,
- dashboard: { ...newDashboard, layout: newLayout },
- filters: newFilter,
- refresh,
- };
- },
- [actions.TOGGLE_FAVE_STAR]() {
- return { ...state, isStarred: action.isStarred };
- },
- [actions.SET_EDIT_MODE]() {
- return { ...state, editMode: action.editMode };
- },
- [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 = state.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) {
- let newFilter = {};
- if (!(sliceId in filters)) {
- // Straight up set the filters if none existed for the slice
- newFilter = { [col]: vals };
- } else if (filters[sliceId] && !(col in filters[sliceId]) || !merge) {
- newFilter = { ...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 (filters[sliceId][col] instanceof Array) {
- newFilter[col] = d3.merge([filters[sliceId][col], vals]);
- } else {
- newFilter[col] = d3.merge([[filters[sliceId][col]], vals])[0] || '';
- }
- filters = { ...filters, [sliceId]: newFilter };
- }
- return { ...state, filters, refresh };
- },
- [actions.CLEAR_FILTER]() {
- const newFilters = { ...state.filters };
- delete newFilters[action.sliceId];
- return { ...state, filter: newFilters, refresh: true };
- },
- [actions.REMOVE_FILTER]() {
- const { sliceId, col, vals, refresh } = action;
- const excluded = new Set(vals);
- const valFilter = val => !excluded.has(val);
-
- let filters = state.filters;
- // Have to be careful not to modify the dashboard state so that
- // the render actually triggers
- if (sliceId in state.filters && col in state.filters[sliceId]) {
- const newFilter = filters[sliceId][col].filter(valFilter);
- filters = { ...filters, [sliceId]: newFilter };
- }
- return { ...state, filters, refresh };
- },
-
- // slice reducer
- [actions.UPDATE_SLICE_NAME]() {
- const newDashboard = alterInArr(
- state.dashboard, 'slices',
- action.slice, { slice_name: action.sliceName },
- 'slice_id');
- return { ...state, dashboard: newDashboard };
- },
- };
-
- if (action.type in actionHandlers) {
- return actionHandlers[action.type]();
- }
- return state;
-};
-
-export default combineReducers({
- charts,
- dashboard,
- impressionId: () => (shortid.generate()),
-});
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
new file mode 100644
index 0000000..84ee58e
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -0,0 +1,128 @@
+/* eslint-disable camelcase */
+import { merge as mergeArray } from 'd3';
+
+import {
+ ADD_SLICE,
+ ADD_FILTER,
+ ON_CHANGE,
+ ON_SAVE,
+ REMOVE_SLICE,
+ REMOVE_FILTER,
+ SET_EDIT_MODE,
+ TOGGLE_BUILDER_PANE,
+ TOGGLE_EXPAND_SLICE,
+ TOGGLE_FAVE_STAR,
+ UPDATE_DASHBOARD_TITLE,
+} from '../actions/dashboardState';
+
+export default function (state = {}, action) {
+ const actionHandlers = {
+ [UPDATE_DASHBOARD_TITLE]() {
+ return { ...state, title: action.title };
+ },
+ [ADD_SLICE]() {
+ const updatedSliceIds = new Set(state.sliceIds);
+ updatedSliceIds.add(action.slice.slice_id);
+ return {
+ ...state,
+ sliceIds: updatedSliceIds,
+ };
+ },
+ [REMOVE_SLICE]() {
+ const sliceId = action.sliceId;
+ const updatedSliceIds = new Set(state.sliceIds);
+ updatedSliceIds.delete(sliceId);
+
+ const key = sliceId;
+ // if this slice is a filter
+ const newFilter = { ...state.filters };
+ let refresh = false;
+ if (state.filters[key]) {
+ delete newFilter[key];
+ refresh = true;
+ }
+ return {
+ ...state,
+ sliceIds: updatedSliceIds,
+ filters: newFilter,
+ refresh,
+ };
+ },
+ [TOGGLE_FAVE_STAR]() {
+ return { ...state, isStarred: action.isStarred };
+ },
+ [SET_EDIT_MODE]() {
+ return { ...state, editMode: action.editMode };
+ },
+ [TOGGLE_BUILDER_PANE]() {
+ return { ...state, showBuilderPane: !state.showBuilderPane };
+ },
+ [TOGGLE_EXPAND_SLICE]() {
+ const updatedExpandedSlices = { ...state.expandedSlices };
+ const sliceId = action.sliceId;
+ if (updatedExpandedSlices[sliceId]) {
+ delete updatedExpandedSlices[sliceId];
+ } else {
+ updatedExpandedSlices[sliceId] = true;
+ }
+ return { ...state, expandedSlices: updatedExpandedSlices };
+ },
+ [ON_CHANGE]() {
+ return { ...state, hasUnsavedChanges: true };
+ },
+ [ON_SAVE]() {
+ return { ...state, hasUnsavedChanges: false };
+ },
+
+ // filters
+ [ADD_FILTER]() {
+ const hasSelectedFilter = state.sliceIds.has(action.chart.id);
+ if (!hasSelectedFilter) {
+ return state;
+ }
+
+ let filters = state.filters;
+ const { chart, col, vals, merge, refresh } = action;
+ const sliceId = chart.id;
+ const filterKeys = ['__from', '__to', '__time_col',
+ '__time_grain', '__time_origin', '__granularity'];
+ if (filterKeys.indexOf(col) >= 0 ||
+ action.chart.formData.groupby.indexOf(col) !== -1) {
+ let newFilter = {};
+ if (!(sliceId in filters)) {
+ // Straight up set the filters if none existed for the slice
+ newFilter = { [col]: vals };
+ } else if (filters[sliceId] && !(col in filters[sliceId]) || !merge) {
+ newFilter = { ...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 (filters[sliceId][col] instanceof Array) {
+ newFilter[col] = mergeArray([filters[sliceId][col], vals]);
+ } else {
+ newFilter[col] = mergeArray([[filters[sliceId][col]], vals])[0] || '';
+ }
+ filters = { ...filters, [sliceId]: newFilter };
+ }
+ return { ...state, filters, refresh };
+ },
+ [REMOVE_FILTER]() {
+ const { sliceId, col, vals, refresh } = action;
+ const excluded = new Set(vals);
+ const valFilter = val => !excluded.has(val);
+
+ let filters = state.filters;
+ // Have to be careful not to modify the dashboard state so that
+ // the render actually triggers
+ if (sliceId in state.filters && col in state.filters[sliceId]) {
+ const newFilter = filters[sliceId][col].filter(valFilter);
+ filters = { ...filters, [sliceId]: newFilter };
+ }
+ return { ...state, filters, refresh };
+ },
+ };
+
+ if (action.type in actionHandlers) {
+ return actionHandlers[action.type]();
+ }
+ return state;
+}
diff --git a/superset/assets/src/dashboard/reducers/datasources.js b/superset/assets/src/dashboard/reducers/datasources.js
new file mode 100644
index 0000000..4df7507
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/datasources.js
@@ -0,0 +1,17 @@
+import * as actions from '../actions/datasources';
+
+export default function datasourceReducer(datasources = {}, action) {
+ const actionHandlers = {
+ [actions.SET_DATASOURCE]() {
+ return action.datasource;
+ },
+ };
+
+ if (action.type in actionHandlers) {
+ return {
+ ...datasources,
+ [action.key]: actionHandlers[action.type](datasources[action.key], action),
+ };
+ }
+ return datasources;
+}
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
new file mode 100644
index 0000000..1129210
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -0,0 +1,109 @@
+/* eslint-disable camelcase */
+import shortid from 'shortid';
+
+import { chart } from '../../chart/chartReducer';
+import { initSliceEntities } from './sliceEntities';
+import { getParam } from '../../modules/utils';
+import { applyDefaultFormData } from '../../explore/stores/store';
+import { getColorFromScheme } from '../../modules/colors';
+import layoutConverter from '../util/dashboardLayoutConverter';
+import { DASHBOARD_ROOT_ID } from '../v2/util/constants';
+
+export default function (bootstrapData) {
+ const { user_id, datasources, common } = bootstrapData;
+ delete common.locale;
+ delete common.language_pack;
+
+ const dashboard = { ...bootstrapData.dashboard_data };
+ let filters = {};
+ try {
+ // allow request parameter overwrite dashboard metadata
+ filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
+ } catch (e) {
+ //
+ }
+
+ // Priming the color palette with user's label-color mapping provided in
+ // the dashboard's JSON metadata
+ if (dashboard.metadata && dashboard.metadata.label_colors) {
+ const colorMap = dashboard.metadata.label_colors;
+ for (const label in colorMap) {
+ getColorFromScheme(label, null, colorMap[label]);
+ }
+ }
+
+ // dashboard layout
+ const positionJson = dashboard.position_json;
+ let layout;
+ if (!positionJson || !positionJson[DASHBOARD_ROOT_ID]) {
+ layout = layoutConverter(dashboard);
+ } else {
+ layout = positionJson;
+ }
+
+ const dashboardLayout = {
+ past: [],
+ present: layout,
+ future: [],
+ };
+ delete dashboard.position_json;
+ delete dashboard.css;
+
+ const chartQueries = {};
+ const slices = {};
+ const sliceIds = new Set();
+ dashboard.slices.forEach((slice) => {
+ const key = slice.slice_id;
+ chartQueries[key] = { ...chart,
+ id: key,
+ form_data: slice.form_data,
+ formData: applyDefaultFormData(slice.form_data),
+ };
+
+ slices[key] = {
+ slice_id: key,
+ slice_url: slice.slice_url,
+ slice_name: slice.slice_name,
+ form_data: slice.form_data,
+ edit_url: slice.edit_url,
+ viz_type: slice.form_data.viz_type,
+ datasource: slice.form_data.datasource,
+ description: slice.description,
+ description_markeddown: slice.description_markeddown,
+ };
+
+ sliceIds.add(key);
+ });
+
+ return {
+ datasources,
+ sliceEntities: { ...initSliceEntities, slices, isLoading: false },
+ charts: chartQueries,
+ dashboardInfo: { /* readOnly props */
+ id: dashboard.id,
+ slug: dashboard.slug,
+ metadata: {
+ filter_immune_slice_fields: dashboard.metadata.filter_immune_slice_fields,
+ filter_immune_slices: dashboard.metadata.filter_immune_slices,
+ timed_refresh_immune_slices: dashboard.metadata.timed_refresh_immune_slices,
+ },
+ userId: user_id,
+ dash_edit_perm: dashboard.dash_edit_perm,
+ dash_save_perm: dashboard.dash_save_perm,
+ common,
+ },
+ dashboardState: {
+ title: dashboard.dashboard_title,
+ sliceIds,
+ refresh: false,
+ filters,
+ expandedSlices: dashboard.metadata.expanded_slices || {},
+ editMode: false,
+ showBuilderPane: false,
+ hasUnsavedChanges: false,
+ },
+ dashboardLayout,
+ messageToasts: [],
+ impressionId: shortid.generate(),
+ };
+}
diff --git a/superset/assets/src/dashboard/reducers/index.js b/superset/assets/src/dashboard/reducers/index.js
new file mode 100644
index 0000000..a2397e0
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/index.js
@@ -0,0 +1,22 @@
+import { combineReducers } from 'redux';
+
+import charts from '../../chart/chartReducer';
+import dashboardState from './dashboardState';
+import datasources from './datasources';
+import sliceEntities from './sliceEntities';
+import dashboardLayout from '../v2/reducers/index';
+import messageToasts from '../v2/reducers/messageToasts';
+
+const dashboardInfo = (state = {}) => (state);
+const impressionId = (state = '') => (state);
+
+export default combineReducers({
+ charts,
+ datasources,
+ sliceEntities,
+ dashboardInfo,
+ dashboardState,
+ dashboardLayout,
+ messageToasts,
+ impressionId,
+});
diff --git a/superset/assets/src/dashboard/reducers/sliceEntities.js b/superset/assets/src/dashboard/reducers/sliceEntities.js
new file mode 100644
index 0000000..61a58f6
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/sliceEntities.js
@@ -0,0 +1,62 @@
+import {
+ FETCH_ALL_SLICES_FAILED,
+ FETCH_ALL_SLICES_STARTED,
+ SET_ALL_SLICES,
+ UPDATE_SLICE_NAME,
+} from '../actions/sliceEntities';
+import { t } from '../../locales';
+
+export const initSliceEntities = {
+ slices: {},
+ isLoading: true,
+ errorMessage: null,
+ lastUpdated: 0,
+};
+
+export default function (state = initSliceEntities, action) {
+ const actionHandlers = {
+ [UPDATE_SLICE_NAME]() {
+ const updatedSlice = {
+ ...state.slices[action.key],
+ slice_name: action.sliceName,
+ };
+ const updatedSlices = {
+ ...state.slices,
+ [action.key]: updatedSlice,
+ };
+ return { ...state, slices: updatedSlices };
+ },
+ [FETCH_ALL_SLICES_STARTED]() {
+ return {
+ ...state,
+ isLoading: true,
+ };
+ },
+ [SET_ALL_SLICES]() {
+ return {
+ ...state,
+ isLoading: false,
+ slices: { ...state.slices, ...action.slices }, // append more slices
+ lastUpdated: new Date().getTime(),
+ };
+ },
+ [FETCH_ALL_SLICES_FAILED]() {
+ const respJSON = action.error.responseJSON;
+ const errorMessage =
+ t('Sorry, there was an error adding slices to this dashboard: ') +
+ (respJSON && respJSON.message) ? respJSON.message :
+ error.responseText;
+ return {
+ ...state,
+ isLoading: false,
+ errorMessage,
+ lastUpdated: new Date().getTime(),
+ };
+ },
+ };
+
+ if (action.type in actionHandlers) {
+ return actionHandlers[action.type]();
+ }
+ return state;
+}
diff --git a/superset/assets/src/dashboard/util/dashboardHelper.js b/superset/assets/src/dashboard/util/dashboardHelper.js
new file mode 100644
index 0000000..c9a6021
--- /dev/null
+++ b/superset/assets/src/dashboard/util/dashboardHelper.js
@@ -0,0 +1,9 @@
+export function getChartIdsFromLayout(layout) {
+ return Object.values(layout)
+ .reduce((chartIds, value) => {
+ if (value && value.meta && value.meta.chartId) {
+ chartIds.push(value.meta.chartId);
+ }
+ return chartIds;
+ }, []);
+}
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
new file mode 100644
index 0000000..854ca65
--- /dev/null
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -0,0 +1,322 @@
+/* eslint-disable no-param-reassign */
+/* eslint-disable camelcase */
+/* eslint-disable no-loop-func */
+import {
+ ROW_TYPE,
+ COLUMN_TYPE,
+ CHART_TYPE,
+ DASHBOARD_HEADER_TYPE,
+ DASHBOARD_ROOT_TYPE,
+ DASHBOARD_GRID_TYPE,
+} from '../v2/util/componentTypes';
+import {
+ DASHBOARD_GRID_ID,
+ DASHBOARD_HEADER_ID,
+ DASHBOARD_ROOT_ID,
+} from '../v2/util/constants';
+
+const MAX_RECURSIVE_LEVEL = 6;
+const GRID_RATIO = 4;
+const ROW_HEIGHT = 8;
+const generateId = (() => {
+ let componentId = 1;
+ return () => (componentId++);
+})();
+
+/**
+ *
+ * @param positions: single array of slices
+ * @returns boundary object {top: number, bottom: number, left: number, right: number}
+ */
+function getBoundary(positions) {
+ let top = Number.MAX_VALUE;
+ let bottom = 0;
+ let left = Number.MAX_VALUE;
+ let right = 1;
+ positions.forEach((item) => {
+ const { row, col, size_x, size_y } = item;
+ if (row <= top) top = row;
+ if (col <= left) left = col;
+ if (bottom <= row + size_y) bottom = row + size_y;
+ if (right <= col + size_x) right = col + size_x;
+ });
+
+ return {
+ top,
+ bottom,
+ left,
+ right,
+ };
+}
+
+function getRowContainer() {
+ const id = 'DASHBOARD_ROW_TYPE-' + generateId();
+ return {
+ version: 'v2',
+ type: ROW_TYPE,
+ id,
+ children: [],
+ meta: {
+ background: 'BACKGROUND_TRANSPARENT',
+ },
+ };
+}
+
+function getColContainer() {
+ const id = 'DASHBOARD_COLUMN_TYPE-' + generateId();
+ return {
+ version: 'v2',
+ type: COLUMN_TYPE,
+ id,
+ children: [],
+ meta: {
+ background: 'BACKGROUND_TRANSPARENT',
+ },
+ };
+}
+
+function getChartHolder(item) {
+ const { row, col, size_x, size_y, slice_id } = item;
+ const converted = {
+ row: Math.round(row / GRID_RATIO),
+ col: Math.floor((col - 1) / GRID_RATIO) + 1,
+ size_x: Math.max(1, Math.floor(size_x / GRID_RATIO)),
+ size_y: Math.max(1, Math.round(size_y / GRID_RATIO)),
+ slice_id,
+ };
+
+ return {
+ version: 'v2',
+ type: CHART_TYPE,
+ id: 'DASHBOARD_CHART_TYPE-' + generateId(),
+ children: [],
+ meta: {
+ width: converted.size_x,
+ height: Math.round(converted.size_y * 100 / ROW_HEIGHT),
+ chartId: slice_id,
+ },
+ };
+}
+
+function getChildrenMax(items, attr, layout) {
+ return Math.max.apply(null, items.map(child => (layout[child].meta[attr])));
+}
+
+function getChildrenSum(items, attr, layout) {
+ return items.reduce((preValue, child) => (preValue + layout[child].meta[attr]), 0);
+}
+
+function sortByRowId(item1, item2) {
+ return item1.row - item2.row;
+}
+
+function sortByColId(item1, item2) {
+ return item1.col - item2.col;
+}
+
+function hasOverlap(positions, xAxis = true) {
+ return positions.slice()
+ .sort(xAxis ? sortByColId : sortByRowId)
+ .some((item, index, arr) => {
+ if (index === arr.length - 1) {
+ return false;
+ }
+
+ if (xAxis) {
+ return (item.col + item.size_x) > arr[index + 1].col;
+ }
+ return (item.row + item.size_y) > arr[index + 1].row;
+ });
+}
+
+function doConvert(positions, level, parent, root) {
+ if (positions.length === 0) {
+ return;
+ }
+
+ if (positions.length === 1 || level >= MAX_RECURSIVE_LEVEL) {
+ // special treatment for single chart dash, always wrap chart inside a row
+ if (parent.type === 'DASHBOARD_GRID_TYPE') {
+ const rowContainer = getRowContainer();
+ root[rowContainer.id] = rowContainer;
+ parent.children.push(rowContainer.id);
+ parent = rowContainer;
+ }
+
+ const chartHolder = getChartHolder(positions[0]);
+ root[chartHolder.id] = chartHolder;
+ parent.children.push(chartHolder.id);
+ return;
+ }
+
+ let currentItems = positions.slice();
+ const { top, bottom, left, right } = getBoundary(positions);
+ // find row dividers
+ const layers = [];
+ let currentRow = top + 1;
+ while (currentItems.length && currentRow <= bottom) {
+ const upper = [];
+ const lower = [];
+
+ const isRowDivider = currentItems.every((item) => {
+ const { row, size_y } = item;
+ if (row + size_y <= currentRow) {
+ lower.push(item);
+ return true;
+ } else if (row >= currentRow) {
+ upper.push(item);
+ return true;
+ }
+ return false;
+ });
+
+ if (isRowDivider) {
+ currentItems = upper.slice();
+ layers.push(lower);
+ }
+ currentRow++;
+ }
+
+ layers.forEach((layer) => {
+ if (layer.length === 0) {
+ return;
+ }
+
+ if (layer.length === 1) {
+ const chartHolder = getChartHolder(layer[0]);
+ root[chartHolder.id] = chartHolder;
+ parent.children.push(chartHolder.id);
+ return;
+ }
+
+ // create a new row
+ const rowContainer = getRowContainer();
+ root[rowContainer.id] = rowContainer;
+ parent.children.push(rowContainer.id);
+
+ currentItems = layer.slice();
+ if (!hasOverlap(currentItems)) {
+ currentItems.sort(sortByColId).forEach((item) => {
+ const chartHolder = getChartHolder(item);
+ root[chartHolder.id] = chartHolder;
+ rowContainer.children.push(chartHolder.id);
+ });
+ } else {
+ // find col dividers for each layer
+ let currentCol = left + 1;
+ while (currentItems.length && currentCol <= right) {
+ const upper = [];
+ const lower = [];
+
+ const isColDivider = currentItems.every((item) => {
+ const { col, size_x } = item;
+ if (col + size_x <= currentCol) {
+ lower.push(item);
+ return true;
+ } else if (col >= currentCol) {
+ upper.push(item);
+ return true;
+ }
+ return false;
+ });
+
+ if (isColDivider) {
+ if (lower.length === 1) {
+ const chartHolder = getChartHolder(lower[0]);
+ root[chartHolder.id] = chartHolder;
+ rowContainer.children.push(chartHolder.id);
+ } else {
+ // create a new column
+ const colContainer = getColContainer();
+ root[colContainer.id] = colContainer;
+ rowContainer.children.push(colContainer.id);
+
+ if (!hasOverlap(lower, false)) {
+ lower.sort(sortByRowId).forEach((item) => {
+ const chartHolder = getChartHolder(item);
+ root[chartHolder.id] = chartHolder;
+ colContainer.children.push(chartHolder.id);
+ });
+ } else {
+ doConvert(lower, level + 2, colContainer, root);
+ }
+
+ // add col meta
+ colContainer.meta.width = getChildrenMax(colContainer.children, 'width', root);
+ }
+
+ currentItems = upper.slice();
+ }
+ currentCol++;
+ }
+ }
+
+ rowContainer.meta.width = getChildrenSum(rowContainer.children, 'width', root);
+ });
+}
+
+export default function (dashboard) {
+ const positions = [];
+
+ // position data clean up. some dashboard didn't have position_json
+ let { position_json } = dashboard;
+ const posDict = {};
+ if (Array.isArray(position_json)) {
+ position_json.forEach((position) => {
+ posDict[position.slice_id] = position;
+ });
+ } else {
+ position_json = [];
+ }
+
+ const lastRowId = Math.max(0, Math.max.apply(null,
+ position_json.map(pos => (pos.row + pos.size_y))));
+ let newSliceCounter = 0;
+ dashboard.slices.forEach((slice) => {
+ const sliceId = slice.slice_id;
+ let pos = posDict[sliceId];
+ if (!pos) {
+ // append new slices to dashboard bottom, 3 slices per row
+ pos = {
+ col: (newSliceCounter % 3) * 16 + 1,
+ row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
+ size_x: 16,
+ size_y: 16,
+ slice_id: String(sliceId),
+ };
+ newSliceCounter++;
+ }
+
+ positions.push(pos);
+ });
+
+ const root = {
+ [DASHBOARD_ROOT_ID]: {
+ version: 'v2',
+ type: DASHBOARD_ROOT_TYPE,
+ id: DASHBOARD_ROOT_ID,
+ children: [DASHBOARD_GRID_ID],
+ },
+ [DASHBOARD_GRID_ID]: {
+ type: DASHBOARD_GRID_TYPE,
+ id: DASHBOARD_GRID_ID,
+ children: [],
+ },
+ [DASHBOARD_HEADER_ID]: {
+ type: DASHBOARD_HEADER_TYPE,
+ id: DASHBOARD_HEADER_ID,
+ },
+ };
+ doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
+
+ // remove row's width/height and col's height
+ Object.values(root).forEach((item) => {
+ if (ROW_TYPE === item.type) {
+ const meta = item.meta;
+ delete meta.width;
+ }
+ });
+
+ // console.log(JSON.stringify(root));
+ return root;
+}
diff --git a/superset/assets/src/dashboard/v2/actions/messageToasts.js b/superset/assets/src/dashboard/v2/actions/messageToasts.js
index af10ead..2ebc06c 100644
--- a/superset/assets/src/dashboard/v2/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/v2/actions/messageToasts.js
@@ -6,7 +6,6 @@ function getToastUuid(type) {
export const ADD_TOAST = 'ADD_TOAST';
export function addToast({ toastType, text }) {
- debugger;
return {
type: ADD_TOAST,
payload: {
diff --git a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
index efef5a5..f9a37cc 100644
--- a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
@@ -1,37 +1,69 @@
import React from 'react';
-import PropTypes from 'prop-types';
+import cx from 'classnames';
-import NewChart from './gridComponents/new/NewChart';
import NewColumn from './gridComponents/new/NewColumn';
import NewDivider from './gridComponents/new/NewDivider';
import NewHeader from './gridComponents/new/NewHeader';
import NewRow from './gridComponents/new/NewRow';
import NewTabs from './gridComponents/new/NewTabs';
+import SliceAdderContainer from '../../../dashboard/components/SliceAdderContainer';
-const propTypes = {
- editMode: PropTypes.bool,
-};
+import '../stylesheets/builder-sidepane.less';
class BuilderComponentPane extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ showSlices: false,
+ };
+
+ this.openSlicesPane = this.showSlices.bind(this, true);
+ this.closeSlicesPane = this.showSlices.bind(this, false);
+ }
+
+ showSlices(show) {
+ this.setState({
+ showSlices: show,
+ });
+ }
+
render() {
return (
<div className="dashboard-builder-sidepane">
<div className="dashboard-builder-sidepane-header">
Insert components
+ {this.state.showSlices &&
+ <i className="fa fa-times close trigger" onClick={this.closeSlicesPane} role="none" />
+ }
</div>
- <NewChart />
- <NewHeader />
- <NewDivider />
+ <div className="component-layer">
+ <div
+ className="dragdroppable dragdroppable-row"
+ onClick={this.openSlicesPane}
+ role="none"
+ >
+ <div className="new-component static">
+ <div className="new-component-placeholder fa fa-area-chart" />
+ Chart
+ <i className="fa fa-arrow-right open trigger" />
+ </div>
+ </div>
- <NewTabs />
- <NewRow />
- <NewColumn />
+ <NewHeader />
+ <NewDivider />
+
+ <NewTabs />
+ <NewRow />
+ <NewColumn />
+ </div>
+
+ <div className={cx('slices-layer', this.state.showSlices && 'show')}>
+ <SliceAdderContainer />
+ </div>
</div>
);
}
}
-BuilderComponentPane.propTypes = propTypes;
-
export default BuilderComponentPane;
diff --git a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
index 8e2d985..f3f5867 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
@@ -20,15 +20,18 @@ import {
} from '../util/constants';
const propTypes = {
+ cells: PropTypes.object.isRequired,
+
// redux
dashboardLayout: PropTypes.object.isRequired,
deleteTopLevelTabs: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
+ showBuilderPane: PropTypes.bool,
handleComponentDrop: PropTypes.func.isRequired,
};
const defaultProps = {
- editMode: true,
+ showBuilderPane: false,
};
class DashboardBuilder extends React.Component {
@@ -105,6 +108,7 @@ class DashboardBuilder extends React.Component {
index={0}
renderTabContent={false}
onChangeTab={this.handleChangeTab}
+ cells={this.props.cells}
/>
</WithPopoverMenu>}
@@ -112,8 +116,11 @@ class DashboardBuilder extends React.Component {
<DashboardGrid
gridComponent={gridComponent}
depth={DASHBOARD_ROOT_DEPTH + 1}
+ cells={this.props.cells}
/>
- {editMode && <BuilderComponentPane />}
+ {this.props.editMode && this.props.showBuilderPane &&
+ <BuilderComponentPane />
+ }
</div>
<ToastPresenter />
</div>
diff --git a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
index 9f4cb93..2aa82af 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
@@ -71,7 +71,7 @@ class DashboardGrid extends React.PureComponent {
}
render() {
- const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
+ const { gridComponent, handleComponentDrop, depth, editMode, cells } = this.props;
const { isResizing, rowGuideTop } = this.state;
return (
@@ -93,6 +93,7 @@ class DashboardGrid extends React.PureComponent {
index={index}
availableColumnCount={GRID_COLUMN_COUNT}
columnWidth={columnWidth}
+ cells={cells}
onResizeStart={this.handleResizeStart}
onResize={this.handleResize}
onResizeStop={this.handleResizeStop}
diff --git a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
index ca204e5..d3ec7ac 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
@@ -52,7 +52,7 @@ class DashboardHeader extends React.Component {
<div className="dashboard-header">
<div className="dashboard-component-header header-large">
<EditableTitle
- title={component.meta.text}
+ title={'Test title'}
onSaveTitle={this.handleChangeText}
showTooltip={false}
canEdit={editMode}
diff --git a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
index 55d7e1d..54ce67e 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
+++ b/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
@@ -17,6 +17,7 @@ export const dragConfig = [
return {
type: component.type,
id: component.id,
+ meta: component.meta,
index,
parentId: parentComponent.id,
parentType: parentComponent.type,
diff --git a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
index f27b604..7cb630d 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
@@ -35,6 +35,7 @@ export default function handleDrop(props, monitor, Component) {
dragging: {
id: draggingItem.id,
type: draggingItem.type,
+ meta: draggingItem.meta,
},
};
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
similarity index 94%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
index 668d268..2aed4b2 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
@@ -19,6 +19,7 @@ const propTypes = {
index: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
editMode: PropTypes.bool.isRequired,
+ chart: PropTypes.object.isRequired,
// grid related
availableColumnCount: PropTypes.number.isRequired,
@@ -35,7 +36,7 @@ const propTypes = {
const defaultProps = {
};
-class Chart extends React.Component {
+class ChartHolder extends React.Component {
constructor(props) {
super(props);
this.state = {
@@ -112,7 +113,7 @@ class Chart extends React.Component {
editMode={editMode}
>
<div className="dashboard-component dashboard-component-chart">
- <div className="fa fa-area-chart" />
+ {this.props.chart}
</div>
{dropIndicatorProps && <div {...dropIndicatorProps} />}
@@ -124,7 +125,7 @@ class Chart extends React.Component {
}
}
-Chart.propTypes = propTypes;
-Chart.defaultProps = defaultProps;
+ChartHolder.propTypes = propTypes;
+ChartHolder.defaultProps = defaultProps;
-export default Chart;
+export default ChartHolder;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
index fe5a721..490d7bd 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
@@ -25,6 +25,7 @@ const propTypes = {
index: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
editMode: PropTypes.bool.isRequired,
+ cells: PropTypes.object.isRequired,
// grid related
availableColumnCount: PropTypes.number.isRequired,
@@ -92,6 +93,7 @@ class Column extends React.PureComponent {
onResizeStop,
handleComponentDrop,
editMode,
+ cells,
} = this.props;
const columnItems = columnComponent.children || [];
@@ -154,19 +156,20 @@ class Column extends React.PureComponent {
</HoverMenu>}
{columnItems.map((componentId, itemIndex) => (
- <DashboardComponent
- key={componentId}
- id={componentId}
- parentId={columnComponent.id}
- depth={depth + 1}
- index={itemIndex}
- availableColumnCount={columnComponent.meta.width}
- columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
- />
- ))}
+ <DashboardComponent
+ key={componentId}
+ id={componentId}
+ parentId={columnComponent.id}
+ depth={depth + 1}
+ index={itemIndex }
+ availableColumnCount={columnComponent.meta.width}
+ columnWidth={columnWidth}
+ cells={cells}
+ onResizeStart={onResizeStart}
+ onResize={onResize}
+ onResizeStop={onResizeStop}
+ />
+ ))}
{dropIndicatorProps && <div {...dropIndicatorProps} />}
</div>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
index 9866bc8..8faaee1 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
@@ -23,6 +23,7 @@ const propTypes = {
index: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
editMode: PropTypes.bool.isRequired,
+ cells: PropTypes.object.isRequired,
// grid related
availableColumnCount: PropTypes.number.isRequired,
@@ -92,6 +93,7 @@ class Row extends React.PureComponent {
onResizeStop,
handleComponentDrop,
editMode,
+ cells,
} = this.props;
const rowItems = rowComponent.children || [];
@@ -142,19 +144,20 @@ class Row extends React.PureComponent {
</HoverMenu>}
{rowItems.map((componentId, itemIndex) => (
- <DashboardComponent
- key={componentId}
- id={componentId}
- parentId={rowComponent.id}
- depth={depth + 1}
- index={itemIndex}
- availableColumnCount={availableColumnCount - occupiedColumnCount}
- columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
- />
- ))}
+
+ <DashboardComponent
+ key={componentId}
+ id={componentId}
+ parentId={rowComponent.id}
+ depth={depth + 1}
+ index={itemIndex }
+ availableColumnCount={availableColumnCount - occupiedColumnCount}
+ columnWidth={columnWidth}
+ cells={cells}onResizeStart={onResizeStart}
+ onResize={onResize}
+ onResizeStop={onResizeStop}
+ />
+ ))}
{dropIndicatorProps && <div {...dropIndicatorProps} />}
</div>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/index.js b/superset/assets/src/dashboard/v2/components/gridComponents/index.js
index 96c9a19..ef6d13f 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/index.js
@@ -9,7 +9,7 @@ import {
TABS_TYPE,
} from '../../util/componentTypes';
-import Chart from './Chart';
+import ChartHolder from './ChartHolder';
import Column from './Column';
import Divider from './Divider';
import Header from './Header';
@@ -17,7 +17,7 @@ import Row from './Row';
import Tab from './Tab';
import Tabs from './Tabs';
-export { default as Chart } from './Chart';
+export { default as ChartHolder } from './ChartHolder';
export { default as Column } from './Column';
export { default as Divider } from './Divider';
export { default as Header } from './Header';
@@ -26,7 +26,7 @@ export { default as Tab } from './Tab';
export { default as Tabs } from './Tabs';
export default {
- [CHART_TYPE]: Chart,
+ [CHART_TYPE]: ChartHolder,
[COLUMN_TYPE]: Column,
[DIVIDER_TYPE]: Divider,
[HEADER_TYPE]: Header,
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
index b8d717e..62fc94a 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
@@ -7,10 +7,12 @@ import {
handleComponentDrop,
} from '../actions/dashboardLayout';
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
return {
dashboardLayout: undoableLayout.present,
- editMode,
+ cells: ownProps.cells,
+ editMode: dashboard.editMode,
+ showBuilderPane: dashboard.showBuilderPane,
};
}
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
index add5a6d..01f7805 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import ComponentLookup from '../components/gridComponents';
import getTotalChildWidth from '../util/getChildWidth';
import { componentShape } from '../util/propShapes';
-import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
+import { CHART_TYPE, COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
import {
@@ -25,14 +25,14 @@ const propTypes = {
handleComponentDrop: PropTypes.func.isRequired,
};
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
const dashboardLayout = undoableLayout.present;
- const { id, parentId } = ownProps;
+ const { id, parentId, cells } = ownProps;
const component = dashboardLayout[id];
const props = {
component,
parentComponent: dashboardLayout[parentId],
- editMode,
+ editMode: dashboard.editMode,
};
// rows and columns need more data about their child dimensions
@@ -51,6 +51,11 @@ function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps
);
}
});
+ } else if (props.component.type === CHART_TYPE) {
+ const chartId = props.component.meta && props.component.meta.chartId;
+ if (chartId) {
+ props.chart = cells[chartId];
+ }
}
return props;
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
index 67b2396..2adc390 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
@@ -7,6 +7,13 @@ import {
resizeComponent,
} from '../actions/dashboardLayout';
+function mapStateToProps({ dashboardState: dashboard }, ownProps) {
+ return {
+ editMode: dashboard.editMode,
+ cells: ownProps.cells,
+ };
+}
+
function mapDispatchToProps(dispatch) {
return bindActionCreators({
handleComponentDrop,
@@ -14,4 +21,4 @@ function mapDispatchToProps(dispatch) {
}, dispatch);
}
-export default connect(({ editMode }) => ({ editMode }), mapDispatchToProps)(DashboardGrid);
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
index 8855d2c..cc8e944 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
@@ -2,32 +2,55 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import DashboardHeader from '../components/DashboardHeader';
-import { DASHBOARD_HEADER_ID } from '../util/constants';
-
+import DashboardHeader from '../../components/Header';
+import {
+ setEditMode,
+ toggleBuilderPane,
+ fetchFaveStar,
+ saveFaveStar,
+ fetchCharts,
+ startPeriodicRender,
+ updateDashboardTitle,
+ onChange,
+ onSave,
+} from '../../actions/dashboardState';
import {
- updateComponents,
handleComponentDrop,
} from '../actions/dashboardLayout';
-import { setEditMode } from '../actions/editMode';
-
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard,
+ dashboardInfo, charts }) {
return {
- component: undoableLayout.present[DASHBOARD_HEADER_ID],
+ dashboardInfo,
canUndo: undoableLayout.past.length > 0,
canRedo: undoableLayout.future.length > 0,
- editMode,
+ layout: undoableLayout.present,
+ filters: dashboard.filters,
+ dashboardTitle: dashboard.title,
+ expandedSlices: dashboard.expandedSlices,
+ charts,
+ userId: dashboardInfo.userId,
+ isStarred: !!dashboard.isStarred,
+ hasUnsavedChanges: !!dashboard.hasUnsavedChanges,
+ editMode: !!dashboard.editMode,
+ showBuilderPane: !!dashboard.showBuilderPane,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
- updateComponents,
handleComponentDrop,
onUndo: UndoActionCreators.undo,
onRedo: UndoActionCreators.redo,
setEditMode,
+ toggleBuilderPane,
+ fetchFaveStar,
+ saveFaveStar,
+ fetchCharts,
+ startPeriodicRender,
+ updateDashboardTitle,
+ onChange,
+ onSave,
}, dispatch);
}
diff --git a/superset/assets/src/dashboard/v2/reducers/index.js b/superset/assets/src/dashboard/v2/reducers/index.js
index 731734d..061255d 100644
--- a/superset/assets/src/dashboard/v2/reducers/index.js
+++ b/superset/assets/src/dashboard/v2/reducers/index.js
@@ -1,17 +1,8 @@
-import { combineReducers } from 'redux';
import undoable, { distinctState } from 'redux-undo';
import dashboardLayout from './dashboardLayout';
-import editMode from './editMode';
-import messageToasts from './messageToasts';
-const undoableLayout = undoable(dashboardLayout, {
+export default undoable(dashboardLayout, {
limit: 15,
filter: distinctState(),
});
-
-export default combineReducers({
- dashboardLayout: undoableLayout,
- editMode,
- messageToasts,
-});
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
new file mode 100644
index 0000000..d9f1069
--- /dev/null
+++ b/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
@@ -0,0 +1,103 @@
+.dashboard-builder-sidepane {
+ .trigger {
+ height: 25px;
+ width: 25px;
+ color: #879399;
+ position: relative;
+
+ &.close {
+ top: 3px;
+ }
+
+ &.open {
+ position: absolute;
+ right: 14px;
+ }
+ }
+
+ .component-layer {
+ .new-component.static {
+ cursor: pointer;
+ }
+ }
+
+ .slices-layer {
+ position: absolute;
+ width: 2px;
+ top: 51px;
+ right: 1px;
+ background: #fff;
+ transition-property: width;
+ transition-duration: 1s;
+ transition-timing-function: ease;
+ overflow: hidden;
+
+ &.show {
+ width: 374px;
+ }
+ }
+
+ .chart-card-container {
+ padding: 16px;
+ cursor: move;
+
+ .chart-card {
+ border: 1px solid #ccc;
+ height: 120px;
+ padding: 16px;
+ pointer-events: unset;
+ }
+
+ .chart-card.is-selected {
+ opacity: 0.45;
+ pointer-events: none;
+ }
+
+ .card-title {
+ margin-bottom: 8px;
+ font-weight: bold;
+ }
+
+ .card-body {
+ display: flex;
+ flex-direction: column;
+
+ .item {
+ height: 18px;
+ }
+
+ label {
+ margin-right: 5px;
+ }
+ }
+ }
+
+ .slice-adder-container {
+ .controls {
+ display: flex;
+ padding: 16px;
+
+ .dropdown.btn-group button,
+ input {
+ font-size: 14px;
+ line-height: 16px;
+ padding: 7px 12px;
+ height: 32px;
+ }
+
+ input {
+ margin-left: 16px;
+ width: 169px;
+ border: 1px solid #b3b3b3;
+
+ &:focus {
+ outline: none;
+ }
+ }
+ }
+
+ .ReactVirtualized__Grid.ReactVirtualized__List:focus {
+ outline: none;
+ }
+ }
+}
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder.less b/superset/assets/src/dashboard/v2/stylesheets/builder.less
index 3651c57..2ff99a4 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/v2/stylesheets/builder.less
@@ -1,5 +1,5 @@
.dashboard-v2 {
- margin-top: -20px;
+ //margin-top: -20px;
position: relative;
color: @almost-black;
}
@@ -48,6 +48,7 @@
flex: 0 0 376px;
border: 1px solid @gray-light;
z-index: 1;
+ position: relative;
}
.dashboard-builder-sidepane-header {
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
index 141c3e9..ce03797 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
+++ b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
@@ -7,10 +7,11 @@
display: flex;
align-items: center;
justify-content: center;
+ position: relative;
}
.dashboard-component-chart .fa {
- font-size: 100px;
+ //font-size: 100px;
opacity: 0.3;
}
diff --git a/superset/assets/src/dashboard/v2/util/constants.js b/superset/assets/src/dashboard/v2/util/constants.js
index 36ef71b..f35614c 100644
--- a/superset/assets/src/dashboard/v2/util/constants.js
+++ b/superset/assets/src/dashboard/v2/util/constants.js
@@ -18,7 +18,7 @@ export const DASHBOARD_ROOT_DEPTH = 0;
export const GRID_BASE_UNIT = 8;
export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
export const GRID_COLUMN_COUNT = 12;
-export const GRID_MIN_COLUMN_COUNT = 3;
+export const GRID_MIN_COLUMN_COUNT = 2;
export const GRID_MIN_ROW_UNITS = 5;
export const GRID_MAX_ROW_UNITS = 100;
export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE;
diff --git a/superset/assets/src/dashboard/v2/util/newComponentFactory.js b/superset/assets/src/dashboard/v2/util/newComponentFactory.js
index af69eb8..b428ddd 100644
--- a/superset/assets/src/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/v2/util/newComponentFactory.js
@@ -34,7 +34,7 @@ function uuid(type) {
return `${type}-${Math.random().toString(16)}`;
}
-export default function entityFactory(type) {
+export default function entityFactory(type, meta) {
return {
version: 'v0',
type,
@@ -42,6 +42,7 @@ export default function entityFactory(type) {
children: [],
meta: {
...typeToDefaultMetaData[type],
+ ...meta,
},
};
}
diff --git a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
index 9e49643..7cccc5f 100644
--- a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
@@ -11,9 +11,10 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
const { dragging, destination } = dropResult;
const dragType = dragging.type;
+ const dragMeta = dragging.meta;
const dropEntity = components[destination.id];
const dropType = dropEntity.type;
- let newDropChild = newComponentFactory(dragType);
+ let newDropChild = newComponentFactory(dragType, dragMeta);
const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
const newEntities = {
diff --git a/superset/assets/src/dashboard/v2/util/propShapes.jsx b/superset/assets/src/dashboard/v2/util/propShapes.jsx
index 8acc192..388c726 100644
--- a/superset/assets/src/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/v2/util/propShapes.jsx
@@ -16,7 +16,6 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
height: PropTypes.number,
// Header
- text: PropTypes.string,
headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)),
// Row
@@ -29,3 +28,52 @@ export const toastShape = PropTypes.shape({
toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
text: PropTypes.string.isRequired,
});
+
+export const chartPropShape = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ chartAlert: PropTypes.string,
+ chartStatus: PropTypes.string,
+ chartUpdateEndTime: PropTypes.number,
+ chartUpdateStartTime: PropTypes.number,
+ latestQueryFormData: PropTypes.object,
+ queryRequest: PropTypes.object,
+ queryResponse: PropTypes.object,
+ triggerQuery: PropTypes.bool,
+ lastRendered: PropTypes.number,
+});
+
+export const slicePropShape = PropTypes.shape({
+ slice_id: PropTypes.number.isRequired,
+ slice_url: PropTypes.string.isRequired,
+ slice_name: PropTypes.string.isRequired,
+ edit_url: PropTypes.string.isRequired,
+ datasource: PropTypes.string,
+ datasource_name: PropTypes.string,
+ datasource_link: PropTypes.string,
+ changedOn: PropTypes.number,
+ modified: PropTypes.string,
+ viz_type: PropTypes.string.isRequired,
+ description: PropTypes.string,
+ description_markeddown: PropTypes.string,
+});
+
+export const dashboardStatePropShape = PropTypes.shape({
+ title: PropTypes.string.isRequired,
+ sliceIds: PropTypes.object.isRequired,
+ refresh: PropTypes.bool.isRequired,
+ filters: PropTypes.object,
+ expandedSlices: PropTypes.object,
+ editMode: PropTypes.bool,
+ showBuilderPane: PropTypes.bool,
+ hasUnsavedChanges: PropTypes.bool,
+});
+
+export const dashboardInfoPropShape = PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ metadata: PropTypes.object,
+ slug: PropTypes.string,
+ dash_edit_perm: PropTypes.bool.isRequired,
+ dash_save_perm: PropTypes.bool.isRequired,
+ common: PropTypes.object,
+ userId: PropTypes.string.isRequired,
+});
diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx
index 69871dc..19416b0 100644
--- a/superset/assets/src/explore/components/ExploreChartHeader.jsx
+++ b/superset/assets/src/explore/components/ExploreChartHeader.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
import ExploreActionButtons from './ExploreActionButtons';
import RowCountLabel from './RowCountLabel';
import EditableTitle from '../../components/EditableTitle';
@@ -28,13 +28,13 @@ const propTypes = {
table_name: PropTypes.string,
form_data: PropTypes.object,
timeout: PropTypes.number,
- chart: PropTypes.shape(chartPropType),
+ chart: chartPropShape,
};
class ExploreChartHeader extends React.PureComponent {
runQuery() {
this.props.actions.runQuery(this.props.form_data, true,
- this.props.timeout, this.props.chart.chartKey);
+ this.props.timeout, this.props.chart.id);
}
updateChartTitleOrSaveSlice(newTitle) {
diff --git a/superset/assets/src/explore/components/ExploreChartPanel.jsx b/superset/assets/src/explore/components/ExploreChartPanel.jsx
index bfb24ff..21c6a64 100644
--- a/superset/assets/src/explore/components/ExploreChartPanel.jsx
+++ b/superset/assets/src/explore/components/ExploreChartPanel.jsx
@@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Panel } from 'react-bootstrap';
-import { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
import ChartContainer from '../../chart/ChartContainer';
import ExploreChartHeader from './ExploreChartHeader';
@@ -27,7 +27,7 @@ const propTypes = {
standalone: PropTypes.bool,
timeout: PropTypes.number,
refreshOverlayVisible: PropTypes.bool,
- chart: PropTypes.shape(chartPropType),
+ chart: chartPropShape,
errorMessage: PropTypes.node,
};
@@ -45,7 +45,7 @@ class ExploreChartPanel extends React.PureComponent {
formData={this.props.form_data}
height={this.getHeight()}
slice={this.props.slice}
- chartKey={this.props.chart.chartKey}
+ chartId={this.props.chart.id}
setControlValue={this.props.actions.setControlValue}
timeout={this.props.timeout}
vizType={this.props.vizType}
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index bc875d4..3e761eb 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -11,7 +11,7 @@ import QueryAndSaveBtns from './QueryAndSaveBtns';
import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils';
import { areObjectsEqual } from '../../reduxUtils';
import { getFormDataFromControls } from '../store';
-import { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
import * as exploreActions from '../actions/exploreActions';
import * as saveModalActions from '../actions/saveModalActions';
import * as chartActions from '../../chart/chartAction';
@@ -22,7 +22,7 @@ const propTypes = {
actions: PropTypes.object.isRequired,
datasource_type: PropTypes.string.isRequired,
isDatasourceMetaLoading: PropTypes.bool.isRequired,
- chart: PropTypes.shape(chartPropType).isRequired,
+ chart: chartPropShape.isRequired,
slice: PropTypes.object,
controls: PropTypes.object.isRequired,
forcedHeight: PropTypes.string,
@@ -72,7 +72,7 @@ class ExploreViewContainer extends React.Component {
}
if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
this.props.actions.resetControls();
- this.props.actions.triggerQuery(true, this.props.chart.chartKey);
+ this.props.actions.triggerQuery(true, this.props.chart.id);
}
if (
np.controls.datasource && (
@@ -86,8 +86,8 @@ class ExploreViewContainer extends React.Component {
const changedControlKeys = this.findChangedControlKeys(this.props.controls, np.controls);
if (this.hasDisplayControlChanged(changedControlKeys, np.controls)) {
this.props.actions.updateQueryFormData(
- getFormDataFromControls(np.controls), this.props.chart.chartKey);
- this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.chartKey);
+ getFormDataFromControls(np.controls), this.props.chart.id);
+ this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.id);
}
if (this.hasQueryControlChanged(changedControlKeys, np.controls)) {
this.setState({ chartIsStale: true, refreshOverlayVisible: true });
@@ -112,7 +112,7 @@ class ExploreViewContainer extends React.Component {
onQuery() {
// remove alerts when query
this.props.actions.removeControlPanelAlert();
- this.props.actions.triggerQuery(true, this.props.chart.chartKey);
+ this.props.actions.triggerQuery(true, this.props.chart.id);
this.setState({ chartIsStale: false, refreshOverlayVisible: false });
this.addHistory({});
@@ -158,7 +158,7 @@ class ExploreViewContainer extends React.Component {
triggerQueryIfNeeded() {
if (this.props.chart.triggerQuery && !this.hasErrors()) {
this.props.actions.runQuery(this.props.form_data, false,
- this.props.timeout, this.props.chart.chartKey);
+ this.props.timeout, this.props.chart.id);
}
}
@@ -198,7 +198,7 @@ class ExploreViewContainer extends React.Component {
formData,
false,
this.props.timeout,
- this.props.chart.chartKey,
+ this.props.chart.id,
);
}
}
diff --git a/superset/assets/src/explore/exploreUtils.js b/superset/assets/src/explore/exploreUtils.js
index 1c1271b..fcab33f 100644
--- a/superset/assets/src/explore/exploreUtils.js
+++ b/superset/assets/src/explore/exploreUtils.js
@@ -3,7 +3,7 @@ import URI from 'urijs';
export function getChartKey(explore) {
const slice = explore.slice;
- return slice ? ('slice_' + slice.slice_id) : 'slice';
+ return slice ? (slice.slice_id) : 0;
}
export function getAnnotationJsonUrl(slice_id, form_data, isNative) {
diff --git a/superset/assets/src/explore/index.jsx b/superset/assets/src/explore/index.jsx
index 5989b5f..07870e4 100644
--- a/superset/assets/src/explore/index.jsx
+++ b/superset/assets/src/explore/index.jsx
@@ -49,7 +49,7 @@ const chartKey = getChartKey(bootstrappedState);
const initState = {
charts: {
[chartKey]: {
- chartKey,
+ id: chartKey,
chartAlert: null,
chartStatus: 'loading',
chartUpdateEndTime: null,
diff --git a/superset/assets/src/explore/reducers/index.js b/superset/assets/src/explore/reducers/index.js
index 13d0ed1..953b0b5 100644
--- a/superset/assets/src/explore/reducers/index.js
+++ b/superset/assets/src/explore/reducers/index.js
@@ -1,13 +1,14 @@
import { combineReducers } from 'redux';
-import shortid from 'shortid';
import charts from '../../chart/chartReducer';
import saveModal from './saveModalReducer';
import explore from './exploreReducer';
+const impressionId = (state = '') => (state);
+
export default combineReducers({
charts,
saveModal,
explore,
- impressionId: () => (shortid.generate()),
+ impressionId,
});
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index 016444a..c4ea9ce 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -165,7 +165,6 @@ export const controllerInterface = {
addFiler: () => {},
setFilter: () => {},
getFilters: () => false,
- clearFilter: () => {},
removeFilter: () => {},
filters: {},
};
diff --git a/superset/assets/src/visualizations/table.css b/superset/assets/src/visualizations/table.css
index a5b8462..9af0c0e 100644
--- a/superset/assets/src/visualizations/table.css
+++ b/superset/assets/src/visualizations/table.css
@@ -30,11 +30,10 @@ table.table thead th.sorting:after, table.table thead th.sorting_asc:after, tabl
white-space: pre-wrap;
}
+.widget.table {
+ width: auto;
+ max-width: unset;
+}
.widget.table thead tr {
height: 25px;
}
-
-.dashboard .slice_container.table {
- padding-left: 10px;
- padding-right: 10px;
-}
diff --git a/superset/assets/stylesheets/dashboard.css b/superset/assets/stylesheets/dashboard.less
similarity index 58%
rename from superset/assets/stylesheets/dashboard.css
rename to superset/assets/stylesheets/dashboard.less
index c1f08a7..b812a42 100644
--- a/superset/assets/stylesheets/dashboard.css
+++ b/superset/assets/stylesheets/dashboard.less
@@ -1,3 +1,5 @@
+@import "./less/cosmo/variables.less";
+
.dashboard a i {
cursor: pointer;
}
@@ -11,28 +13,58 @@
border-color: #AAA;
opacity: 0.3;
}
-div.widget .chart-controls {
- background-clip: content-box;
+.dashboard .widget {
position: absolute;
- z-index: 100;
- right: 0;
- top: 5px;
- padding: 5px 5px;
- opacity: 0;
- transition: opacity 0.5s ease-in-out;
-}
-div.widget:hover .chart-controls {
- opacity: 0.75;
- transition: opacity 0.5s ease-in-out;
-}
-.slice-grid div.widget {
- border-radius: 0;
- border: 0;
+ top: 16px;
+ left: 16px;
box-shadow: none;
- background-color: #fff;
+ background-color: transparent;
overflow: visible;
}
+.dashboard .chart-header {
+ .dropdown.btn-group {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+
+ .dropdown-menu.dropdown-menu-right {
+ right: 7px;
+ top: -3px
+ }
+}
+
+.slice-header-controls-trigger {
+ border: 0;
+ padding: 0 0 0 20px;
+ background: none;
+ outline: none;
+ box-shadow: none;
+ color: #263238;
+ &.is-cached {
+ color: red;
+ }
+
+ &:hover, &:focus {
+ background: none;
+ cursor: pointer;
+ }
+
+ .controls-container.dropdown-menu {
+ top: 0;
+ left: unset;
+ right: 10px;
+
+ &.is-open {
+ display: block;
+ }
+
+ & li {
+ white-space: nowrap;
+ }
+ }
+}
.slice-grid .slice_container {
background-color: #fff;
}
@@ -73,26 +105,16 @@ div.widget:hover .chart-controls {
display: none;
}
-.slice-grid div.separator.widget {
- border: 1px solid transparent;
- box-shadow: none;
- z-index: 1;
-}
-.slice-grid div.separator.widget:hover {
- border: 1px solid #EEE;
-}
-.slice-grid div.separator.widget .chart-header {
- background-color: transparent;
- color: transparent;
-}
-.slice-grid div.separator.widget h1,h2,h3,h4 {
- margin-top: 0px;
-}
-
.slice-cell {
box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
transition: box-shadow 1s ease-in;
- height: 100%;
+
+ .dropdown,
+ .dropdown-menu {
+ .fa {
+ font-size: 14px;
+ }
+ }
}
.slice-cell-highlight {
@@ -104,30 +126,8 @@ div.widget:hover .chart-controls {
font-weight: bold;
}
-.dashboard .separator.widget .slice_container {
- padding: 0;
- overflow: visible;
-}
-.dashboard .separator.widget .slice_container hr {
- margin-top: 5px;
- margin-bottom: 5px;
-}
-.separator .chart-container {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
-}
-
-.dashboard .title {
- margin: 0 20px;
-}
-
-.dashboard .title .favstar {
- font-size: 20px;
- position: relative;
- top: -5px;
+.chart-container {
+ box-sizing: border-box;
}
.chart-header .header {
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 743daa8..6987544 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -162,7 +162,6 @@ li.widget:hover {
div.widget .chart-header {
padding-top: 8px;
color: #333;
- border-bottom: 1px solid #aaa;
margin: 0 10px;
}
@@ -177,10 +176,6 @@ div.widget .chart-header {
}
-div.widget .chart-header a {
- margin-left: 5px;
-}
-
#is_cached {
display: none;
}
@@ -458,6 +453,17 @@ g.annotation-container {
border-color: @brand-primary;
}
+.fave-unfave-icon {
+ .fa-star-o,
+ .fa-star {
+ &,
+ &:hover,
+ &:active {
+ color: #263238;
+ }
+ }
+}
+
.metric-edit-popover-label-input {
border-radius: 4px;
height: 30px;
diff --git a/superset/models/core.py b/superset/models/core.py
index 4674520..b450be0 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -146,6 +146,11 @@ class Slice(Model, AuditMixinNullable, ImportMixin):
datasource = self.datasource
return datasource.link if datasource else None
+ def datasource_name_text(self):
+ # pylint: disable=no-member
+ datasource = self.datasource
+ return datasource.name if datasource else None
+
@property
def datasource_edit_url(self):
# pylint: disable=no-member
@@ -338,14 +343,6 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
@property
def url(self):
- if self.json_metadata:
- # add default_filters to the preselect_filters of dashboard
- json_metadata = json.loads(self.json_metadata)
- default_filters = json_metadata.get('default_filters')
- if default_filters:
- filters = parse.quote(default_filters.encode('utf8'))
- return '/superset/dashboard/{}/?preselect_filters={}'.format(
- self.slug or self.id, filters)
return '/superset/dashboard/{}/'.format(self.slug or self.id)
@property
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index 25633da..1a158d9 100644
--- a/superset/templates/superset/dashboard.html
+++ b/superset/templates/superset/dashboard.html
@@ -3,6 +3,7 @@
{% block body %}
<div
id="app"
+ class="dashboard container-fluid"
data-bootstrap="{{ bootstrap_data }}"
>
</div>
diff --git a/superset/views/core.py b/superset/views/core.py
index 3a0f28f..bd7715d 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -518,9 +518,10 @@ appbuilder.add_view_no_menu(SliceAsync)
class SliceAddView(SliceModelView): # noqa
list_columns = [
- 'id', 'slice_name', 'slice_link', 'viz_type',
- 'datasource_link', 'owners', 'modified', 'changed_on']
- show_columns = list(set(SliceModelView.edit_columns + list_columns))
+ 'id', 'slice_name', 'slice_url', 'edit_url', 'viz_type', 'params',
+ 'description', 'description_markeddown',
+ 'datasource_name_text', 'datasource_link',
+ 'owners', 'modified', 'changed_on']
appbuilder.add_view_no_menu(SliceAddView)
@@ -1593,9 +1594,17 @@ class Superset(BaseSupersetView):
@staticmethod
def _set_dash_metadata(dashboard, data):
positions = data['positions']
- slice_ids = [int(d['slice_id']) for d in positions]
- dashboard.slices = [o for o in dashboard.slices if o.id in slice_ids]
- positions = sorted(data['positions'], key=lambda x: int(x['slice_id']))
+ # find slices in the position data
+ slice_ids = []
+ for value in positions.values():
+ if value.get('meta') and value.get('meta').get('chartId'):
+ slice_ids.append(int(value.get('meta').get('chartId')))
+ session = db.session()
+ Slice = models.Slice # noqa
+ current_slices = session.query(Slice).filter(
+ Slice.id.in_(slice_ids)).all()
+
+ dashboard.slices = current_slices
dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
md = dashboard.params_dict
dashboard.css = data['css']
@@ -1608,7 +1617,11 @@ class Superset(BaseSupersetView):
if 'filter_immune_slice_fields' not in md:
md['filter_immune_slice_fields'] = {}
md['expanded_slices'] = data['expanded_slices']
- md['default_filters'] = data.get('default_filters', '')
+ default_filters_data = json.loads(data.get('default_filters', ''))
+ for key in default_filters_data.keys():
+ if int(key) not in slice_ids:
+ del default_filters_data[key]
+ md['default_filters'] = json.dumps(default_filters_data)
dashboard.json_metadata = json.dumps(md, indent=4)
@api