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 <grace....@airbnb.com> 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", "{}"))