[Dashboard] fix a filter refresh bug and add Test (#3967)

This commit is contained in:
Grace Guo 2017-12-01 10:58:55 -08:00 committed by GitHub
parent 84a7730f47
commit 028456572b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 284 additions and 112 deletions

View File

@ -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 });

View File

@ -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,
@ -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'
@ -123,7 +123,7 @@ class Controls extends React.PureComponent {
/>
<SaveModal
dashboard={dashboard}
readFilters={readFilters}
filters={filters}
serialize={serialize}
onSave={onSave}
css={this.state.css}

View File

@ -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';
@ -30,6 +29,7 @@ const defaultProps = {
slices: {},
datasources: {},
filters: {},
refresh: false,
timeout: 60,
userId: '',
isStarred: false,
@ -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);
@ -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();
}
}
}
@ -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);
}
@ -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();
@ -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}

View File

@ -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,

View File

@ -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,
@ -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}

View File

@ -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,
};
@ -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;

View File

@ -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) {
//
}
@ -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 };
@ -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 };
@ -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 };
@ -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
@ -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 };
},
};

View File

@ -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);

View File

@ -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);
});
});
});

View File

@ -1,70 +1,134 @@
export const slice = {
token: 'token_089ec8c1',
csv_endpoint: '',
edit_url: '/slicemodelview/edit/39',
viz_name: 'filter_box',
json_endpoint: '',
slice_id: 39,
standalone_endpoint: '',
description_markeddown: '',
form_data: {
collapsed_fieldsets: null,
time_grain_sqla: 'Time Column',
granularity_sqla: 'year',
standalone: null,
date_filter: false,
until: '2014-01-02',
extra_filters: null,
force: null,
where: '',
since: '2014-01-01',
async: null,
slice_id: null,
json: null,
having: '',
flt_op_2: 'in',
previous_viz_type: 'filter_box',
groupby: [
'region',
'country_name',
],
flt_col_7: '',
slice_name: null,
viz_type: 'filter_box',
metric: 'sum__SP_POP_TOTL',
flt_col_8: '',
import { getInitialState } from '../../../javascripts/dashboard/reducers';
export const defaultFilters = {
256: {
region: [],
country_name: ['United States'],
},
slice_url: '',
slice_name: 'Region Filter',
description: null,
column_formats: {},
};
export const dashboardData = {
export const regionFilter = {
datasource: null,
description: null,
description_markeddown: '',
edit_url: '/slicemodelview/edit/256',
form_data: {
datasource: '2__table',
date_filter: false,
filters: [{
col: 'country_name',
op: 'in',
val: ['United States', 'France', 'Japan'],
}],
granularity_sqla: null,
groupby: ['region', 'country_name'],
having: '',
instant_filtering: true,
metric: 'sum__SP_POP_TOTL',
show_druid_time_granularity: false,
show_druid_time_origin: false,
show_sqla_time_column: false,
show_sqla_time_granularity: false,
since: '100 years ago',
slice_id: 256,
time_grain_sqla: null,
until: 'now',
viz_type: 'filter_box',
where: '',
},
slice_id: 256,
slice_name: 'Region Filters',
slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20256%7D',
};
export const slice = {
datasource: null,
description: null,
description_markeddown: '',
edit_url: '/slicemodelview/edit/248',
form_data: {
annotation_layers: [],
bottom_margin: 'auto',
color_scheme: 'bnbColors',
contribution: false,
datasource: '2__table',
filters: [],
granularity_sqla: null,
groupby: [],
having: '',
left_margin: 'auto',
limit: 50,
line_interpolation: 'linear',
metrics: ['sum__SP_POP_TOTL'],
num_period_compare: '',
order_desc: true,
period_ratio_type: 'growth',
resample_fillmethod: null,
resample_how: null,
resample_rule: null,
rich_tooltip: true,
rolling_type: 'None',
show_brush: false,
show_legend: true,
show_markers: false,
since: '1961-01-01T00:00:00',
slice_id: 248,
time_compare: null,
time_grain_sqla: null,
timeseries_limit_metric: null,
until: '2014-12-31T00:00:00',
viz_type: 'line',
where: '',
x_axis_format: 'smart_date',
x_axis_label: '',
x_axis_showminmax: true,
y_axis_bounds: [null, null],
y_axis_format: '.3s',
y_axis_label: '',
y_axis_showminmax: true,
y_log_scale: false,
},
slice_id: 248,
slice_name: 'Filtered Population',
slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
};
const datasources = {};
const mockDashboardData = {
css: '',
dash_edit_perm: true,
dash_save_perm: true,
dashboard_title: 'Births',
id: 2,
metadata: {
default_filters: JSON.stringify(defaultFilters),
filter_immune_slices: [],
timed_refresh_immune_slices: [],
filter_immune_slice_fields: {},
expanded_slices: {},
},
slug: 'births',
position_json: [
{
size_x: 2,
slice_id: '52',
size_x: 4,
slice_id: '256',
row: 0,
size_y: 2,
size_y: 4,
col: 5,
},
{
size_x: 4,
slice_id: '248',
row: 0,
size_y: 4,
col: 1,
},
],
id: 2,
slices: [slice],
dashboard_title: 'Births',
};
export const contextData = {
dash_save_perm: true,
slug: 'births',
slices: [regionFilter, slice],
standalone_mode: false,
dash_edit_perm: true,
userId: '1',
};
export const { dashboard, charts } = getInitialState({
common: {},
dashboard_data: mockDashboardData,
datasources,
user_id: '1',
});

View File

@ -0,0 +1,30 @@
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { dashboard as reducers } from '../../../javascripts/dashboard/reducers';
import * as actions from '../../../javascripts/dashboard/actions';
import { defaultFilters, dashboard as initState } from './fixtures';
describe('Dashboard reducers', () => {
it('should remove slice', () => {
const action = {
type: actions.REMOVE_SLICE,
slice: initState.dashboard.slices[1],
};
const { dashboard, filters, refresh } = reducers(initState, action);
expect(dashboard.slices).to.have.length(1);
expect(filters).to.deep.equal(defaultFilters);
expect(refresh).to.equal(false);
});
it('should remove filter slice', () => {
const action = {
type: actions.REMOVE_SLICE,
slice: initState.dashboard.slices[0],
};
const { dashboard, filters, refresh } = reducers(initState, action);
expect(dashboard.slices).to.have.length(1);
expect(filters).to.deep.equal({});
expect(refresh).to.equal(true);
});
});