This is an automated email from the ASF dual-hosted git repository.
graceguo pushed a commit to branch feature--dashboard-scoped-filter
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to
refs/heads/feature--dashboard-scoped-filter by this push:
new f869e19 [WIP][dashboard scoped filter] part 3: merge filter scope
settings into dashboard redux state (#8522)
f869e19 is described below
commit f869e1920e3fb4688a1bc6440d55441509ecc649
Author: Grace Guo <[email protected]>
AuthorDate: Thu Nov 14 17:07:09 2019 -0800
[WIP][dashboard scoped filter] part 3: merge filter scope settings into
dashboard redux state (#8522)
* merge filter scope settings into dashboard redux state
* fix/add unit tests
* minor bug fixes
* fix save filter Scopes behavior
* resolve review comments
* fix save filter scope settings
* minor comments
---
.../dashboard/actions/dashboardLayout_spec.js | 70 +++++++----
.../dashboard/components/Dashboard_spec.jsx | 121 ++++---------------
.../components/FilterIndicatorsContainer_spec.jsx | 43 ++++---
.../dashboard/fixtures/mockDashboardFilters.js | 1 +
.../dashboard/fixtures/mockDashboardLayout.js | 51 ++++++++
.../dashboard/reducers/dashboardFilters_spec.js | 35 +++++-
.../dashboard/util/getDashboardUrl_spec.js | 8 +-
.../util/getFormDataWithExtraFilters_spec.js | 32 +----
.../src/dashboard/actions/dashboardFilters.js | 16 +++
.../src/dashboard/actions/dashboardLayout.js | 40 ++++++-
.../assets/src/dashboard/actions/dashboardState.js | 17 ++-
.../assets/src/dashboard/components/Dashboard.jsx | 82 +++++--------
.../components/FilterIndicatorsContainer.jsx | 89 ++++++--------
.../assets/src/dashboard/components/Header.jsx | 5 -
.../dashboard/components/HeaderActionsDropdown.jsx | 6 +-
.../assets/src/dashboard/components/SaveModal.jsx | 4 -
.../src/dashboard/components/SliceHeader.jsx | 2 -
.../dashboard/components/SliceHeaderControls.jsx | 5 +-
.../components/filterscope/FilterFieldTree.jsx | 27 +----
.../components/filterscope/FilterScopeModal.jsx | 2 +-
.../components/filterscope/FilterScopeSelector.jsx | 65 ++++++-----
.../components/filterscope/FilterScopeTree.jsx | 27 +----
.../{FilterScopeTree.jsx => treeIcons.jsx} | 54 +--------
.../dashboard/components/gridComponents/Chart.jsx | 16 ++-
superset/assets/src/dashboard/containers/Chart.jsx | 11 +-
.../assets/src/dashboard/containers/Dashboard.jsx | 10 +-
.../src/dashboard/containers/DashboardHeader.jsx | 2 -
.../src/dashboard/containers/FilterIndicators.jsx | 6 +-
.../src/dashboard/containers/FilterScope.jsx | 11 +-
.../src/dashboard/reducers/dashboardFilters.js | 53 ++++++++-
.../src/dashboard/reducers/dashboardLayout.js | 1 +
.../src/dashboard/reducers/getInitialState.js | 49 +++++++-
.../stylesheets/filter-scope-selector.less | 1 +
.../src/dashboard/util/activeDashboardFilters.js | 129 ++++++++++++++++-----
.../util/charts/getEffectiveExtraFilters.js | 47 +-------
.../util/charts/getFormDataWithExtraFilters.js | 13 +--
.../src/dashboard/util/getCurrentScopeChartIds.js | 62 ----------
.../assets/src/dashboard/util/getDashboardUrl.js | 9 +-
.../src/dashboard/util/getFilterFieldNodesTree.js | 2 +-
.../src/dashboard/util/getFilterScopeNodesTree.js | 2 +-
...erScopeTree.js => getFilterValuesByFilterId.js} | 24 ++--
.../src/dashboard/util/getKeyForFilterScopeTree.js | 6 +-
...erScopeTree.js => isInDifferentFilterScopes.js} | 21 ++--
.../src/dashboard/util/newEntitiesFromDrop.js | 4 +
superset/assets/src/dashboard/util/propShapes.jsx | 27 +++--
...hboardUrl.js => serializeActiveFilterValues.js} | 21 +++-
.../dashboard/util/serializeFilterScopes.js} | 25 ++--
superset/views/core.py | 14 ++-
48 files changed, 706 insertions(+), 662 deletions(-)
diff --git
a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index 0dfca9c..1da0568 100644
--- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -39,6 +39,7 @@ import {
} from '../../../../src/dashboard/actions/dashboardLayout';
import { setUnsavedChanges } from
'../../../../src/dashboard/actions/dashboardState';
+import * as dashboardFilters from
'../../../../src/dashboard/actions/dashboardFilters';
import {
addWarningToast,
ADD_TOAST,
@@ -80,6 +81,12 @@ describe('dashboardLayout actions', () => {
return { getState, dispatch, state };
}
+ beforeEach(() => {
+ sinon.spy(dashboardFilters, 'updateLayoutComponents');
+ });
+ afterEach(() => {
+ dashboardFilters.updateLayoutComponents.restore();
+ });
describe('updateComponents', () => {
it('should dispatch an updateLayout action', () => {
@@ -92,6 +99,9 @@ describe('dashboardLayout actions', () => {
type: UPDATE_COMPONENTS,
payload: { nextComponents },
});
+
+ // update component should not trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0);
});
it('should dispatch a setUnsavedChanges action if
hasUnsavedChanges=false', () => {
@@ -101,8 +111,10 @@ describe('dashboardLayout actions', () => {
const nextComponents = { 1: {} };
const thunk = updateComponents(nextComponents);
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(2);
expect(dispatch.getCall(1).args[0]).toEqual(setUnsavedChanges(true));
+
+ // update component should not trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0);
});
});
@@ -111,11 +123,13 @@ describe('dashboardLayout actions', () => {
const { getState, dispatch } = setup();
const thunk = deleteComponent('id', 'parentId');
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(1);
expect(dispatch.getCall(0).args[0]).toEqual({
type: DELETE_COMPONENT,
payload: { id: 'id', parentId: 'parentId' },
});
+
+ // delete components should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
it('should dispatch a setUnsavedChanges action if
hasUnsavedChanges=false', () => {
@@ -124,8 +138,10 @@ describe('dashboardLayout actions', () => {
});
const thunk = deleteComponent('id', 'parentId');
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0]).toEqual(setUnsavedChanges(true));
+ expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true));
+
+ // delete components should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
});
@@ -149,7 +165,8 @@ describe('dashboardLayout actions', () => {
},
});
- expect(dispatch.callCount).toBe(2);
+ // update dashboard title should not trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0);
});
});
@@ -159,11 +176,13 @@ describe('dashboardLayout actions', () => {
const dropResult = {};
const thunk = createTopLevelTabs(dropResult);
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(1);
expect(dispatch.getCall(0).args[0]).toEqual({
type: CREATE_TOP_LEVEL_TABS,
payload: { dropResult },
});
+
+ // create top level tabs should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
it('should dispatch a setUnsavedChanges action if
hasUnsavedChanges=false', () => {
@@ -173,8 +192,10 @@ describe('dashboardLayout actions', () => {
const dropResult = {};
const thunk = createTopLevelTabs(dropResult);
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0]).toEqual(setUnsavedChanges(true));
+ expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true));
+
+ // create top level tabs should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
});
@@ -184,11 +205,13 @@ describe('dashboardLayout actions', () => {
const dropResult = {};
const thunk = deleteTopLevelTabs(dropResult);
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(1);
expect(dispatch.getCall(0).args[0]).toEqual({
type: DELETE_TOP_LEVEL_TABS,
payload: {},
});
+
+ // delete top level tabs should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
it('should dispatch a setUnsavedChanges action if
hasUnsavedChanges=false', () => {
@@ -198,8 +221,10 @@ describe('dashboardLayout actions', () => {
const dropResult = {};
const thunk = deleteTopLevelTabs(dropResult);
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0]).toEqual(setUnsavedChanges(true));
+ expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true));
+
+ // delete top level tabs should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
});
@@ -261,6 +286,9 @@ describe('dashboardLayout actions', () => {
thunk2(dispatch, getState);
expect(dispatch.callCount).toBe(3);
+
+ // resize components should not trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0);
});
});
@@ -286,7 +314,8 @@ describe('dashboardLayout actions', () => {
},
});
- expect(dispatch.callCount).toBe(2);
+ // create components should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
it('should move a component if the component is not new', () => {
@@ -315,7 +344,8 @@ describe('dashboardLayout actions', () => {
},
});
- expect(dispatch.callCount).toBe(2);
+ // create components should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
it('should dispatch a toast if the drop overflows the destination', () => {
@@ -379,9 +409,6 @@ describe('dashboardLayout actions', () => {
parentId: 'parentId',
},
});
-
- // move thunk, delete thunk, delete result actions
- expect(dispatch.callCount).toBe(3);
});
it('should create top-level tabs if dropped on root', () => {
@@ -404,8 +431,6 @@ describe('dashboardLayout actions', () => {
dropResult,
},
});
-
- expect(dispatch.callCount).toBe(2);
});
it('should dispatch a toast if drop top-level tab into nested tab', () => {
@@ -497,8 +522,10 @@ describe('dashboardLayout actions', () => {
const thunk = redoLayoutAction();
thunk(dispatch, getState);
- expect(dispatch.callCount).toBe(1);
expect(dispatch.getCall(0).args[0]).toEqual(UndoActionCreators.redo());
+
+ // redo/undo should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
it('should dispatch a setUnsavedChanges(true) action if
hasUnsavedChanges=false', () => {
@@ -507,9 +534,10 @@ describe('dashboardLayout actions', () => {
});
const thunk = redoLayoutAction();
thunk(dispatch, getState);
+ expect(dispatch.getCall(2).args[0]).toEqual(setUnsavedChanges(true));
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0]).toEqual(setUnsavedChanges(true));
+ // redo/undo should trigger action for dashboardFilters
+ expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
});
});
});
diff --git
a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
index 02e71e4..fbf44fa 100644
--- a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
@@ -24,7 +24,7 @@ import Dashboard from
'../../../../src/dashboard/components/Dashboard';
import DashboardBuilder from
'../../../../src/dashboard/containers/DashboardBuilder';
// mock data
-import chartQueries, { sliceId as chartId } from
'../fixtures/mockChartQueries';
+import chartQueries from '../fixtures/mockChartQueries';
import datasources from '../../../fixtures/mockDatasource';
import dashboardInfo from '../fixtures/mockDashboardInfo';
import { dashboardLayout } from '../fixtures/mockDashboardLayout';
@@ -46,7 +46,7 @@ describe('Dashboard', () => {
dashboardState,
dashboardInfo,
charts: chartQueries,
- filters: {},
+ activeFilters: {},
slices: sliceEntities.slices,
datasources,
layout: dashboardLayout.present,
@@ -61,10 +61,12 @@ describe('Dashboard', () => {
return wrapper;
}
+ // activeFilters map use id_column) as key
const OVERRIDE_FILTERS = {
- 1: { region: [] },
- 2: { country_name: ['USA'] },
- 3: { region: [], country_name: ['USA'] },
+ '1_region': [],
+ '2_country_name': ['USA'],
+ '3_region': [],
+ '3_country_name': ['USA'],
};
it('should render a DashboardBuilder', () => {
@@ -72,85 +74,6 @@ describe('Dashboard', () => {
expect(wrapper.find(DashboardBuilder)).toHaveLength(1);
});
- describe('refreshExcept', () => {
- const overrideDashboardInfo = {
- ...dashboardInfo,
- metadata: {
- ...dashboardInfo.metadata,
- filterImmuneSliceFields: { [chartQueries[chartId].id]: ['region'] },
- },
- };
-
- const overrideCharts = {
- ...chartQueries,
- 1001: {
- ...chartQueries[chartId],
- id: 1001,
- },
- };
-
- const overrideSlices = {
- ...props.slices,
- 1001: {
- ...props.slices[chartId],
- slice_id: 1001,
- },
- };
-
- it('should call triggerQuery for all non-exempt slices', () => {
- const wrapper = setup({ charts: overrideCharts, slices: overrideSlices
});
- const spy = sinon.spy(props.actions, 'triggerQuery');
- wrapper.instance().refreshExcept('1001');
- spy.restore();
- expect(spy.callCount).toBe(Object.keys(overrideCharts).length - 1);
- });
-
- it('should not call triggerQuery for filterImmuneSlices', () => {
- const wrapper = setup({
- charts: overrideCharts,
- dashboardInfo: {
- ...dashboardInfo,
- metadata: {
- ...dashboardInfo.metadata,
- filterImmuneSlices: Object.keys(overrideCharts).map(id =>
- Number(id),
- ),
- },
- },
- });
- const spy = sinon.spy(props.actions, 'triggerQuery');
- wrapper.instance().refreshExcept();
- spy.restore();
- expect(spy.callCount).toBe(0);
- });
-
- it('should not call triggerQuery for filterImmuneSliceFields', () => {
- const wrapper = setup({
- filters: OVERRIDE_FILTERS,
- dashboardInfo: overrideDashboardInfo,
- });
- const spy = sinon.spy(props.actions, 'triggerQuery');
- wrapper.instance().refreshExcept('1');
- expect(spy.callCount).toBe(0);
- spy.restore();
- });
-
- it('should call triggerQuery if filter has more filter-able fields', () =>
{
- const wrapper = setup({
- filters: OVERRIDE_FILTERS,
- dashboardInfo: overrideDashboardInfo,
- });
- const spy = sinon.spy(props.actions, 'triggerQuery');
-
- // if filter have additional fields besides immune ones,
- // should apply filter.
- wrapper.instance().refreshExcept('3');
- expect(spy.callCount).toBe(1);
-
- spy.restore();
- });
- });
-
describe('componentWillReceiveProps', () => {
const layoutWithExtraChart = {
...props.layout,
@@ -186,17 +109,17 @@ describe('Dashboard', () => {
describe('componentDidUpdate', () => {
let wrapper;
let prevProps;
- let refreshExceptSpy;
+ let refreshSpy;
beforeEach(() => {
- wrapper = setup({ filters: OVERRIDE_FILTERS });
+ wrapper = setup({ activeFilters: OVERRIDE_FILTERS });
wrapper.instance().appliedFilters = OVERRIDE_FILTERS;
prevProps = wrapper.instance().props;
- refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+ refreshSpy = sinon.spy(wrapper.instance(), 'refreshCharts');
});
afterEach(() => {
- refreshExceptSpy.restore();
+ refreshSpy.restore();
});
it('should not call refresh when is editMode', () => {
@@ -207,15 +130,15 @@ describe('Dashboard', () => {
},
});
wrapper.instance().componentDidUpdate(prevProps);
- expect(refreshExceptSpy.callCount).toBe(0);
+ expect(refreshSpy.callCount).toBe(0);
});
it('should not call refresh when there is no change', () => {
wrapper.setProps({
- filters: OVERRIDE_FILTERS,
+ activeFilters: OVERRIDE_FILTERS,
});
wrapper.instance().componentDidUpdate(prevProps);
- expect(refreshExceptSpy.callCount).toBe(0);
+ expect(refreshSpy.callCount).toBe(0);
expect(wrapper.instance().appliedFilters).toBe(OVERRIDE_FILTERS);
});
@@ -224,12 +147,12 @@ describe('Dashboard', () => {
gender: ['boy', 'girl'],
};
wrapper.setProps({
- filters: {
+ activeFilters: {
...OVERRIDE_FILTERS,
...newFilter,
},
});
- expect(refreshExceptSpy.callCount).toBe(1);
+ expect(refreshSpy.callCount).toBe(1);
expect(wrapper.instance().appliedFilters).toEqual({
...OVERRIDE_FILTERS,
...newFilter,
@@ -238,23 +161,23 @@ describe('Dashboard', () => {
it('should call refresh if a filter is removed', () => {
wrapper.setProps({
- filters: {},
+ activeFilters: {},
});
- expect(refreshExceptSpy.callCount).toBe(1);
+ expect(refreshSpy.callCount).toBe(1);
expect(wrapper.instance().appliedFilters).toEqual({});
});
it('should call refresh if a filter is changed', () => {
wrapper.setProps({
- filters: {
+ activeFilters: {
...OVERRIDE_FILTERS,
- region: ['Canada'],
+ '1_region': ['Canada'],
},
});
- expect(refreshExceptSpy.callCount).toBe(1);
+ expect(refreshSpy.callCount).toBe(1);
expect(wrapper.instance().appliedFilters).toEqual({
...OVERRIDE_FILTERS,
- region: ['Canada'],
+ '1_region': ['Canada'],
});
});
});
diff --git
a/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx
b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx
index 0e5392f..089f185 100644
---
a/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx
+++
b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx
@@ -20,28 +20,34 @@ import React from 'react';
import { shallow } from 'enzyme';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
+import { sliceId as chartId } from '../fixtures/mockChartQueries';
import { filterId, column } from '../fixtures/mockSliceEntities';
import FilterIndicatorsContainer from
'../../../../src/dashboard/components/FilterIndicatorsContainer';
import FilterIndicator from
'../../../../src/dashboard/components/FilterIndicator';
import * as colorMap from
'../../../../src/dashboard/util/dashboardFiltersColorMap';
+import { buildActiveFilters } from
'../../../../src/dashboard/util/activeDashboardFilters';
+import { getDashboardFilterKey } from
'../../../../src/dashboard/util/getDashboardFilterKey';
+import { DASHBOARD_ROOT_ID } from '../../../../src/dashboard/util/constants';
+import { dashboardWithFilter } from '../fixtures/mockDashboardLayout';
describe('FilterIndicatorsContainer', () => {
- const chartId = 1;
const mockedProps = {
dashboardFilters,
chartId,
chartStatus: 'success',
- filterImmuneSlices: [],
- filterImmuneSliceFields: {},
setDirectPathToChild: () => {},
filterFieldOnFocus: {},
};
- colorMap.getFilterColorKey = jest.fn(() => 'id_column');
colorMap.getFilterColorMap = jest.fn(() => ({
- id_column: 'badge-1',
+ [getDashboardFilterKey({ chartId, column })]: 'badge-1',
}));
+ buildActiveFilters({
+ dashboardFilters,
+ components: dashboardWithFilter,
+ });
+
function setup(overrideProps) {
return shallow(
<FilterIndicatorsContainer {...mockedProps} {...overrideProps} />,
@@ -58,18 +64,25 @@ describe('FilterIndicatorsContainer', () => {
expect(wrapper.find(FilterIndicator)).toHaveLength(0);
});
- it('should not show indicator when chart is immune', () => {
- const wrapper = setup({ filterImmuneSlices: [chartId] });
- expect(wrapper.find(FilterIndicator)).toHaveLength(0);
- });
-
- it('should not show indicator when chart field is immune', () => {
- const wrapper = setup({ filterImmuneSliceFields: { [chartId]: [column] }
});
- expect(wrapper.find(FilterIndicator)).toHaveLength(0);
- });
-
it('should show indicator', () => {
const wrapper = setup();
expect(wrapper.find(FilterIndicator)).toHaveLength(1);
});
+
+ it('should not show indicator when chart is immune', () => {
+ const overwriteDashboardFilters = {
+ ...dashboardFilters,
+ [filterId]: {
+ ...dashboardFilters[filterId],
+ scopes: {
+ region: {
+ scope: [DASHBOARD_ROOT_ID],
+ immune: [chartId],
+ },
+ },
+ },
+ };
+ const wrapper = setup({ dashboardFilters: overwriteDashboardFilters });
+ expect(wrapper.find(FilterIndicator)).toHaveLength(0);
+ });
});
diff --git
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
index 7f13db7..c5640c5 100644
---
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
+++
b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
@@ -36,6 +36,7 @@ export const dashboardFilters = {
],
scopes: {
region: DASHBOARD_FILTER_SCOPE_GLOBAL,
+ gender: DASHBOARD_FILTER_SCOPE_GLOBAL,
},
isDateFilter: false,
isInstantFilter: true,
diff --git
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
index 3267c5c..ee1dc4f 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
@@ -207,3 +207,54 @@ export const filterComponent = {
sliceName: 'Filter',
},
};
+
+export const dashboardWithFilter = {
+ [DASHBOARD_ROOT_ID]: {
+ type: DASHBOARD_ROOT_TYPE,
+ id: DASHBOARD_ROOT_ID,
+ children: [DASHBOARD_GRID_ID],
+ },
+
+ [DASHBOARD_GRID_ID]: {
+ type: DASHBOARD_GRID_TYPE,
+ id: DASHBOARD_GRID_ID,
+ children: ['ROW_ID'],
+ meta: {},
+ },
+
+ [DASHBOARD_HEADER_ID]: {
+ type: DASHBOARD_HEADER_TYPE,
+ id: DASHBOARD_HEADER_ID,
+ meta: {
+ text: 'New dashboard',
+ },
+ },
+
+ ROW_ID: {
+ ...newComponentFactory(ROW_TYPE),
+ id: 'ROW_ID',
+ children: ['CHART_ID', 'FILTER_ID'],
+ },
+
+ FILTER_ID: {
+ ...newComponentFactory(CHART_TYPE),
+ id: 'FILTER_ID',
+ meta: {
+ chartId: filterId,
+ width: 3,
+ height: 10,
+ chartName: 'filter name',
+ },
+ },
+
+ CHART_ID: {
+ ...newComponentFactory(CHART_TYPE),
+ id: 'CHART_ID',
+ meta: {
+ chartId,
+ width: 3,
+ height: 10,
+ chartName: 'Mock chart name',
+ },
+ },
+};
diff --git
a/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
b/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
index e234248..b759610 100644
---
a/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
+++
b/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
@@ -21,6 +21,7 @@ import {
ADD_FILTER,
REMOVE_FILTER,
CHANGE_FILTER,
+ UPDATE_DASHBOARD_FILTERS_SCOPE,
} from '../../../../src/dashboard/actions/dashboardFilters';
import dashboardFiltersReducer, {
DASHBOARD_FILTER_SCOPE_GLOBAL,
@@ -35,6 +36,7 @@ import {
column,
} from '../fixtures/mockSliceEntities';
import { filterComponent } from '../fixtures/mockDashboardLayout';
+import * as activeDashboardFilters from
'../../../../src/dashboard/util/activeDashboardFilters';
describe('dashboardFilters reducer', () => {
const form_data = sliceEntitiesForDashboard.slices[filterId].form_data;
@@ -98,6 +100,7 @@ describe('dashboardFilters reducer', () => {
},
scopes: {
[column]: DASHBOARD_FILTER_SCOPE_GLOBAL,
+ gender: DASHBOARD_FILTER_SCOPE_GLOBAL,
},
},
});
@@ -129,7 +132,8 @@ describe('dashboardFilters reducer', () => {
[column]: column,
},
scopes: {
- [column]: DASHBOARD_FILTER_SCOPE_GLOBAL,
+ region: DASHBOARD_FILTER_SCOPE_GLOBAL,
+ gender: DASHBOARD_FILTER_SCOPE_GLOBAL,
},
},
});
@@ -143,4 +147,33 @@ describe('dashboardFilters reducer', () => {
}),
).toEqual({});
});
+
+ it('should buildActiveFilters on UPDATE_DASHBOARD_FILTERS_SCOPE', () => {
+ const regionScope = {
+ scope: ['TAB-1'],
+ immune: [],
+ };
+ const genderScope = {
+ scope: ['ROOT_ID'],
+ immune: [1],
+ };
+ const scopes = {
+ [`${filterId}_region`]: regionScope,
+ [`${filterId}_gender`]: genderScope,
+ };
+ activeDashboardFilters.buildActiveFilters = jest.fn();
+ expect(
+ dashboardFiltersReducer(dashboardFilters, {
+ type: UPDATE_DASHBOARD_FILTERS_SCOPE,
+ scopes,
+ })[filterId].scopes,
+ ).toEqual({
+ region: regionScope,
+ gender: genderScope,
+ });
+
+ // when UPDATE_DASHBOARD_FILTERS_SCOPE is changed, applicable filters to a
chart
+ // might be changed.
+ expect(activeDashboardFilters.buildActiveFilters).toBeCalled();
+ });
});
diff --git
a/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
b/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
index 76a2da6..8b6b7fc 100644
--- a/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
@@ -17,10 +17,16 @@
* under the License.
*/
import getDashboardUrl from '../../../../src/dashboard/util/getDashboardUrl';
+import { DASHBOARD_FILTER_SCOPE_GLOBAL } from
'../../../../src/dashboard/reducers/dashboardFilters';
describe('getChartIdsFromLayout', () => {
it('should encode filters', () => {
- const filters = { 35: { key: ['value'] } };
+ const filters = {
+ '35_key': {
+ values: ['value'],
+ scope: DASHBOARD_FILTER_SCOPE_GLOBAL,
+ },
+ };
const url = getDashboardUrl('path', filters);
expect(url).toBe(
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D',
diff --git
a/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
b/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
index 544b2d4..95cb141 100644
---
a/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
+++
b/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
@@ -33,15 +33,9 @@ describe('getFormDataWithExtraFilters', () => {
],
},
},
- dashboardMetadata: {
- filterImmuneSlices: [],
- filterImmuneSliceFields: {},
- },
filters: {
- filterId: {
- region: ['Spain'],
- color: ['pink', 'purple'],
- },
+ region: ['Spain'],
+ color: ['pink', 'purple'],
},
sliceId: chartId,
};
@@ -60,26 +54,4 @@ describe('getFormDataWithExtraFilters', () => {
val: ['pink', 'purple'],
});
});
-
- it('should not add additional filters if the slice is immune to them', () =>
{
- const result = getFormDataWithExtraFilters({
- ...mockArgs,
- dashboardMetadata: {
- filterImmuneSlices: [chartId],
- },
- });
- expect(result.extra_filters).toHaveLength(0);
- });
-
- it('should not add additional filters for fields to which the slice is
immune', () => {
- const result = getFormDataWithExtraFilters({
- ...mockArgs,
- dashboardMetadata: {
- filterImmuneSliceFields: {
- [chartId]: ['region'],
- },
- },
- });
- expect(result.extra_filters).toHaveLength(1);
- });
});
diff --git a/superset/assets/src/dashboard/actions/dashboardFilters.js
b/superset/assets/src/dashboard/actions/dashboardFilters.js
index c2b8b9c..b8f92b8 100644
--- a/superset/assets/src/dashboard/actions/dashboardFilters.js
+++ b/superset/assets/src/dashboard/actions/dashboardFilters.js
@@ -46,11 +46,13 @@ export const CHANGE_FILTER = 'CHANGE_FILTER';
export function changeFilter(chartId, newSelectedValues, merge) {
return (dispatch, getState) => {
if (isValidFilter(getState, chartId)) {
+ const components = getState().dashboardLayout.present;
return dispatch({
type: CHANGE_FILTER,
chartId,
newSelectedValues,
merge,
+ components,
});
}
return getState().dashboardFilters;
@@ -66,3 +68,17 @@ export function updateDirectPathToFilter(chartId, path) {
return getState().dashboardFilters;
};
}
+
+export const UPDATE_LAYOUT_COMPONENTS = 'UPDATE_LAYOUT_COMPONENTS';
+export function updateLayoutComponents(components) {
+ return dispatch => {
+ dispatch({ type: UPDATE_LAYOUT_COMPONENTS, components });
+ };
+}
+
+export const UPDATE_DASHBOARD_FILTERS_SCOPE = 'UPDATE_DASHBOARD_FILTERS_SCOPE';
+export function updateDashboardFiltersScope(scopes) {
+ return dispatch => {
+ dispatch({ type: UPDATE_DASHBOARD_FILTERS_SCOPE, scopes });
+ };
+}
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js
b/superset/assets/src/dashboard/actions/dashboardLayout.js
index b276e37..1122918 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -20,6 +20,7 @@ import { ActionCreators as UndoActionCreators } from
'redux-undo';
import { t } from '@superset-ui/translation';
import { addWarningToast } from '../../messageToasts/actions';
+import { updateLayoutComponents } from './dashboardFilters';
import { setUnsavedChanges } from './dashboardState';
import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes';
import {
@@ -29,6 +30,10 @@ import {
} from '../util/constants';
import dropOverflowsParent from '../util/dropOverflowsParent';
import findParentId from '../util/findParentId';
+import isInDifferentFilterScopes from '../util/isInDifferentFilterScopes';
+
+// Component CRUD -------------------------------------------------------------
+export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
// this is a helper that takes an action as input and dispatches
// an additional setUnsavedChanges(true) action after the dispatch in the case
@@ -42,15 +47,22 @@ function setUnsavedChangesAfterAction(action) {
dispatch(result);
}
+ const isComponentLevelEvent =
+ result.type === UPDATE_COMPONENTS &&
+ result.payload &&
+ result.payload.nextComponents;
+ // trigger dashboardFilters state update if dashboard layout is changed.
+ if (!isComponentLevelEvent) {
+ const components = getState().dashboardLayout.present;
+ dispatch(updateLayoutComponents(components));
+ }
+
if (!getState().dashboardState.hasUnsavedChanges) {
dispatch(setUnsavedChanges(true));
}
};
}
-// Component CRUD -------------------------------------------------------------
-export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
-
export const updateComponents = setUnsavedChangesAfterAction(
nextComponents => ({
type: UPDATE_COMPONENTS,
@@ -199,12 +211,13 @@ export function handleComponentDrop(dropResult) {
// call getState() again down here in case redux state is stale after
// previous dispatch(es)
- const { dashboardLayout: undoableLayout } = getState();
+ const { dashboardFilters, dashboardLayout: undoableLayout } = getState();
// if we moved a child from a Tab or Row parent and it was the only child,
delete the parent.
if (!isNewComponent) {
const { present: layout } = undoableLayout;
- const sourceComponent = layout[source.id];
+ const sourceComponent = layout[source.id] || {};
+ const destinationComponent = layout[destination.id] || {};
if (
(sourceComponent.type === TABS_TYPE ||
sourceComponent.type === ROW_TYPE) &&
@@ -217,6 +230,23 @@ export function handleComponentDrop(dropResult) {
});
dispatch(deleteComponent(source.id, parentId));
}
+
+ // show warning if item has been moved between different scope
+ if (
+ isInDifferentFilterScopes({
+ dashboardFilters,
+ source: (sourceComponent.parents || []).concat(source.id),
+ destination: (destinationComponent.parents || []).concat(
+ destination.id,
+ ),
+ })
+ ) {
+ dispatch(
+ addWarningToast(
+ t('This chart has been moved to a different filter scope.'),
+ ),
+ );
+ }
}
return null;
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js
b/superset/assets/src/dashboard/actions/dashboardState.js
index b1d850f..cea6e25 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -38,6 +38,10 @@ import {
addDangerToast,
} from '../../messageToasts/actions';
import { UPDATE_COMPONENTS_PARENTS_LIST } from '../actions/dashboardLayout';
+import serializeActiveFilterValues from '../util/serializeActiveFilterValues';
+import serializeFilterScopes from '../util/serializeFilterScopes';
+import { getActiveFilters } from '../util/activeDashboardFilters';
+import { safeStringify } from '../../utils/safeStringify';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
export function setUnsavedChanges(hasUnsavedChanges) {
@@ -178,10 +182,19 @@ export function saveDashboardRequest(data, id, saveType) {
directPathToFilter.push(componentId);
dispatch(updateDirectPathToFilter(chartId, directPathToFilter));
});
-
+ // serialize selected values for each filter field, grouped by filter id
+ const serializedFilters = serializeActiveFilterValues(getActiveFilters());
+ // serialize filter scope for each filter field, grouped by filter id
+ const serializedFilterScopes = serializeFilterScopes(dashboardFilters);
return SupersetClient.post({
endpoint: `/superset/${path}/${id}/`,
- postPayload: { data },
+ postPayload: {
+ data: {
+ ...data,
+ default_filters: safeStringify(serializedFilters),
+ filter_scopes: safeStringify(serializedFilterScopes),
+ },
+ },
})
.then(response => {
dispatch(saveDashboardRequestSuccess());
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx
b/superset/assets/src/dashboard/components/Dashboard.jsx
index 334704e..441f807 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -28,9 +28,7 @@ import {
slicePropShape,
dashboardInfoPropShape,
dashboardStatePropShape,
- loadStatsPropShape,
} from '../util/propShapes';
-import { areObjectsEqual } from '../../reduxUtils';
import { LOG_ACTIONS_MOUNT_DASHBOARD } from '../../logger/LogUtils';
import OmniContainer from '../../components/OmniContainer';
import { safeStringify } from '../../utils/safeStringify';
@@ -48,9 +46,8 @@ const propTypes = {
dashboardState: dashboardStatePropShape.isRequired,
charts: PropTypes.objectOf(chartPropShape).isRequired,
slices: PropTypes.objectOf(slicePropShape).isRequired,
- filters: PropTypes.object.isRequired,
+ activeFilters: PropTypes.object.isRequired,
datasources: PropTypes.object.isRequired,
- loadStats: loadStatsPropShape.isRequired,
layout: PropTypes.object.isRequired,
impressionId: PropTypes.string.isRequired,
initMessages: PropTypes.array,
@@ -118,31 +115,42 @@ class Dashboard extends React.PureComponent {
const { hasUnsavedChanges, editMode } = this.props.dashboardState;
const appliedFilters = this.appliedFilters;
- const { filters } = this.props;
+ const { activeFilters } = this.props;
// do not apply filter when dashboard in edit mode
- if (!editMode && safeStringify(appliedFilters) !== safeStringify(filters))
{
+ if (
+ !editMode &&
+ safeStringify(appliedFilters) !== safeStringify(activeFilters)
+ ) {
// refresh charts if a filter was removed, added, or changed
- let changedFilterKey = null;
- const currFilterKeys = Object.keys(filters);
+ const currFilterKeys = Object.keys(activeFilters);
const appliedFilterKeys = Object.keys(appliedFilters);
- currFilterKeys.forEach(key => {
- if (
- // filter was added or changed
- typeof appliedFilters[key] === 'undefined' ||
- !areObjectsEqual(appliedFilters[key], filters[key])
+ const allKeys = new Set(currFilterKeys.concat(appliedFilterKeys));
+ const affectedChartIds = [];
+ [...allKeys].forEach(filterKey => {
+ if (!currFilterKeys.includes(filterKey)) {
+ // removed filter?
+ [].push.apply(affectedChartIds, appliedFilters[filterKey].scope);
+ } else if (!appliedFilterKeys.includes(filterKey)) {
+ // added filter?
+ [].push.apply(affectedChartIds, activeFilters[filterKey].scope);
+ } else if (
+ safeStringify(activeFilters[filterKey].values) !==
+ safeStringify(appliedFilters[filterKey].values) ||
+ safeStringify(activeFilters[filterKey].scope) !==
+ safeStringify(appliedFilters[filterKey].scope)
) {
- changedFilterKey = key;
+ // changed filter field value?
+ const affectedScope = activeFilters[filterKey].scope.concat(
+ appliedFilters[filterKey].scope,
+ );
+ [].push.apply(affectedChartIds, affectedScope);
}
});
- if (
- !!changedFilterKey ||
- currFilterKeys.length !== appliedFilterKeys.length // remove 1 or more
filters
- ) {
- this.refreshExcept(changedFilterKey);
- this.appliedFilters = filters;
- }
+ const idSet = new Set(affectedChartIds);
+ this.refreshCharts([...idSet]);
+ this.appliedFilters = activeFilters;
}
if (hasUnsavedChanges) {
@@ -157,35 +165,9 @@ class Dashboard extends React.PureComponent {
return Object.values(this.props.charts);
}
- refreshExcept(filterKey) {
- const { filters } = this.props;
- const currentFilteredNames =
- filterKey && filters[filterKey] ? Object.keys(filters[filterKey]) : [];
- const filterImmuneSlices = this.props.dashboardInfo.metadata
- .filterImmuneSlices;
- const filterImmuneSliceFields = this.props.dashboardInfo.metadata
- .filterImmuneSliceFields;
-
- this.getAllCharts().forEach(chart => {
- // filterKey is a string, filter_immune_slices array contains numbers
- if (
- String(chart.id) === filterKey ||
- filterImmuneSlices.includes(chart.id)
- ) {
- return;
- }
-
- const filterImmuneSliceFieldsNames =
- filterImmuneSliceFields[chart.id] || [];
- // has filter-able field names
- if (
- currentFilteredNames.length === 0 ||
- currentFilteredNames.some(
- name => !filterImmuneSliceFieldsNames.includes(name),
- )
- ) {
- this.props.actions.triggerQuery(true, chart.id);
- }
+ refreshCharts(ids) {
+ ids.forEach(id => {
+ this.props.actions.triggerQuery(true, id);
});
}
diff --git
a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
index f287108..795c645 100644
--- a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
+++ b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
@@ -23,6 +23,7 @@ import { isEmpty } from 'lodash';
import FilterIndicator from './FilterIndicator';
import FilterIndicatorGroup from './FilterIndicatorGroup';
import { FILTER_INDICATORS_DISPLAY_LENGTH } from '../util/constants';
+import { getChartIdsInFilterScope } from '../util/activeDashboardFilters';
import { getDashboardFilterKey } from '../util/getDashboardFilterKey';
import { getFilterColorMap } from '../util/dashboardFiltersColorMap';
@@ -33,8 +34,6 @@ const propTypes = {
chartStatus: PropTypes.string,
// from redux
- filterImmuneSlices: PropTypes.arrayOf(PropTypes.number).isRequired,
- filterImmuneSliceFields: PropTypes.object.isRequired,
setDirectPathToChild: PropTypes.func.isRequired,
filterFieldOnFocus: PropTypes.object.isRequired,
};
@@ -59,8 +58,6 @@ export default class FilterIndicatorsContainer extends
React.PureComponent {
const {
dashboardFilters,
chartId: currentChartId,
- filterImmuneSlices,
- filterImmuneSliceFields,
filterFieldOnFocus,
} = this.props;
@@ -76,60 +73,50 @@ export default class FilterIndicatorsContainer extends
React.PureComponent {
chartId,
componentId,
directPathToFilter,
- scope,
isDateFilter,
isInstantFilter,
columns,
labels,
+ scopes,
} = dashboardFilter;
- // do not apply filter on filter_box itself
- // do not apply filter on filterImmuneSlices list
- if (
- currentChartId !== chartId &&
- !filterImmuneSlices.includes(currentChartId)
- ) {
- Object.keys(columns).forEach(name => {
- const colorMapKey = getDashboardFilterKey({
- chartId,
- column: name,
+ if (currentChartId !== chartId) {
+ Object.keys(columns)
+ .filter(name =>
+ getChartIdsInFilterScope({ filterScope: scopes[name] }).includes(
+ currentChartId,
+ ),
+ )
+ .forEach(name => {
+ const colorMapKey = getDashboardFilterKey({
+ chartId,
+ column: name,
+ });
+ const indicator = {
+ chartId,
+ colorCode: dashboardFiltersColorMap[colorMapKey],
+ componentId,
+ directPathToFilter: directPathToFilter.concat(`LABEL-${name}`),
+ isDateFilter,
+ isInstantFilter,
+ name,
+ label: labels[name] || name,
+ values:
+ isEmpty(columns[name]) ||
+ (isDateFilter && columns[name] === 'No filter')
+ ? []
+ : [].concat(columns[name]),
+ isFilterFieldActive:
+ chartId === filterFieldOnFocus.chartId &&
+ name === filterFieldOnFocus.column,
+ };
+
+ if (isEmpty(indicator.values)) {
+ indicators[1].push(indicator);
+ } else {
+ indicators[0].push(indicator);
+ }
});
- const directPathToLabel = directPathToFilter.slice();
- directPathToLabel.push(`LABEL-${name}`);
- const indicator = {
- chartId,
- colorCode: dashboardFiltersColorMap[colorMapKey],
- componentId,
- directPathToFilter: directPathToLabel,
- scope,
- isDateFilter,
- isInstantFilter,
- name,
- label: labels[name] || name,
- values:
- isEmpty(columns[name]) ||
- (isDateFilter && columns[name] === 'No filter')
- ? []
- : [].concat(columns[name]),
- isFilterFieldActive:
- chartId === filterFieldOnFocus.chartId &&
- name === filterFieldOnFocus.column,
- };
-
- // do not apply filter on fields in the filterImmuneSliceFields map
- if (
- filterImmuneSliceFields[currentChartId] &&
- filterImmuneSliceFields[currentChartId].includes(name)
- ) {
- return;
- }
-
- if (isEmpty(indicator.values)) {
- indicators[1].push(indicator);
- } else {
- indicators[0].push(indicator);
- }
- });
}
return indicators;
diff --git a/superset/assets/src/dashboard/components/Header.jsx
b/superset/assets/src/dashboard/components/Header.jsx
index 569dac9..843eeb5 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -53,7 +53,6 @@ const propTypes = {
dashboardTitle: PropTypes.string.isRequired,
charts: PropTypes.objectOf(chartPropShape).isRequired,
layout: PropTypes.object.isRequired,
- filters: PropTypes.object.isRequired,
expandedSlices: PropTypes.object.isRequired,
css: PropTypes.string.isRequired,
colorNamespace: PropTypes.string,
@@ -217,7 +216,6 @@ class Header extends React.PureComponent {
css,
colorNamespace,
colorScheme,
- filters,
dashboardInfo,
refreshFrequency,
} = this.props;
@@ -235,7 +233,6 @@ class Header extends React.PureComponent {
color_scheme: colorScheme,
label_colors: labelColors,
dashboard_title: dashboardTitle,
- default_filters: safeStringify(filters),
refresh_frequency: refreshFrequency,
};
@@ -263,7 +260,6 @@ class Header extends React.PureComponent {
const {
dashboardTitle,
layout,
- filters,
expandedSlices,
css,
colorNamespace,
@@ -415,7 +411,6 @@ class Header extends React.PureComponent {
dashboardId={dashboardInfo.id}
dashboardTitle={dashboardTitle}
layout={layout}
- filters={filters}
expandedSlices={expandedSlices}
css={css}
colorNamespace={colorNamespace}
diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
index 6fc0182..cf89e5e 100644
--- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -29,6 +29,7 @@ import injectCustomCss from '../util/injectCustomCss';
import { SAVE_TYPE_NEWDASHBOARD } from '../util/constants';
import URLShortLinkModal from '../../components/URLShortLinkModal';
import getDashboardUrl from '../util/getDashboardUrl';
+import { getActiveFilters } from '../util/activeDashboardFilters';
const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
@@ -50,7 +51,6 @@ const propTypes = {
userCanSave: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
layout: PropTypes.object.isRequired,
- filters: PropTypes.object.isRequired,
expandedSlices: PropTypes.object.isRequired,
onSave: PropTypes.func.isRequired,
};
@@ -120,7 +120,6 @@ class HeaderActionsDropdown extends React.PureComponent {
colorScheme,
hasUnsavedChanges,
layout,
- filters,
expandedSlices,
onSave,
userCanEdit,
@@ -148,7 +147,6 @@ class HeaderActionsDropdown extends React.PureComponent {
dashboardTitle={dashboardTitle}
saveType={SAVE_TYPE_NEWDASHBOARD}
layout={layout}
- filters={filters}
expandedSlices={expandedSlices}
refreshFrequency={refreshFrequency}
css={css}
@@ -200,7 +198,7 @@ class HeaderActionsDropdown extends React.PureComponent {
<URLShortLinkModal
url={getDashboardUrl(
window.location.pathname,
- this.props.filters,
+ getActiveFilters(),
window.location.hash,
)}
emailSubject={emailSubject}
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx
b/superset/assets/src/dashboard/components/SaveModal.jsx
index d926bc8..2f53c8a 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -26,7 +26,6 @@ import { t } from '@superset-ui/translation';
import ModalTrigger from '../../components/ModalTrigger';
import Checkbox from '../../components/Checkbox';
import { SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD } from
'../util/constants';
-import { safeStringify } from '../../utils/safeStringify';
const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
@@ -37,7 +36,6 @@ const propTypes = {
layout: PropTypes.object.isRequired,
saveType: PropTypes.oneOf([SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD]),
triggerNode: PropTypes.node.isRequired,
- filters: PropTypes.object.isRequired,
css: PropTypes.string.isRequired,
colorNamespace: PropTypes.string,
colorScheme: PropTypes.string,
@@ -101,7 +99,6 @@ class SaveModal extends React.PureComponent {
colorNamespace,
colorScheme,
expandedSlices,
- filters,
dashboardId,
refreshFrequency,
} = this.props;
@@ -120,7 +117,6 @@ class SaveModal extends React.PureComponent {
expanded_slices: expandedSlices,
dashboard_title:
saveType === SAVE_TYPE_NEWDASHBOARD ? newDashName : dashboardTitle,
- default_filters: safeStringify(filters),
duplicate_slices: this.state.duplicateSlices,
refresh_frequency: refreshFrequency,
};
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx
b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 100a070..3d36a51 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -94,7 +94,6 @@ class SliceHeader extends React.PureComponent {
annotationQuery,
annotationError,
componentId,
- filters,
addDangerToast,
} = this.props;
@@ -146,7 +145,6 @@ class SliceHeader extends React.PureComponent {
supersetCanCSV={supersetCanCSV}
sliceCanEdit={sliceCanEdit}
componentId={componentId}
- filters={filters}
addDangerToast={addDangerToast}
/>
)}
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index 65565f0..779609d 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -23,11 +23,11 @@ import { Dropdown, MenuItem } from 'react-bootstrap';
import { t } from '@superset-ui/translation';
import URLShortLinkModal from '../../components/URLShortLinkModal';
import getDashboardUrl from '../util/getDashboardUrl';
+import { getActiveFilters } from '../util/activeDashboardFilters';
const propTypes = {
slice: PropTypes.object.isRequired,
componentId: PropTypes.string.isRequired,
- filters: PropTypes.object.isRequired,
addDangerToast: PropTypes.func.isRequired,
isCached: PropTypes.bool,
isExpanded: PropTypes.bool,
@@ -107,7 +107,6 @@ class SliceHeaderControls extends React.PureComponent {
isCached,
cachedDttm,
updatedDttm,
- filters,
componentId,
addDangerToast,
} = this.props;
@@ -162,7 +161,7 @@ class SliceHeaderControls extends React.PureComponent {
<URLShortLinkModal
url={getDashboardUrl(
window.location.pathname,
- filters,
+ getActiveFilters(),
componentId,
)}
addDangerToast={addDangerToast}
diff --git
a/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx
b/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx
index 7b778ed..6ac3a1d 100644
--- a/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx
+++ b/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx
@@ -19,14 +19,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import CheckboxTree from 'react-checkbox-tree';
-import { t } from '@superset-ui/translation';
-import 'react-checkbox-tree/lib/react-checkbox-tree.css';
-import {
- CheckboxChecked,
- CheckboxUnchecked,
- CheckboxHalfChecked,
-} from '../../../components/CheckboxIcons';
+import treeIcons from './treeIcons';
import renderFilterFieldTreeNodes from './renderFilterFieldTreeNodes';
import { filterScopeSelectorTreeNodePropShape } from '../../util/propShapes';
@@ -48,23 +42,6 @@ const defaultProps = {
activeKey: null,
};
-const FILTER_FIELD_CHECKBOX_TREE_ICONS = {
- check: <CheckboxChecked />,
- uncheck: <CheckboxUnchecked />,
- halfCheck: <CheckboxHalfChecked />,
- expandClose: <span className="rct-icon rct-icon-expand-close" />,
- expandOpen: <span className="rct-icon rct-icon-expand-open" />,
- expandAll: (
- <span className="rct-icon rct-icon-expand-all">{t('Expand all')}</span>
- ),
- collapseAll: (
- <span className="rct-icon rct-icon-collapse-all">{t('Collapse all')}</span>
- ),
- parentClose: <span className="rct-icon rct-icon-parent-close" />,
- parentOpen: <span className="rct-icon rct-icon-parent-open" />,
- leaf: <span className="rct-icon rct-icon-leaf" />,
-};
-
export default function FilterFieldTree({
activeKey,
nodes = [],
@@ -85,7 +62,7 @@ export default function FilterFieldTree({
onClick={onClick}
onCheck={onCheck}
onExpand={onExpand}
- icons={FILTER_FIELD_CHECKBOX_TREE_ICONS}
+ icons={treeIcons}
/>
);
}
diff --git
a/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx
b/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx
index b2a8be8..fc4b766 100644
--- a/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx
+++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx
@@ -35,7 +35,7 @@ export default class FilterScopeModal extends
React.PureComponent {
}
handleCloseModal() {
- if (this.modal) {
+ if (this.modal.current) {
this.modal.current.close();
}
}
diff --git
a/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx
b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx
index ae42a57..9435d7b 100644
---
a/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx
+++
b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx
@@ -26,24 +26,26 @@ import buildFilterScopeTreeEntry from
'../../util/buildFilterScopeTreeEntry';
import getFilterScopeNodesTree from '../../util/getFilterScopeNodesTree';
import getFilterFieldNodesTree from '../../util/getFilterFieldNodesTree';
import getFilterScopeParentNodes from '../../util/getFilterScopeParentNodes';
-import getCurrentScopeChartIds from '../../util/getCurrentScopeChartIds';
import getKeyForFilterScopeTree from '../../util/getKeyForFilterScopeTree';
import getSelectedChartIdForFilterScopeTree from
'../../util/getSelectedChartIdForFilterScopeTree';
+import getFilterScopeFromNodesTree from
'../../util/getFilterScopeFromNodesTree';
import getRevertedFilterScope from '../../util/getRevertedFilterScope';
import FilterScopeTree from './FilterScopeTree';
import FilterFieldTree from './FilterFieldTree';
+import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters';
import {
getChartIdAndColumnFromFilterKey,
getDashboardFilterKey,
} from '../../util/getDashboardFilterKey';
import { ALL_FILTERS_ROOT } from '../../util/constants';
+import { dashboardFilterPropShape } from '../../util/propShapes';
const propTypes = {
- dashboardFilters: PropTypes.object.isRequired,
+ dashboardFilters: PropTypes.objectOf(dashboardFilterPropShape).isRequired,
layout: PropTypes.object.isRequired,
- filterImmuneSlices: PropTypes.arrayOf(PropTypes.number).isRequired,
- filterImmuneSliceFields: PropTypes.object.isRequired,
- setDirectPathToChild: PropTypes.func.isRequired,
+
+ updateDashboardFiltersScope: PropTypes.func.isRequired,
+ setUnsavedChanges: PropTypes.func.isRequired,
onCloseModal: PropTypes.func.isRequired,
};
@@ -51,12 +53,7 @@ export default class FilterScopeSelector extends
React.PureComponent {
constructor(props) {
super(props);
- const {
- dashboardFilters,
- filterImmuneSlices,
- filterImmuneSliceFields,
- layout,
- } = props;
+ const { dashboardFilters, layout } = props;
if (Object.keys(dashboardFilters).length > 0) {
// display filter fields in tree structure
@@ -88,14 +85,12 @@ export default class FilterScopeSelector extends
React.PureComponent {
filterFields: [filterKey],
selectedChartId: filterId,
});
- const checked = getCurrentScopeChartIds({
- scopeComponentIds: ['ROOT_ID'], //
dashboardFilters[chartId].scopes[columnName],
- filterField: columnName,
- filterImmuneSlices,
- filterImmuneSliceFields,
- components: layout,
- });
const expanded = getFilterScopeParentNodes(nodes, 1);
+ // display filter_box chart as checked, but do not show checkbox
+ const chartIdsInFilterScope = getChartIdsInFilterScope({
+ filterScope: dashboardFilters[filterId].scopes[columnName],
+ });
+
return {
...mapByChartId,
[filterKey]: {
@@ -103,7 +98,7 @@ export default class FilterScopeSelector extends
React.PureComponent {
nodes,
// filtered nodes in display if searchText is not empty
nodesFiltered: [...nodes],
- checked,
+ checked: chartIdsInFilterScope,
expanded,
},
};
@@ -304,18 +299,28 @@ export default class FilterScopeSelector extends
React.PureComponent {
onSave() {
const { filterScopeMap } = this.state;
- console.log(
- 'i am current state',
- this.allfilterFields.reduce(
- (map, key) => ({
+ const allFilterFieldScopes = this.allfilterFields.reduce(
+ (map, filterKey) => {
+ const nodes = filterScopeMap[filterKey].nodes;
+ const checkedChartIds = filterScopeMap[filterKey].checked;
+
+ return {
...map,
- [key]: filterScopeMap[key].checked,
- }),
- {},
- ),
+ [filterKey]: getFilterScopeFromNodesTree({
+ filterKey,
+ nodes,
+ checkedChartIds,
+ }),
+ };
+ },
+ {},
);
- // save does not close modal
+ this.props.updateDashboardFiltersScope(allFilterFieldScopes);
+ this.props.setUnsavedChanges(true);
+
+ // click Save button will do save and close modal
+ this.props.onCloseModal();
}
filterTree() {
@@ -486,7 +491,7 @@ export default class FilterScopeSelector extends
React.PureComponent {
<div className="filter-scope-container">
<div className="filter-scope-header">
<h4>{t('Configure filter scopes')}</h4>
- {this.renderEditingFiltersName()}
+ {showSelector && this.renderEditingFiltersName()}
</div>
{!showSelector ? (
@@ -503,7 +508,7 @@ export default class FilterScopeSelector extends
React.PureComponent {
)}
</div>
<div className="dashboard-modal-actions-container">
- <Button onClick={this.onClose}>{t('Cancel')}</Button>
+ <Button onClick={this.onClose}>{t('Close')}</Button>
{showSelector && (
<Button bsStyle="primary" onClick={this.onSave}>
{t('Save')}
diff --git
a/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
b/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
index e433f15..e280335 100644
--- a/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
+++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
@@ -19,15 +19,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import CheckboxTree from 'react-checkbox-tree';
-import 'react-checkbox-tree/lib/react-checkbox-tree.css';
-import { t } from '@superset-ui/translation';
-import {
- CheckboxChecked,
- CheckboxUnchecked,
- CheckboxHalfChecked,
-} from '../../../components/CheckboxIcons';
import renderFilterScopeTreeNodes from './renderFilterScopeTreeNodes';
+import treeIcons from './treeIcons';
import { filterScopeSelectorTreeNodePropShape } from '../../util/propShapes';
const propTypes = {
@@ -49,23 +43,6 @@ const defaultProps = {
const NOOP = () => {};
-const FILTER_SCOPE_CHECKBOX_TREE_ICONS = {
- check: <CheckboxChecked />,
- uncheck: <CheckboxUnchecked />,
- halfCheck: <CheckboxHalfChecked />,
- expandClose: <span className="rct-icon rct-icon-expand-close" />,
- expandOpen: <span className="rct-icon rct-icon-expand-open" />,
- expandAll: (
- <span className="rct-icon rct-icon-expand-all">{t('Expand all')}</span>
- ),
- collapseAll: (
- <span className="rct-icon rct-icon-collapse-all">{t('Collapse all')}</span>
- ),
- parentClose: <span className="rct-icon rct-icon-parent-close" />,
- parentOpen: <span className="rct-icon rct-icon-parent-open" />,
- leaf: <span className="rct-icon rct-icon-leaf" />,
-};
-
export default function FilterScopeTree({
nodes = [],
checked = [],
@@ -85,7 +62,7 @@ export default function FilterScopeTree({
onCheck={onCheck}
onExpand={onExpand}
onClick={NOOP}
- icons={FILTER_SCOPE_CHECKBOX_TREE_ICONS}
+ icons={treeIcons}
/>
);
}
diff --git
a/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
b/superset/assets/src/dashboard/components/filterscope/treeIcons.jsx
similarity index 54%
copy from
superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
copy to superset/assets/src/dashboard/components/filterscope/treeIcons.jsx
index e433f15..a11fec6 100644
--- a/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
+++ b/superset/assets/src/dashboard/components/filterscope/treeIcons.jsx
@@ -17,39 +17,16 @@
* under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import CheckboxTree from 'react-checkbox-tree';
-import 'react-checkbox-tree/lib/react-checkbox-tree.css';
import { t } from '@superset-ui/translation';
+import 'react-checkbox-tree/lib/react-checkbox-tree.css';
import {
CheckboxChecked,
CheckboxUnchecked,
CheckboxHalfChecked,
} from '../../../components/CheckboxIcons';
-import renderFilterScopeTreeNodes from './renderFilterScopeTreeNodes';
-import { filterScopeSelectorTreeNodePropShape } from '../../util/propShapes';
-
-const propTypes = {
- nodes: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired,
- checked: PropTypes.arrayOf(
- PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- ).isRequired,
- expanded: PropTypes.arrayOf(
- PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- ).isRequired,
- onCheck: PropTypes.func.isRequired,
- onExpand: PropTypes.func.isRequired,
- selectedChartId: PropTypes.oneOfType([null, PropTypes.number]),
-};
-const defaultProps = {
- selectedChartId: null,
-};
-
-const NOOP = () => {};
-
-const FILTER_SCOPE_CHECKBOX_TREE_ICONS = {
+const treeIcons = {
check: <CheckboxChecked />,
uncheck: <CheckboxUnchecked />,
halfCheck: <CheckboxHalfChecked />,
@@ -66,29 +43,4 @@ const FILTER_SCOPE_CHECKBOX_TREE_ICONS = {
leaf: <span className="rct-icon rct-icon-leaf" />,
};
-export default function FilterScopeTree({
- nodes = [],
- checked = [],
- expanded = [],
- onCheck,
- onExpand,
- selectedChartId,
-}) {
- return (
- <CheckboxTree
- showExpandAll
- expandOnClick
- showNodeIcon={false}
- nodes={renderFilterScopeTreeNodes({ nodes, selectedChartId })}
- checked={checked}
- expanded={expanded}
- onCheck={onCheck}
- onExpand={onExpand}
- onClick={NOOP}
- icons={FILTER_SCOPE_CHECKBOX_TREE_ICONS}
- />
- );
-}
-
-FilterScopeTree.propTypes = propTypes;
-FilterScopeTree.defaultProps = defaultProps;
+export default treeIcons;
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 4d34cc6..d50c24c 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -30,6 +30,9 @@ import {
LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
LOG_ACTIONS_FORCE_REFRESH_CHART,
} from '../../../logger/LogUtils';
+import { safeStringify } from '../../../utils/safeStringify';
+import { isFilterBox } from '../../util/activeDashboardFilters';
+import getFilterValuesByFilterId from '../../util/getFilterValuesByFilterId';
const propTypes = {
id: PropTypes.number.isRequired,
@@ -46,6 +49,7 @@ const propTypes = {
slice: slicePropShape.isRequired,
sliceName: PropTypes.string.isRequired,
timeout: PropTypes.number.isRequired,
+ // all active filter fields in dashboard
filters: PropTypes.object.isRequired,
refreshChart: PropTypes.func.isRequired,
logEvent: PropTypes.func.isRequired,
@@ -115,7 +119,9 @@ class Chart extends React.Component {
for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) {
const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i];
- if (nextProps[prop] !== this.props[prop]) {
+ if (
+ safeStringify(nextProps[prop]) !== safeStringify(this.props[prop])
+ ) {
return true;
}
}
@@ -238,6 +244,12 @@ class Chart extends React.Component {
const isCached = queryResponse && queryResponse.is_cached;
const cachedDttm = queryResponse && queryResponse.cached_dttm;
const isOverflowable = OVERFLOWABLE_VIZ_TYPES.has(slice.viz_type);
+ const initialValues = isFilterBox(id)
+ ? getFilterValuesByFilterId({
+ activeFilters: filters,
+ filterId: id,
+ })
+ : {};
return (
<div>
@@ -297,7 +309,7 @@ class Chart extends React.Component {
chartId={id}
chartStatus={chart.chartStatus}
datasource={datasource}
- initialValues={filters[id]}
+ initialValues={initialValues}
formData={formData}
queryResponse={chart.queryResponse}
timeout={timeout}
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx
b/superset/assets/src/dashboard/containers/Chart.jsx
index 5f0b107..361fe47 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -29,7 +29,10 @@ import { changeFilter } from '../actions/dashboardFilters';
import { addDangerToast } from '../../messageToasts/actions';
import { refreshChart } from '../../chart/chartAction';
import { logEvent } from '../../logger/actions';
-import { getActiveFilters } from '../util/activeDashboardFilters';
+import {
+ getActiveFilters,
+ getAppliedFilterValues,
+} from '../util/activeDashboardFilters';
import getFormDataWithExtraFilters from
'../util/charts/getFormDataWithExtraFilters';
import Chart from '../components/gridComponents/Chart';
@@ -48,7 +51,6 @@ function mapStateToProps(
const { id } = ownProps;
const chart = chartQueries[id] || {};
const { colorScheme, colorNamespace } = dashboardState;
- const filters = getActiveFilters();
return {
chart,
@@ -57,12 +59,11 @@ function mapStateToProps(
{},
slice: sliceEntities.slices[id],
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
- filters: filters || EMPTY_FILTERS,
+ filters: getActiveFilters() || EMPTY_FILTERS,
// note: this method caches filters if possible to prevent render cascades
formData: getFormDataWithExtraFilters({
chart,
- dashboardMetadata: dashboardInfo.metadata,
- filters,
+ filters: getAppliedFilterValues(id),
colorScheme,
colorNamespace,
sliceId: id,
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx
b/superset/assets/src/dashboard/containers/Dashboard.jsx
index c602cd3..7ed9bd8 100644
--- a/superset/assets/src/dashboard/containers/Dashboard.jsx
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -27,7 +27,6 @@ import {
} from '../actions/dashboardState';
import { triggerQuery } from '../../chart/chartAction';
import { logEvent } from '../../logger/actions';
-import getLoadStatsPerTopLevelComponent from
'../util/logging/getLoadStatsPerTopLevelComponent';
import { getActiveFilters } from '../util/activeDashboardFilters';
function mapStateToProps(state) {
@@ -49,18 +48,15 @@ function mapStateToProps(state) {
dashboardState,
charts,
datasources,
- // filters prop: All the filter_box's state in this dashboard
+ // filters prop: a map structure for all the active filter_box's values
and scope in this dashboard,
+ // for each filter field. map key is [chartId_column]
// When dashboard is first loaded into browser,
// its value is from preselect_filters that dashboard owner saved in
dashboard's meta data
// When user start interacting with dashboard, it will be user picked
values from all filter_box
- filters: getActiveFilters(),
+ activeFilters: getActiveFilters(),
slices: sliceEntities.slices,
layout: dashboardLayout.present,
impressionId,
- loadStats: getLoadStatsPerTopLevelComponent({
- layout: dashboardLayout.present,
- chartQueries: charts,
- }),
};
}
diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
index 9d11998..88e54ac 100644
--- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -52,7 +52,6 @@ import {
import { logEvent } from '../../logger/actions';
import { DASHBOARD_HEADER_ID } from '../util/constants';
-import { getActiveFilters } from '../util/activeDashboardFilters';
function mapStateToProps({
dashboardLayout: undoableLayout,
@@ -65,7 +64,6 @@ function mapStateToProps({
undoLength: undoableLayout.past.length,
redoLength: undoableLayout.future.length,
layout: undoableLayout.present,
- filters: getActiveFilters(),
dashboardTitle: (
(undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
).text,
diff --git a/superset/assets/src/dashboard/containers/FilterIndicators.jsx
b/superset/assets/src/dashboard/containers/FilterIndicators.jsx
index 9ac8d62..80e3f77 100644
--- a/superset/assets/src/dashboard/containers/FilterIndicators.jsx
+++ b/superset/assets/src/dashboard/containers/FilterIndicators.jsx
@@ -23,7 +23,7 @@ import FilterIndicatorsContainer from
'../components/FilterIndicatorsContainer';
import { setDirectPathToChild } from '../actions/dashboardState';
function mapStateToProps(
- { dashboardState, dashboardFilters, dashboardInfo, charts },
+ { dashboardState, dashboardFilters, dashboardLayout, charts },
ownProps,
) {
const chartId = ownProps.chartId;
@@ -33,9 +33,7 @@ function mapStateToProps(
dashboardFilters,
chartId,
chartStatus,
- filterImmuneSlices: dashboardInfo.metadata.filterImmuneSlices || [],
- filterImmuneSliceFields:
- dashboardInfo.metadata.filterImmuneSliceFields || {},
+ layout: dashboardLayout.present,
filterFieldOnFocus:
dashboardState.focusedFilterField.length === 0
? {}
diff --git a/superset/assets/src/dashboard/containers/FilterScope.jsx
b/superset/assets/src/dashboard/containers/FilterScope.jsx
index f88d665..d816df9 100644
--- a/superset/assets/src/dashboard/containers/FilterScope.jsx
+++ b/superset/assets/src/dashboard/containers/FilterScope.jsx
@@ -19,15 +19,13 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import { setDirectPathToChild } from '../actions/dashboardState';
+import { updateDashboardFiltersScope } from '../actions/dashboardFilters';
+import { setUnsavedChanges } from '../actions/dashboardState';
import FilterScopeSelector from
'../components/filterscope/FilterScopeSelector';
-function mapStateToProps({ dashboardLayout, dashboardFilters, dashboardInfo })
{
+function mapStateToProps({ dashboardLayout, dashboardFilters }) {
return {
dashboardFilters,
- filterImmuneSlices: dashboardInfo.metadata.filterImmuneSlices || [],
- filterImmuneSliceFields:
- dashboardInfo.metadata.filterImmuneSliceFields || {},
layout: dashboardLayout.present,
};
}
@@ -35,7 +33,8 @@ function mapStateToProps({ dashboardLayout, dashboardFilters,
dashboardInfo }) {
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
- setDirectPathToChild,
+ updateDashboardFiltersScope,
+ setUnsavedChanges,
},
dispatch,
);
diff --git a/superset/assets/src/dashboard/reducers/dashboardFilters.js
b/superset/assets/src/dashboard/reducers/dashboardFilters.js
index 4c1f14d..7886ddc 100644
--- a/superset/assets/src/dashboard/reducers/dashboardFilters.js
+++ b/superset/assets/src/dashboard/reducers/dashboardFilters.js
@@ -22,12 +22,15 @@ import {
REMOVE_FILTER,
CHANGE_FILTER,
UPDATE_DIRECT_PATH_TO_FILTER,
+ UPDATE_LAYOUT_COMPONENTS,
+ UPDATE_DASHBOARD_FILTERS_SCOPE,
} from '../actions/dashboardFilters';
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
+import { DASHBOARD_ROOT_ID } from '../util/constants';
import getFilterConfigsFromFormdata from
'../util/getFilterConfigsFromFormdata';
import { buildFilterColorMap } from '../util/dashboardFiltersColorMap';
import { buildActiveFilters } from '../util/activeDashboardFilters';
-import { DASHBOARD_ROOT_ID } from '../util/constants';
+import { getChartIdAndColumnFromFilterKey } from
'../util/getDashboardFilterKey';
export const DASHBOARD_FILTER_SCOPE_GLOBAL = {
scope: [DASHBOARD_ROOT_ID],
@@ -35,7 +38,7 @@ export const DASHBOARD_FILTER_SCOPE_GLOBAL = {
};
export const dashboardFilter = {
- chartId: 0,
+ chartId: null,
componentId: null,
filterName: null,
directPathToFilter: [],
@@ -46,6 +49,8 @@ export const dashboardFilter = {
scopes: {},
};
+const CHANGE_FILTER_VALUE_ACTIONS = [ADD_FILTER, REMOVE_FILTER, CHANGE_FILTER];
+
export default function dashboardFiltersReducer(dashboardFilters = {}, action)
{
const actionHandlers = {
[ADD_FILTER]() {
@@ -113,10 +118,43 @@ export default function
dashboardFiltersReducer(dashboardFilters = {}, action) {
},
};
- if (action.type === REMOVE_FILTER) {
+ if (action.type === UPDATE_LAYOUT_COMPONENTS) {
+ buildActiveFilters({
+ dashboardFilters,
+ components: action.components,
+ });
+ return dashboardFilters;
+ } else if (action.type === UPDATE_DASHBOARD_FILTERS_SCOPE) {
+ const allDashboardFiltersScope = action.scopes;
+ // update filter scope for each filter field
+ const updatedFilters = Object.entries(allDashboardFiltersScope).reduce(
+ (map, entry) => {
+ const [filterKey, { scope, immune }] = entry;
+ const { chartId, column } =
getChartIdAndColumnFromFilterKey(filterKey);
+ const scopes = {
+ ...map[chartId].scopes,
+ [column]: {
+ scope,
+ immune,
+ },
+ };
+ return {
+ ...map,
+ [chartId]: {
+ ...map[chartId],
+ scopes,
+ },
+ };
+ },
+ dashboardFilters,
+ );
+
+ buildActiveFilters({ dashboardFilters: updatedFilters });
+ return updatedFilters;
+ } else if (action.type === REMOVE_FILTER) {
const { chartId } = action;
const { [chartId]: deletedFilter, ...updatedFilters } = dashboardFilters;
- buildActiveFilters(updatedFilters);
+ buildActiveFilters({ dashboardFilters: updatedFilters });
buildFilterColorMap(updatedFilters);
return updatedFilters;
@@ -127,8 +165,11 @@ export default function
dashboardFiltersReducer(dashboardFilters = {}, action) {
dashboardFilters[action.chartId],
),
};
- buildActiveFilters(updatedFilters);
- buildFilterColorMap(updatedFilters);
+
+ if (CHANGE_FILTER_VALUE_ACTIONS.includes(action.type)) {
+ buildActiveFilters({ dashboardFilters: updatedFilters });
+ buildFilterColorMap(updatedFilters);
+ }
return updatedFilters;
}
diff --git a/superset/assets/src/dashboard/reducers/dashboardLayout.js
b/superset/assets/src/dashboard/reducers/dashboardLayout.js
index 23459e0..d473cb6 100644
--- a/superset/assets/src/dashboard/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -155,6 +155,7 @@ const actionHandlers = {
const destinationChildren = destinationEntity.children;
const newRow = newComponentFactory(ROW_TYPE);
newRow.children = [destinationChildren[destination.index]];
+ newRow.parents = (destinationEntity.parents ||
[]).concat(destination.id);
destinationChildren[destination.index] = newRow.id;
nextEntities[newRow.id] = newRow;
}
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js
b/superset/assets/src/dashboard/reducers/getInitialState.js
index 185a8b0..91f7483 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -22,7 +22,10 @@ import shortid from 'shortid';
import { CategoricalColorNamespace } from '@superset-ui/color';
import { chart } from '../../chart/chartReducer';
-import { dashboardFilter } from './dashboardFilters';
+import {
+ DASHBOARD_FILTER_SCOPE_GLOBAL,
+ dashboardFilter,
+} from './dashboardFilters';
import { initSliceEntities } from './sliceEntities';
import { getParam } from '../../modules/utils';
import { applyDefaultFormData } from '../../explore/store';
@@ -98,6 +101,11 @@ export default function(bootstrapData) {
let newSlicesContainer;
let newSlicesContainerWidth = 0;
+ const filterImmuneSliceFields =
+ dashboard.metadata.filter_immune_slice_fields || {};
+ const filterImmuneSlices = dashboard.metadata.filter_immune_slices || [];
+ const filterScopes = dashboard.metadata.filter_scopes || {};
+
const chartQueries = {};
const dashboardFilters = {};
const slices = {};
@@ -173,6 +181,34 @@ export default function(bootstrapData) {
});
}
+ // backward compatible:
+ // merge scoped filter settings with old global immune settings
+ const scopesByChartId = Object.keys(columns).reduce((map, column) => {
+ const scopeSettings = {
+ ...filterScopes[key],
+ };
+ const { scope, immune } = {
+ ...DASHBOARD_FILTER_SCOPE_GLOBAL,
+ ...scopeSettings[column],
+ };
+ const immuneChartIds = new Set(filterImmuneSlices);
+ Object.keys(filterImmuneSliceFields)
+ .filter(strChartId =>
+ filterImmuneSliceFields[strChartId].includes(column),
+ )
+ .forEach(strChartId => {
+ immuneChartIds.add(parseInt(strChartId, 10));
+ });
+
+ return {
+ ...map,
+ [column]: {
+ scope,
+ immune: [...immuneChartIds].concat(immune),
+ },
+ };
+ }, {});
+
const componentId = chartIdToLayoutId[key];
const directPathToFilter = (layout[componentId].parents || []).slice();
directPathToFilter.push(componentId);
@@ -184,6 +220,7 @@ export default function(bootstrapData) {
directPathToFilter,
columns,
labels,
+ scopes: scopesByChartId,
isInstantFilter: !!slice.form_data.instant_filtering,
isDateFilter: Object.keys(columns).includes(TIME_RANGE),
};
@@ -198,8 +235,11 @@ export default function(bootstrapData) {
layout[layoutId].meta.sliceName = slice.slice_name;
}
});
- buildActiveFilters(dashboardFilters);
- buildFilterColorMap(dashboardFilters);
+ buildActiveFilters({
+ dashboardFilters,
+ components: layout,
+ });
+ buildFilterColorMap(dashboardFilters, layout);
// store the header as a layout component so we can undo/redo changes
layout[DASHBOARD_HEADER_ID] = {
@@ -233,9 +273,6 @@ export default function(bootstrapData) {
id: dashboard.id,
slug: dashboard.slug,
metadata: {
- filterImmuneSliceFields:
- dashboard.metadata.filter_immune_slice_fields || {},
- filterImmuneSlices: dashboard.metadata.filter_immune_slices || [],
timed_refresh_immune_slices:
dashboard.metadata.timed_refresh_immune_slices,
},
diff --git
a/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less
b/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less
index dd19a66..6df2826 100644
--- a/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less
+++ b/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less
@@ -88,6 +88,7 @@
label {
font-weight: normal;
margin: 0 0 0 16px;
+ word-break: break-all;
}
}
diff --git a/superset/assets/src/dashboard/util/activeDashboardFilters.js
b/superset/assets/src/dashboard/util/activeDashboardFilters.js
index 8fa00d0..3674eb3 100644
--- a/superset/assets/src/dashboard/util/activeDashboardFilters.js
+++ b/superset/assets/src/dashboard/util/activeDashboardFilters.js
@@ -16,52 +16,119 @@
* specific language governing permissions and limitations
* under the License.
*/
-let activeFilters = {};
+import { isEmpty } from 'lodash';
+import { mapValues, flow, keyBy } from 'lodash/fp';
+
+import {
+ getChartIdAndColumnFromFilterKey,
+ getDashboardFilterKey,
+} from './getDashboardFilterKey';
+import { CHART_TYPE } from '../util/componentTypes';
+
let allFilterBoxChartIds = [];
+let activeFilters = {};
+let appliedFilterValuesByChart = {};
+let allComponents = {};
+// output: { [id_column]: { values, scope } }
export function getActiveFilters() {
return activeFilters;
}
-// currently filterbox is a chart,
-// when define filter scopes, they have to be out pulled out in a few places.
-// after we make filterbox a dashboard build-in component,
-// will not need this check anymore
+// currently filter_box is a chart,
+// when selecting filter scopes, they have to be out pulled out in a few
places.
+// after we make filter_box a dashboard build-in component, will not need this
check anymore.
export function isFilterBox(chartId) {
return allFilterBoxChartIds.includes(chartId);
}
-export function getAllFilterBoxChartIds() {
- return allFilterBoxChartIds;
+// this function is to find all filter values applied to a chart,
+// it goes through all active filters and their scopes.
+// return: { [column]: array of selected values }
+export function getAppliedFilterValues(chartId) {
+ // use cached data if possible
+ if (!(chartId in appliedFilterValuesByChart)) {
+ const applicableFilters = Object.entries(activeFilters).filter(
+ ([, { scope: chartIds }]) => chartIds.includes(chartId),
+ );
+ appliedFilterValuesByChart[chartId] = flow(
+ keyBy(
+ ([filterKey]) => getChartIdAndColumnFromFilterKey(filterKey).column,
+ ),
+ mapValues(([, { values }]) => values),
+ )(applicableFilters);
+ }
+ return appliedFilterValuesByChart[chartId];
+}
+
+export function getChartIdsInFilterScope({ filterScope }) {
+ function traverse(chartIds = [], component = {}, immuneChartIds = []) {
+ if (!component) {
+ return;
+ }
+
+ if (
+ component.type === CHART_TYPE &&
+ component.meta &&
+ component.meta.chartId &&
+ !immuneChartIds.includes(component.meta.chartId)
+ ) {
+ chartIds.push(component.meta.chartId);
+ } else if (component.children) {
+ component.children.forEach(child =>
+ traverse(chartIds, allComponents[child], immuneChartIds),
+ );
+ }
+ }
+
+ const chartIds = [];
+ const { scope: scopeComponentIds, immune: immuneChartIds } = filterScope;
+ scopeComponentIds.forEach(componentId =>
+ traverse(chartIds, allComponents[componentId], immuneChartIds),
+ );
+
+ return chartIds;
}
-// non-empty filters from dashboardFilters,
-// this function does not take into account: filter immune or filter scope
settings
-export function buildActiveFilters(allDashboardFilters = {}) {
- allFilterBoxChartIds = Object.values(allDashboardFilters).map(
+// non-empty filter fields in dashboardFilters,
+// activeFilters map contains selected values and filter scope.
+// values: array of selected values
+// scope: array of chartIds that applicable to the filter field.
+export function buildActiveFilters({ dashboardFilters = {}, components = {} })
{
+ allFilterBoxChartIds = Object.values(dashboardFilters).map(
filter => filter.chartId,
);
- activeFilters = Object.values(allDashboardFilters).reduce(
- (result, filter) => {
- const { chartId, columns } = filter;
+ // clear cache
+ if (!isEmpty(components)) {
+ allComponents = components;
+ }
+ appliedFilterValuesByChart = {};
+ activeFilters = Object.values(dashboardFilters).reduce((result, filter) => {
+ const { chartId, columns, scopes } = filter;
+ const nonEmptyFilters = {};
- Object.keys(columns).forEach(key => {
- if (
- Array.isArray(columns[key])
- ? columns[key].length
- : columns[key] !== undefined
- ) {
- /* eslint-disable no-param-reassign */
- result[chartId] = {
- ...result[chartId],
- [key]: columns[key],
- };
- }
- });
+ Object.keys(columns).forEach(column => {
+ if (
+ Array.isArray(columns[column])
+ ? columns[column].length
+ : columns[column] !== undefined
+ ) {
+ // remove filter itself
+ const scope = getChartIdsInFilterScope({
+ filterScope: scopes[column],
+ }).filter(id => chartId !== id);
- return result;
- },
- {},
- );
+ nonEmptyFilters[getDashboardFilterKey({ chartId, column })] = {
+ values: columns[column],
+ scope,
+ };
+ }
+ });
+
+ return {
+ ...result,
+ ...nonEmptyFilters,
+ };
+ }, {});
}
diff --git
a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
index fa6ad2a..b7a768b 100644
--- a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
+++ b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
@@ -16,45 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-export default function getEffectiveExtraFilters({
- dashboardMetadata,
- filters,
- sliceId,
-}) {
- const immuneSlices = dashboardMetadata.filterImmuneSlices || [];
-
- if (sliceId && immuneSlices.includes(sliceId)) {
- // The slice is immune to dashboard filters
- return [];
- }
-
- // Build a list of fields the slice is immune to filters on
- const effectiveFilters = [];
- let immuneToFields = [];
- if (
- sliceId &&
- dashboardMetadata.filterImmuneSliceFields &&
- dashboardMetadata.filterImmuneSliceFields[sliceId]
- ) {
- immuneToFields = dashboardMetadata.filterImmuneSliceFields[sliceId];
- }
-
- Object.keys(filters).forEach(filteringSliceId => {
- if (filteringSliceId === sliceId.toString()) {
- // Filters applied by the slice don't apply to itself
- return;
- }
- const filtersFromSlice = filters[filteringSliceId];
- Object.keys(filtersFromSlice).forEach(field => {
- if (!immuneToFields.includes(field)) {
- effectiveFilters.push({
- col: field,
- op: 'in',
- val: filtersFromSlice[field],
- });
- }
- });
- });
-
- return effectiveFilters;
+export default function getEffectiveExtraFilters(filters) {
+ return Object.entries(filters).map(([column, values]) => ({
+ col: column,
+ op: 'in',
+ val: values,
+ }));
}
diff --git
a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
index 5869a09..3566931 100644
--- a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
+++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
@@ -22,13 +22,14 @@ import getEffectiveExtraFilters from
'./getEffectiveExtraFilters';
// We cache formData objects so that our connected container components don't
always trigger
// render cascades. we cannot leverage the reselect library because our cache
size is >1
-const cachedDashboardMetadataByChart = {};
const cachedFiltersByChart = {};
const cachedFormdataByChart = {};
+// this function merge chart's formData with dashboard filters value,
+// and generate a new formData which will be used in the new query.
+// filters param only contains those applicable to this chart.
export default function getFormDataWithExtraFilters({
chart = {},
- dashboardMetadata,
filters,
colorScheme,
colorNamespace,
@@ -40,7 +41,6 @@ export default function getFormDataWithExtraFilters({
// if dashboard metadata + filters have not changed, use cache if possible
if (
- (cachedDashboardMetadataByChart[sliceId] || {}) === dashboardMetadata &&
(cachedFiltersByChart[sliceId] || {}) === filters &&
(colorScheme == null ||
cachedFormdataByChart[sliceId].color_scheme === colorScheme) &&
@@ -55,14 +55,9 @@ export default function getFormDataWithExtraFilters({
...chart.formData,
...(colorScheme && { color_scheme: colorScheme }),
label_colors: labelColors,
- extra_filters: getEffectiveExtraFilters({
- dashboardMetadata,
- filters,
- sliceId,
- }),
+ extra_filters: getEffectiveExtraFilters(filters),
};
- cachedDashboardMetadataByChart[sliceId] = dashboardMetadata;
cachedFiltersByChart[sliceId] = filters;
cachedFormdataByChart[sliceId] = formData;
diff --git a/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js
b/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js
deleted file mode 100644
index 60d86b5..0000000
--- a/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { CHART_TYPE } from '../util/componentTypes';
-
-export default function getCurrentScopeChartIds({
- scopeComponentIds,
- filterField,
- filterImmuneSlices,
- filterImmuneSliceFields,
- components,
-}) {
- let chartIds = [];
-
- function traverse(component) {
- if (!component) {
- return;
- }
-
- if (
- component.type === CHART_TYPE &&
- component.meta &&
- component.meta.chartId
- ) {
- chartIds.push(component.meta.chartId);
- } else if (component.children) {
- component.children.forEach(child => traverse(components[child]));
- }
- }
-
- scopeComponentIds.forEach(componentId => traverse(components[componentId]));
-
- if (filterImmuneSlices && filterImmuneSlices.length) {
- chartIds = chartIds.filter(id => !filterImmuneSlices.includes(id));
- }
-
- if (filterImmuneSliceFields) {
- chartIds = chartIds.filter(
- id =>
- !(id.toString() in filterImmuneSliceFields) ||
- !filterImmuneSliceFields[id].includes(filterField),
- );
- }
-
- return chartIds;
-}
diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js
b/superset/assets/src/dashboard/util/getDashboardUrl.js
index 243c8cb..bf2040c 100644
--- a/superset/assets/src/dashboard/util/getDashboardUrl.js
+++ b/superset/assets/src/dashboard/util/getDashboardUrl.js
@@ -16,10 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-/* eslint camelcase: 0 */
+import serializeActiveFilterValues from './serializeActiveFilterValues';
export default function getDashboardUrl(pathname, filters = {}, hash = '') {
- const preselect_filters = encodeURIComponent(JSON.stringify(filters));
+ // convert flattened { [id_column]: values } object
+ // to nested filter object
+ const obj = serializeActiveFilterValues(filters);
+ const preselectFilters = encodeURIComponent(JSON.stringify(obj));
const hashSection = hash ? `#${hash}` : '';
- return `${pathname}?preselect_filters=${preselect_filters}${hashSection}`;
+ return `${pathname}?preselect_filters=${preselectFilters}${hashSection}`;
}
diff --git a/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js
b/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js
index b55d28f..5d9b59e 100644
--- a/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js
+++ b/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js
@@ -39,7 +39,7 @@ export default function getFilterFieldNodesTree({
dashboardFilters = {} }) {
return [
{
value: ALL_FILTERS_ROOT,
- label: t('Select/deselect all filters'),
+ label: t('All filters'),
children: allFilters,
},
];
diff --git a/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js
b/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js
index 470ac08..60669bd 100644
--- a/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js
+++ b/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js
@@ -86,7 +86,7 @@ function traverse({
if (FILTER_SCOPE_CONTAINER_TYPES.includes(type)) {
let label = null;
if (type === DASHBOARD_ROOT_TYPE) {
- label = t('Select/deselect all charts');
+ label = t('All charts');
} else {
label =
currentNode.meta && currentNode.meta.text
diff --git a/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
b/superset/assets/src/dashboard/util/getFilterValuesByFilterId.js
similarity index 58%
copy from superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
copy to superset/assets/src/dashboard/util/getFilterValuesByFilterId.js
index e85dd51..75f9c70 100644
--- a/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
+++ b/superset/assets/src/dashboard/util/getFilterValuesByFilterId.js
@@ -16,13 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { safeStringify } from '../../utils/safeStringify';
+import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey';
-export default function getKeyForFilterScopeTree({
- activeFilterField,
- checkedFilterFields,
+// input: { [id_column1]: values, [id_column2]: values }
+// output: { column1: values, column2: values }
+export default function getFilterValuesByFilterId({
+ activeFilters = {},
+ filterId,
}) {
- return activeFilterField
- ? safeStringify([activeFilterField])
- : safeStringify(checkedFilterFields);
+ return Object.entries(activeFilters).reduce((map, entry) => {
+ const [filterKey, { values }] = entry;
+ const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey);
+ if (chartId === filterId) {
+ return {
+ ...map,
+ [column]: values,
+ };
+ }
+ return map;
+ }, {});
}
diff --git a/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
b/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
index e85dd51..808bbf8 100644
--- a/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
+++ b/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
@@ -22,7 +22,7 @@ export default function getKeyForFilterScopeTree({
activeFilterField,
checkedFilterFields,
}) {
- return activeFilterField
- ? safeStringify([activeFilterField])
- : safeStringify(checkedFilterFields);
+ return safeStringify(
+ activeFilterField ? [activeFilterField] : checkedFilterFields,
+ );
}
diff --git a/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
b/superset/assets/src/dashboard/util/isInDifferentFilterScopes.js
similarity index 64%
copy from superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
copy to superset/assets/src/dashboard/util/isInDifferentFilterScopes.js
index e85dd51..3789d07 100644
--- a/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
+++ b/superset/assets/src/dashboard/util/isInDifferentFilterScopes.js
@@ -16,13 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { safeStringify } from '../../utils/safeStringify';
-
-export default function getKeyForFilterScopeTree({
- activeFilterField,
- checkedFilterFields,
+export default function isInDifferentFilterScopes({
+ dashboardFilters = {},
+ source = [],
+ destination = [],
}) {
- return activeFilterField
- ? safeStringify([activeFilterField])
- : safeStringify(checkedFilterFields);
+ const sourceSet = new Set(source);
+ const destinationSet = new Set(destination);
+
+ const allScopes = [].concat(
+ ...Object.values(dashboardFilters).map(({ scopes }) =>
+ [].concat(...Object.values(scopes).map(({ scope }) => scope)),
+ ),
+ );
+ return allScopes.some(tab => destinationSet.has(tab) !== sourceSet.has(tab));
}
diff --git a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
index e1febff..c0e5157 100644
--- a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
@@ -30,6 +30,7 @@ export default function newEntitiesFromDrop({ dropResult,
layout }) {
const dropEntity = layout[destination.id];
const dropType = dropEntity.type;
let newDropChild = newComponentFactory(dragType, dragging.meta);
+ newDropChild.parents = (dropEntity.parents || []).concat(dropEntity.id);
if (componentIsResizable(dragging)) {
newDropChild.meta.width = // don't set a 0 width
@@ -48,11 +49,14 @@ export default function newEntitiesFromDrop({ dropResult,
layout }) {
if (wrapChildInRow) {
const rowWrapper = newComponentFactory(ROW_TYPE);
rowWrapper.children = [newDropChild.id];
+ rowWrapper.parents = (dropEntity.parents || []).concat(dropEntity.id);
newEntities[rowWrapper.id] = rowWrapper;
newDropChild = rowWrapper;
+ newDropChild.parents = rowWrapper.parents.concat(rowWrapper.id);
} else if (dragType === TABS_TYPE) {
// create a new tab component
const tabChild = newComponentFactory(TAB_TYPE);
+ tabChild.parents = (dropEntity.parents || []).concat(dropEntity.id);
newDropChild.children = [tabChild.id];
newEntities[tabChild.id] = tabChild;
}
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx
b/superset/assets/src/dashboard/util/propShapes.jsx
index b59f41e..6fb83e8 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -24,6 +24,7 @@ import headerStyleOptions from './headerStyleOptions';
export const componentShape = PropTypes.shape({
id: PropTypes.string.isRequired,
type: PropTypes.oneOf(Object.values(componentTypes)).isRequired,
+ parents: PropTypes.arrayOf(PropTypes.string),
children: PropTypes.arrayOf(PropTypes.string),
meta: PropTypes.shape({
// Dimensions
@@ -79,10 +80,21 @@ export const filterIndicatorPropShape = PropTypes.shape({
isInstantFilter: PropTypes.bool.isRequired,
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
- scope: PropTypes.arrayOf(PropTypes.string),
values: PropTypes.array.isRequired,
});
+export const dashboardFilterPropShape = PropTypes.shape({
+ chartId: PropTypes.number.isRequired,
+ componentId: PropTypes.string.isRequired,
+ filterName: PropTypes.string.isRequired,
+ directPathToFilter: PropTypes.arrayOf(PropTypes.string).isRequired,
+ isDateFilter: PropTypes.bool.isRequired,
+ isInstantFilter: PropTypes.bool.isRequired,
+ columns: PropTypes.object,
+ labels: PropTypes.object,
+ scopes: PropTypes.object,
+});
+
export const dashboardStatePropShape = PropTypes.shape({
sliceIds: PropTypes.arrayOf(PropTypes.number).isRequired,
expandedSlices: PropTypes.object,
@@ -128,16 +140,3 @@ export const filterScopeSelectorTreeNodePropShape =
PropTypes.oneOfType([
PropTypes.shape(parentShape),
leafType,
]);
-
-export const loadStatsPropShape = PropTypes.objectOf(
- PropTypes.shape({
- didLoad: PropTypes.bool.isRequired,
- minQueryStartTime: PropTypes.number.isRequired,
- id: PropTypes.string.isRequired,
- type: PropTypes.string.isRequired,
- parent_id: PropTypes.string,
- parent_type: PropTypes.string,
- index: PropTypes.number.isRequired,
- slice_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
- }),
-);
diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js
b/superset/assets/src/dashboard/util/serializeActiveFilterValues.js
similarity index 57%
copy from superset/assets/src/dashboard/util/getDashboardUrl.js
copy to superset/assets/src/dashboard/util/serializeActiveFilterValues.js
index 243c8cb..6497b10 100644
--- a/superset/assets/src/dashboard/util/getDashboardUrl.js
+++ b/superset/assets/src/dashboard/util/serializeActiveFilterValues.js
@@ -16,10 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
-/* eslint camelcase: 0 */
+import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey';
-export default function getDashboardUrl(pathname, filters = {}, hash = '') {
- const preselect_filters = encodeURIComponent(JSON.stringify(filters));
- const hashSection = hash ? `#${hash}` : '';
- return `${pathname}?preselect_filters=${preselect_filters}${hashSection}`;
+// input: { [id_column1]: values, [id_column2]: values }
+// output: { id: { column1: values, column2: values } }
+export default function serializeActiveFilterValues(activeFilters) {
+ return Object.entries(activeFilters).reduce((map, entry) => {
+ const [filterKey, { values }] = entry;
+ const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey);
+ const entryByChartId = {
+ ...map[chartId],
+ [column]: values,
+ };
+ return {
+ ...map,
+ [chartId]: entryByChartId,
+ };
+ }, {});
}
diff --git
a/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
b/superset/assets/src/dashboard/util/serializeFilterScopes.js
similarity index 67%
copy from
superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
copy to superset/assets/src/dashboard/util/serializeFilterScopes.js
index 76a2da6..303171f 100644
--- a/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
+++ b/superset/assets/src/dashboard/util/serializeFilterScopes.js
@@ -16,14 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-import getDashboardUrl from '../../../../src/dashboard/util/getDashboardUrl';
-
-describe('getChartIdsFromLayout', () => {
- it('should encode filters', () => {
- const filters = { 35: { key: ['value'] } };
- const url = getDashboardUrl('path', filters);
- expect(url).toBe(
-
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D',
+export default function serializeFilterScopes(dashboardFilters) {
+ return Object.values(dashboardFilters).reduce((map, { chartId, scopes }) => {
+ const scopesById = Object.keys(scopes).reduce(
+ (scopesByColumn, column) => ({
+ ...scopesByColumn,
+ [column]: scopes[column],
+ }),
+ {},
);
- });
-});
+
+ return {
+ ...map,
+ [chartId]: scopesById,
+ };
+ }, {});
+}
diff --git a/superset/views/core.py b/superset/views/core.py
index cbbd1a8..707e0be 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1657,12 +1657,18 @@ class Superset(BaseSupersetView):
dashboard.css = data.get("css")
dashboard.dashboard_title = data["dashboard_title"]
- if "filter_immune_slices" not in md:
- md["filter_immune_slices"] = []
if "timed_refresh_immune_slices" not in md:
md["timed_refresh_immune_slices"] = []
- if "filter_immune_slice_fields" not in md:
- md["filter_immune_slice_fields"] = {}
+
+ if "filter_scopes" in data:
+ md.pop("filter_immune_slices", None)
+ md.pop("filter_immune_slice_fields", None)
+ md["filter_scopes"] = json.loads(data.get("filter_scopes", "{}"))
+ else:
+ if "filter_immune_slices" not in md:
+ md["filter_immune_slices"] = []
+ if "filter_immune_slice_fields" not in md:
+ md["filter_immune_slice_fields"] = {}
md["expanded_slices"] = data["expanded_slices"]
md["refresh_frequency"] = data.get("refresh_frequency", 0)
default_filters_data = json.loads(data.get("default_filters", "{}"))