Skip to content

Commit

Permalink
[Dashboard] fix a filter refresh bug and add Test (apache#3967)
Browse files Browse the repository at this point in the history
  • Loading branch information
Grace Guo authored Dec 1, 2017
1 parent 84a7730 commit 0284565
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class CodeModal extends React.PureComponent {
}
beforeOpen() {
let code = this.props.code;
if (this.props.codeCallback) {
if (!code && this.props.codeCallback) {
code = this.props.codeCallback();
}
this.setState({ code });
Expand Down
8 changes: 4 additions & 4 deletions superset/assets/javascripts/dashboard/components/Controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ const $ = window.$ = require('jquery');

const propTypes = {
dashboard: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
slices: PropTypes.array,
userId: PropTypes.string.isRequired,
addSlicesToDashboard: PropTypes.func,
onSave: PropTypes.func,
onChange: PropTypes.func,
readFilters: PropTypes.func,
renderSlices: PropTypes.func,
serialize: PropTypes.func,
startPeriodicRender: PropTypes.func,
Expand Down Expand Up @@ -92,8 +92,8 @@ class Controls extends React.PureComponent {
this.props.onChange();
}
render() {
const { dashboard, userId,
addSlicesToDashboard, startPeriodicRender, readFilters,
const { dashboard, userId, filters,
addSlicesToDashboard, startPeriodicRender,
serialize, onSave, editMode } = this.props;
const emailBody = t('Checkout this dashboard: %s', window.location.href);
const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
Expand Down Expand Up @@ -123,7 +123,7 @@ class Controls extends React.PureComponent {
/>
<SaveModal
dashboard={dashboard}
readFilters={readFilters}
filters={filters}
serialize={serialize}
onSave={onSave}
css={this.state.css}
Expand Down
50 changes: 16 additions & 34 deletions superset/assets/javascripts/dashboard/components/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import AlertsWrapper from '../../components/AlertsWrapper';
import GridLayout from './GridLayout';
import Header from './Header';
import { getExploreUrl } from '../../explore/exploreUtils';
import { areObjectsEqual } from '../../reduxUtils';
import { t } from '../../locales';

Expand All @@ -30,6 +29,7 @@ const defaultProps = {
slices: {},
datasources: {},
filters: {},
refresh: false,
timeout: 60,
userId: '',
isStarred: false,
Expand All @@ -50,7 +50,6 @@ class Dashboard extends React.PureComponent {
this.onSave = this.onSave.bind(this);
this.onChange = this.onChange.bind(this);
this.serialize = this.serialize.bind(this);
this.readFilters = this.readFilters.bind(this);
this.fetchAllSlices = this.fetchSlices.bind(this, this.getAllSlices());
this.startPeriodicRender = this.startPeriodicRender.bind(this);
this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this);
Expand All @@ -69,14 +68,18 @@ class Dashboard extends React.PureComponent {
}

componentDidMount() {
this.loadPreSelectFilters();
this.firstLoad = false;
window.addEventListener('resize', this.rerenderCharts);
}

componentDidUpdate(prevProps) {
if (!areObjectsEqual(prevProps.filters, this.props.filters) && this.props.refresh) {
Object.keys(this.props.filters).forEach(sliceId => (this.refreshExcept(sliceId)));
const currentFilterKeys = Object.keys(this.props.filters);
if (currentFilterKeys.length) {
currentFilterKeys.forEach(key => (this.refreshExcept(key)));
} else {
this.refreshExcept();
}
}
}

Expand Down Expand Up @@ -160,31 +163,15 @@ class Dashboard extends React.PureComponent {
return f;
}

jsonEndpoint(data, force = false) {
let endpoint = getExploreUrl(data, 'json', force);
if (endpoint.charAt(0) !== '/') {
// Known issue for IE <= 11:
// https://connect.microsoft.com/IE/feedbackdetail/view/1002846/pathname-incorrect-for-out-of-document-elements
endpoint = '/' + endpoint;
}
return endpoint;
}

loadPreSelectFilters() {
for (const key in this.props.filters) {
for (const col in this.props.filters[key]) {
const sliceId = parseInt(key, 10);
this.props.actions.addFilter(sliceId, col,
this.props.filters[key][col], false, false,
);
}
}
}

refreshExcept(sliceId) {
refreshExcept(filterKey) {
const immune = this.props.dashboard.metadata.filter_immune_slices || [];
const slices = this.getAllSlices()
.filter(slice => slice.slice_id !== sliceId && immune.indexOf(slice.slice_id) === -1);
let slices = this.getAllSlices();
if (filterKey) {
slices = slices.filter(slice => (
String(slice.slice_id) !== filterKey &&
immune.indexOf(slice.slice_id) === -1
));
}
this.fetchSlices(slices);
}

Expand Down Expand Up @@ -213,11 +200,6 @@ class Dashboard extends React.PureComponent {
fetchAndRender();
}

readFilters() {
// Returns a list of human readable active filters
return JSON.stringify(this.props.filters, null, ' ');
}

updateDashboardTitle(title) {
this.props.actions.updateDashboardTitle(title);
this.onChange();
Expand Down Expand Up @@ -280,13 +262,13 @@ class Dashboard extends React.PureComponent {
<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}
readFilters={this.readFilters}
fetchFaveStar={this.props.actions.fetchFaveStar}
saveFaveStar={this.props.actions.saveFaveStar}
renderSlices={this.fetchAllSlices}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function mapStateToProps({ charts, dashboard }) {
slices: charts,
datasources: dashboard.datasources,
filters: dashboard.filters,
refresh: dashboard.refresh,
refresh: !!dashboard.refresh,
userId: dashboard.userId,
isStarred: !!dashboard.isStarred,
editMode: dashboard.editMode,
Expand Down
4 changes: 2 additions & 2 deletions superset/assets/javascripts/dashboard/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { t } from '../../locales';

const propTypes = {
dashboard: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
isStarred: PropTypes.bool,
addSlicesToDashboard: PropTypes.func,
onSave: PropTypes.func,
onChange: PropTypes.func,
fetchFaveStar: PropTypes.func,
readFilters: PropTypes.func,
renderSlices: PropTypes.func,
saveFaveStar: PropTypes.func,
serialize: PropTypes.func,
Expand Down Expand Up @@ -95,11 +95,11 @@ class Header extends React.PureComponent {
{this.renderEditButton()}
<Controls
dashboard={dashboard}
filters={this.props.filters}
userId={this.props.userId}
addSlicesToDashboard={this.props.addSlicesToDashboard}
onSave={this.props.onSave}
onChange={this.props.onChange}
readFilters={this.props.readFilters}
renderSlices={this.props.renderSlices}
serialize={this.props.serialize}
startPeriodicRender={this.props.startPeriodicRender}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const propTypes = {
css: PropTypes.string,
dashboard: PropTypes.object.isRequired,
triggerNode: PropTypes.node.isRequired,
readFilters: PropTypes.func,
filters: PropTypes.object.isRequired,
serialize: PropTypes.func,
onSave: PropTypes.func,
};
Expand Down Expand Up @@ -81,7 +81,7 @@ class SaveModal extends React.PureComponent {
css: this.state.css,
expanded_slices: dashboard.metadata.expanded_slices || {},
dashboard_title: dashboard.dashboard_title,
default_filters: this.props.readFilters(),
default_filters: JSON.stringify(this.props.filters),
duplicate_slices: this.state.duplicateSlices,
};
let url = null;
Expand Down
36 changes: 21 additions & 15 deletions superset/assets/javascripts/dashboard/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@ export function getInitialState(bootstrapData) {
delete common.language_pack;

const dashboard = { ...bootstrapData.dashboard_data };
const filters = {};
let filters = {};
try {
// allow request parameter overwrite dashboard metadata
const filterData = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
for (const key in filterData) {
const sliceId = parseInt(key, 10);
filters[sliceId] = filterData[key];
}
filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
} catch (e) {
//
}
Expand Down Expand Up @@ -87,7 +83,7 @@ export function getInitialState(bootstrapData) {
};
}

const dashboard = function (state = {}, action) {
export const dashboard = function (state = {}, action) {
const actionHandlers = {
[actions.UPDATE_DASHBOARD_TITLE]() {
const newDashboard = { ...state.dashboard, dashboard_title: action.title };
Expand All @@ -98,11 +94,22 @@ const dashboard = function (state = {}, action) {
return { ...state, dashboard: newDashboard };
},
[actions.REMOVE_SLICE]() {
const newLayout = state.dashboard.layout.filter(function (reactPos) {
return reactPos.i !== String(action.slice.slice_id);
});
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');
return { ...state, dashboard: { ...newDashboard, layout: newLayout } };
// 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 };
Expand Down Expand Up @@ -158,8 +165,7 @@ const dashboard = function (state = {}, action) {
[actions.CLEAR_FILTER]() {
const newFilters = { ...state.filters };
delete newFilters[action.sliceId];

return { ...state.dashboard, filter: newFilters, refresh: true };
return { ...state, filter: newFilters, refresh: true };
},
[actions.REMOVE_FILTER]() {
const newFilters = { ...state.filters };
Expand All @@ -176,7 +182,7 @@ const dashboard = function (state = {}, action) {
newFilters[sliceId][col] = a;
}
}
return { ...state.dashboard, filter: newFilters, refresh: true };
return { ...state, filter: newFilters, refresh: true };
},

// slice reducer
Expand All @@ -185,7 +191,7 @@ const dashboard = function (state = {}, action) {
state.dashboard, 'slices',
action.slice, { slice_name: action.sliceName },
'slice_id');
return { ...state.dashboard, dashboard: newDashboard };
return { ...state, dashboard: newDashboard };
},
};

Expand Down
1 change: 1 addition & 0 deletions superset/assets/spec/helpers/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ global.assert = chai.assert;
global.sinon.useFakeXMLHttpRequest();

global.window.XMLHttpRequest = global.XMLHttpRequest;
global.window.location = { href: 'about:blank' };
global.$ = require('jquery')(global.window);
89 changes: 89 additions & 0 deletions superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 '../../../javascripts/dashboard/actions';
import * as chartActions from '../../../javascripts/chart/chartAction';
import Dashboard from '../../../javascripts/dashboard/components/Dashboard';
import { defaultFilters, dashboard, charts } from './fixtures';

describe('Dashboard', () => {
const mockedProps = {
actions: { ...chartActions, ...dashboardActions },
initMessages: [],
dashboard: dashboard.dashboard,
slices: charts,
filters: dashboard.filters,
datasources: dashboard.datasources,
refresh: false,
timeout: 60,
isStarred: false,
userId: dashboard.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(2);
});

it('should handle metadata default_filters', () => {
const wrapper = shallow(<Dashboard {...mockedProps} />);
expect(wrapper.instance().props.filters).deep.equal(defaultFilters);
});

describe('getFormDataExtra', () => {
let wrapper;
let selectedSlice;
beforeEach(() => {
wrapper = shallow(<Dashboard {...mockedProps} />);
selectedSlice = wrapper.instance().props.dashboard.slices[1];
});

it('should carry default_filters', () => {
const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).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({
filters: {
256: {
region: [],
country_name: ['France'],
},
},
});
const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['France'] });
});
});

describe('refreshExcept', () => {
let wrapper;
let spy;
beforeEach(() => {
wrapper = shallow(<Dashboard {...mockedProps} />);
spy = sinon.spy(wrapper.instance(), 'fetchSlices');
});
afterEach(() => {
spy.restore();
});

it('should not refresh filter slice', () => {
const filterKey = Object.keys(defaultFilters)[0];
wrapper.instance().refreshExcept(filterKey);
expect(spy.callCount).to.equal(1);
expect(spy.getCall(0).args[0].length).to.equal(1);
});

it('should refresh all slices', () => {
wrapper.instance().refreshExcept();
expect(spy.callCount).to.equal(1);
expect(spy.getCall(0).args[0].length).to.equal(2);
});
});
});
Loading

0 comments on commit 0284565

Please sign in to comment.