diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 30c41d7c9f85d..236ce9698bb69 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -59,6 +59,7 @@ "fontsource-inter": "^3.0.5", "geolib": "^2.0.24", "global-box": "^1.2.0", + "immer": "^8.0.1", "html-webpack-plugin": "^4.5.1", "immutable": "^4.0.0-rc.12", "interweave": "^11.2.0", @@ -23776,6 +23777,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -32460,8 +32462,7 @@ "node_modules/immer": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", - "dev": true + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" }, "node_modules/immutable": { "version": "4.0.0-rc.12", @@ -35048,6 +35049,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -54860,6 +54862,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -55906,6 +55909,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -86375,8 +86379,7 @@ "immer": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", - "dev": true + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" }, "immutable": { "version": "4.0.0-rc.12", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 1a00dbe884bc3..33d0b16641b22 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -112,6 +112,7 @@ "geolib": "^2.0.24", "global-box": "^1.2.0", "html-webpack-plugin": "^4.5.1", + "immer": "^8.0.1", "immutable": "^4.0.0-rc.12", "interweave": "^11.2.0", "jquery": "^3.5.1", diff --git a/superset-frontend/spec/fixtures/mockNativeFilters.ts b/superset-frontend/spec/fixtures/mockNativeFilters.ts index b60bff23b58c8..6d475e3b0955e 100644 --- a/superset-frontend/spec/fixtures/mockNativeFilters.ts +++ b/superset-frontend/spec/fixtures/mockNativeFilters.ts @@ -17,6 +17,7 @@ * under the License. */ import { NativeFiltersState } from 'src/dashboard/reducers/types'; +import { DataMaskStateWithId } from '../../src/dataMask/types'; export const nativeFilters: NativeFiltersState = { filterSets: {}, @@ -72,33 +73,34 @@ export const nativeFilters: NativeFiltersState = { isInstant: true, }, }, - filtersState: { - crossFilters: {}, - ownFilters: {}, - nativeFilters: { - 'NATIVE_FILTER-e7Q8zKixx': { - id: 'NATIVE_FILTER-e7Q8zKixx', - extraFormData: { - append_form_data: { - filters: [ - { - col: 'region', - op: 'IN', - val: ['East Asia & Pacific'], - }, - ], - }, - }, - currentState: { - value: ['East Asia & Pacific'], +}; + +export const dataMaskWith2Filters: DataMaskStateWithId = { + crossFilters: {}, + ownFilters: {}, + nativeFilters: { + 'NATIVE_FILTER-e7Q8zKixx': { + id: 'NATIVE_FILTER-e7Q8zKixx', + extraFormData: { + append_form_data: { + filters: [ + { + col: 'region', + op: 'IN', + val: ['East Asia & Pacific'], + }, + ], }, }, - 'NATIVE_FILTER-x9QPw0so1': { - id: 'NATIVE_FILTER-x9QPw0so1', - extraFormData: {}, - currentState: {}, + currentState: { + value: ['East Asia & Pacific'], }, }, + 'NATIVE_FILTER-x9QPw0so1': { + id: 'NATIVE_FILTER-x9QPw0so1', + extraFormData: {}, + currentState: {}, + }, }, }; @@ -132,14 +134,15 @@ export const singleNativeFiltersState = { isRequired: false, }, }, - filtersState: { - nativeFilters: { - [NATIVE_FILTER_ID]: { - id: NATIVE_FILTER_ID, - extraFormData, - currentState: { - value: ['No, not an ethnic minority'], - }, +}; + +export const dataMaskWith1Filter = { + nativeFilters: { + [NATIVE_FILTER_ID]: { + id: NATIVE_FILTER_ID, + extraFormData, + currentState: { + value: ['No, not an ethnic minority'], }, }, }, diff --git a/superset-frontend/spec/fixtures/mockState.js b/superset-frontend/spec/fixtures/mockState.js index 1f1e6c4d2a06a..dfa77fdf477a0 100644 --- a/superset-frontend/spec/fixtures/mockState.js +++ b/superset-frontend/spec/fixtures/mockState.js @@ -18,7 +18,10 @@ */ import datasources from 'spec/fixtures/mockDatasource'; import messageToasts from 'spec/javascripts/messageToasts/mockMessageToasts'; -import { nativeFiltersInfo } from 'spec/javascripts/dashboard/fixtures/mockNativeFilters'; +import { + nativeFiltersInfo, + mockDataMaskInfo, +} from 'spec/javascripts/dashboard/fixtures/mockNativeFilters'; import chartQueries from './mockChartQueries'; import { dashboardLayout } from './mockDashboardLayout'; import dashboardInfo from './mockDashboardInfo'; @@ -31,6 +34,7 @@ export default { sliceEntities: sliceEntitiesForChart, charts: chartQueries, nativeFilters: nativeFiltersInfo, + dataMask: mockDataMaskInfo, dashboardInfo, dashboardFilters: emptyFilters, dashboardState, diff --git a/superset-frontend/spec/fixtures/mockStore.js b/superset-frontend/spec/fixtures/mockStore.js index 2b34ff390ea31..8bf1a073321f2 100644 --- a/superset-frontend/spec/fixtures/mockStore.js +++ b/superset-frontend/spec/fixtures/mockStore.js @@ -28,7 +28,7 @@ import { } from './mockDashboardLayout'; import { sliceId } from './mockChartQueries'; import { dashboardFilters } from './mockDashboardFilters'; -import { nativeFilters } from './mockNativeFilters'; +import { nativeFilters, dataMaskWith2Filters } from './mockNativeFilters'; export const storeWithState = state => createStore(rootReducer, state, compose(applyMiddleware(thunk))); @@ -77,6 +77,7 @@ export const getMockStoreWithFilters = () => createStore(rootReducer, { ...mockState, dashboardFilters, + dataMask: dataMaskWith2Filters, charts: { ...mockState.charts, [sliceIdWithAppliedFilter]: { @@ -102,6 +103,7 @@ export const getMockStoreWithNativeFilters = () => createStore(rootReducer, { ...mockState, nativeFilters, + dataMask: dataMaskWith2Filters, charts: { ...mockState.charts, [sliceIdWithAppliedFilter]: { diff --git a/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx index 96b7e6ce6bde5..733f82490c43d 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx @@ -33,6 +33,7 @@ import { NATIVE_FILTER_ID, layoutForSingleNativeFilter, singleNativeFiltersState, + dataMaskWith1Filter, } from 'spec/fixtures/mockNativeFilters'; import dashboardInfo from 'spec/fixtures/mockDashboardInfo'; import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout'; @@ -154,7 +155,8 @@ describe('Dashboard', () => { activeFilters: { ...OVERRIDE_FILTERS, ...getActiveNativeFilters({ - nativeFilters: singleNativeFiltersState, + dataMask: dataMaskWith1Filter, + filters: singleNativeFiltersState.filters, layout: layoutForSingleNativeFilter, }), }, diff --git a/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.js b/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.ts similarity index 66% rename from superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.js rename to superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.ts index a667ffc41a7e1..aaef255674f62 100644 --- a/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.js +++ b/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.ts @@ -16,12 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -export const nativeFiltersInfo = { +import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types'; +import { NativeFiltersState } from 'src/dashboard/reducers/types'; + +export const nativeFiltersInfo: NativeFiltersState = { + filterSets: {}, filters: { DefaultsID: { + cascadeParentIds: [], id: 'DefaultsID', name: 'test', - type: 'filter_select', + filterType: 'filter_select', targets: [ { datasetId: 0, @@ -37,17 +42,22 @@ export const nativeFiltersInfo = { excluded: [], }, isInstant: true, - allowsMultipleValues: true, - isRequired: false, + controlValues: { + allowsMultipleValues: true, + isRequired: false, + }, }, }, - filtersState: { - nativeFilters: { - DefaultsID: { - id: 'DefaultId', - currentState: { - value: [], - }, +}; + +export const mockDataMaskInfo: DataMaskStateWithId = { + [DataMaskType.CrossFilters]: {}, + [DataMaskType.OwnFilters]: {}, + [DataMaskType.NativeFilters]: { + DefaultsID: { + id: 'DefaultId', + currentState: { + value: [], }, }, }, diff --git a/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts b/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts index 7f538fba4fa45..18242e32491a3 100644 --- a/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts +++ b/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts @@ -61,15 +61,15 @@ describe('getFormDataWithExtraFilters', () => { }, } as unknown) as Filter, }, - filtersState: { - crossFilters: {}, - ownFilters: {}, - nativeFilters: { - [filterId]: { - id: filterId, - extraFormData: {}, - currentState: {}, - }, + }, + dataMask: { + crossFilters: {}, + ownFilters: {}, + nativeFilters: { + [filterId]: { + id: filterId, + extraFormData: {}, + currentState: {}, }, }, }, diff --git a/superset-frontend/src/chart/ChartContainer.jsx b/superset-frontend/src/chart/ChartContainer.jsx index 118acec4d5a22..9925986ad2f48 100644 --- a/superset-frontend/src/chart/ChartContainer.jsx +++ b/superset-frontend/src/chart/ChartContainer.jsx @@ -22,14 +22,14 @@ import { bindActionCreators } from 'redux'; import * as actions from './chartAction'; import { logEvent } from '../logger/actions'; import Chart from './Chart'; -import { updateExtraFormData } from '../dashboard/actions/nativeFilters'; +import { updateDataMask } from '../dataMask/actions'; function mapDispatchToProps(dispatch) { return { actions: bindActionCreators( { ...actions, - updateExtraFormData, + updateDataMask, logEvent, }, dispatch, diff --git a/superset-frontend/src/chart/ChartRenderer.jsx b/superset-frontend/src/chart/ChartRenderer.jsx index ca391bac95679..bb9857861346c 100644 --- a/superset-frontend/src/chart/ChartRenderer.jsx +++ b/superset-frontend/src/chart/ChartRenderer.jsx @@ -75,11 +75,8 @@ class ChartRenderer extends React.Component { setControlValue: this.handleSetControlValue, onFilterMenuOpen: this.props.onFilterMenuOpen, onFilterMenuClose: this.props.onFilterMenuClose, - setDataMask: filtersState => { - this.props.actions?.updateExtraFormData( - this.props.chartId, - filtersState, - ); + setDataMask: dataMask => { + this.props.actions?.updateDataMask(this.props.chartId, dataMask); }, }; } @@ -97,6 +94,7 @@ class ChartRenderer extends React.Component { return ( this.hasQueryResponseChange || nextProps.annotationData !== this.props.annotationData || + nextProps.ownCurrentState !== this.props.ownCurrentState || nextProps.height !== this.props.height || nextProps.width !== this.props.width || nextProps.triggerRender || diff --git a/superset-frontend/src/chart/chartAction.js b/superset-frontend/src/chart/chartAction.js index 139b7f17abf73..79418c1a99577 100644 --- a/superset-frontend/src/chart/chartAction.js +++ b/superset-frontend/src/chart/chartAction.js @@ -41,7 +41,7 @@ import { logEvent } from '../logger/actions'; import { Logger, LOG_ACTIONS_LOAD_CHART } from '../logger/LogUtils'; import { getClientErrorObject } from '../utils/getClientErrorObject'; import { allowCrossDomain as domainShardingEnabled } from '../utils/hostNamesConfig'; -import { updateExtraFormData } from '../dashboard/actions/nativeFilters'; +import { updateDataMask } from '../dataMask/actions'; export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED'; export function chartUpdateStarted(queryController, latestQueryFormData, key) { @@ -366,8 +366,8 @@ export function exploreJSON( }; if (dashboardId) requestParams.dashboard_id = dashboardId; - const setDataMask = filtersState => { - dispatch(updateExtraFormData(formData.slice_id, filtersState)); + const setDataMask = dataMask => { + dispatch(updateDataMask(formData.slice_id, dataMask)); }; const chartDataRequest = getChartDataRequest({ setDataMask, diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 6b3c655f57565..a1571fed7810c 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -17,16 +17,16 @@ * under the License. */ -import { makeApi, DataMask } from '@superset-ui/core'; +import { makeApi } from '@superset-ui/core'; import { Dispatch } from 'redux'; import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types'; -import { dashboardInfoChanged } from './dashboardInfo'; +import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types'; import { - FiltersState, - FilterState, - FiltersSet, - FilterStateType, -} from '../reducers/types'; + SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, + SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, +} from 'src/dataMask/actions'; +import { dashboardInfoChanged } from './dashboardInfo'; +import { FiltersSet } from '../reducers/types'; export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN'; export interface SetFilterConfigBegin { @@ -99,8 +99,13 @@ export const setFilterConfiguration = ( type: SET_FILTER_CONFIG_COMPLETE, filterConfig, }); + dispatch({ + type: SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, + filterConfig, + }); } catch (err) { dispatch({ type: SET_FILTER_CONFIG_FAIL, filterConfig }); + dispatch({ type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, filterConfig }); } }; @@ -143,62 +148,24 @@ export const setFilterSetsConfiguration = ( } }; -export const UPDATE_EXTRA_FORM_DATA = 'UPDATE_EXTRA_FORM_DATA'; -export interface UpdateExtraFormData { - type: typeof UPDATE_EXTRA_FORM_DATA; - filterId: string; - nativeFilters?: Omit; - crossFilters?: Omit; - ownFilters?: Omit; -} - export const SAVE_FILTER_SETS = 'SAVE_FILTER_SETS'; export interface SaveFilterSets { type: typeof SAVE_FILTER_SETS; name: string; - filtersState: Pick; + dataMask: Pick; filtersSetId: string; } -export const SET_FILTERS_STATE = 'SET_FILTERS_STATE'; -export interface SetFiltersState { - type: typeof SET_FILTERS_STATE; - filtersState: FiltersState; -} - -/** - * Sets the selected option(s) for a given filter - * @param filterId the id of the nativeFilters filter - * @param filterState - */ -export function updateExtraFormData( - filterId: string, - filterState: DataMask, -): UpdateExtraFormData { - return { - type: UPDATE_EXTRA_FORM_DATA, - filterId, - ...filterState, - }; -} - export function saveFilterSets( name: string, filtersSetId: string, - filtersState: Pick, + dataMask: Pick, ): SaveFilterSets { return { type: SAVE_FILTER_SETS, name, filtersSetId, - filtersState, - }; -} - -export function setFiltersState(filtersState: FiltersState): SetFiltersState { - return { - type: SET_FILTERS_STATE, - filtersState, + dataMask, }; } @@ -209,6 +176,4 @@ export type AnyFilterAction = | SetFilterSetsConfigBegin | SetFilterSetsConfigComplete | SetFilterSetsConfigFail - | SetFiltersState - | SaveFilterSets - | UpdateExtraFormData; + | SaveFilterSets; diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts index abef1afb36423..89eea4bc9c5a5 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts @@ -19,6 +19,7 @@ import { TIME_FILTER_MAP } from 'src/explore/constants'; import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters'; import { NativeFiltersState } from 'src/dashboard/reducers/types'; +import { DataMaskStateWithId } from 'src/dataMask/types'; import { Layout } from '../../types'; import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils'; @@ -173,6 +174,7 @@ export const selectIndicatorsForChart = ( export const selectNativeIndicatorsForChart = ( nativeFilters: NativeFiltersState, + dataMask: DataMaskStateWithId, chartId: number, charts: any, dashboardLayout: Layout, @@ -210,9 +212,8 @@ export const selectNativeIndicatorsForChart = ( layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId, ); const column = nativeFilter.targets[0]?.column?.name; - const filterState = - nativeFilters.filtersState.nativeFilters?.[nativeFilter.id]; - let value = filterState?.currentState?.value ?? []; + const dataMaskNativeFilters = dataMask.nativeFilters?.[nativeFilter.id]; + let value = dataMaskNativeFilters?.currentState?.value ?? []; if (!Array.isArray(value)) { value = [value]; } diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx index 8d7f60f0a815c..a0e6d673a2259 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx @@ -26,7 +26,7 @@ import { CascadeFilter } from './types'; interface CascadeFilterControlProps { filter: CascadeFilter; directPathToChild?: string[]; - onFilterSelectionChange: (filter: Filter, filterState: DataMask) => void; + onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void; } const StyledCascadeChildrenList = styled.ul` diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx index 0ab31ac8ca6bc..430003bb1bafb 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx @@ -21,7 +21,9 @@ import { styled, t, DataMask } from '@superset-ui/core'; import Popover from 'src/common/components/Popover'; import Icon from 'src/components/Icon'; import { Pill } from 'src/dashboard/components/FiltersBadge/Styles'; -import { useFilterStateNative } from './state'; +import { useSelector } from 'react-redux'; +import { getInitialMask } from 'src/dataMask/reducer'; +import { MaskWithId } from 'src/dataMask/types'; import FilterControl from './FilterControl'; import CascadeFilterControl from './CascadeFilterControl'; import { CascadeFilter } from './types'; @@ -32,7 +34,7 @@ interface CascadePopoverProps { visible: boolean; directPathToChild?: string[]; onVisibleChange: (visible: boolean) => void; - onFilterSelectionChange: (filter: Filter, filterState: DataMask) => void; + onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void; } const StyledTitleBox = styled.div` @@ -80,7 +82,10 @@ const CascadePopover: React.FC = ({ directPathToChild, }) => { const [currentPathToChild, setCurrentPathToChild] = useState(); - const filterStateNative = useFilterStateNative(filter.id); + const dataMask = useSelector( + state => + state.dataMask.nativeFilters[filter.id] ?? getInitialMask(filter.id), + ); useEffect(() => { setCurrentPathToChild(directPathToChild); @@ -93,7 +98,7 @@ const CascadePopover: React.FC = ({ const getActiveChildren = useCallback( (filter: CascadeFilter): CascadeFilter[] | null => { const children = filter.cascadeChildren || []; - const currentValue = filterStateNative.currentState?.value; + const currentValue = dataMask.currentState?.value; const activeChildren = children.flatMap( childFilter => getActiveChildren(childFilter) || [], @@ -109,7 +114,7 @@ const CascadePopover: React.FC = ({ return null; }, - [filterStateNative], + [dataMask], ); const getAllFilters = (filter: CascadeFilter): CascadeFilter[] => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index b1c403e641792..c6dacec883490 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -16,21 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -import { styled, t, tn, DataMask } from '@superset-ui/core'; +import { styled, t, tn } from '@superset-ui/core'; import React, { useState, useEffect, useMemo, ChangeEvent } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import cx from 'classnames'; import Button from 'src/components/Button'; import Icon from 'src/components/Icon'; -import { FiltersSet, FilterState } from 'src/dashboard/reducers/types'; +import { FiltersSet } from 'src/dashboard/reducers/types'; import { Input, Select } from 'src/common/components'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; +import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters'; +import { updateDataMask } from 'src/dataMask/actions'; import { - setFilterSetsConfiguration, - updateExtraFormData, -} from 'src/dashboard/actions/nativeFilters'; + DataMaskUnitWithId, + MaskWithId, + DataMaskUnit, + DataMaskState, +} from 'src/dataMask/types'; import FilterConfigurationLink from './FilterConfigurationLink'; -import { useFilters, useFilterSets, useFiltersStateNative } from './state'; +import { useFilters, useFilterSets } from './state'; import { useFilterConfiguration } from '../state'; import { Filter } from '../types'; import { @@ -176,11 +180,11 @@ const FilterBar: React.FC = ({ toggleFiltersBar, directPathToChild, }) => { - const [filterData, setFilterData] = useState<{ - [filterId: string]: Omit; - }>({}); + const [filterData, setFilterData] = useState({}); const dispatch = useDispatch(); - const filtersStateNative = useFiltersStateNative(); + const dataMaskState = useSelector( + state => state.dataMask.nativeFilters ?? {}, + ); const filterSets = useFilterSets(); const filterConfigs = useFilterConfiguration(); const filterSetsConfigs = useSelector( @@ -232,21 +236,21 @@ const FilterBar: React.FC = ({ const handleFilterSelectionChange = ( filter: Pick & Partial, - filtersState: DataMask, + dataMask: Partial, ) => { setFilterData(prevFilterData => { const children = cascadeChildren[filter.id] || []; // force instant updating on initialization or for parent filters if (filter.isInstant || children.length > 0) { - dispatch(updateExtraFormData(filter.id, filtersState)); + dispatch(updateDataMask(filter.id, dataMask)); } - if (!filtersState.nativeFilters) { + if (!dataMask.nativeFilters) { return { ...prevFilterData }; } return { ...prevFilterData, - [filter.id]: filtersState.nativeFilters, + [filter.id]: dataMask.nativeFilters, }; }); }; @@ -257,9 +261,9 @@ const FilterBar: React.FC = ({ return; } const filtersSet = filterSets[value]; - Object.values(filtersSet.filtersState?.nativeFilters ?? []).forEach( - filterState => { - const { extraFormData, currentState, id } = filterState as FilterState; + Object.values(filtersSet.dataMask?.nativeFilters ?? []).forEach( + dataMask => { + const { extraFormData, currentState, id } = dataMask as MaskWithId; handleFilterSelectionChange( { id }, { nativeFilters: { extraFormData, currentState } }, @@ -273,7 +277,7 @@ const FilterBar: React.FC = ({ filterIds.forEach(filterId => { if (filterData[filterId]) { dispatch( - updateExtraFormData(filterId, { + updateDataMask(filterId, { nativeFilters: filterData[filterId], }), ); @@ -294,8 +298,8 @@ const FilterBar: React.FC = ({ { name: filtersSetName.trim(), id: generateFiltersSetId(), - filtersState: { - nativeFilters: filtersStateNative, + dataMask: { + nativeFilters: dataMaskState, }, }, ]), @@ -319,7 +323,7 @@ const FilterBar: React.FC = ({ const handleResetAll = () => { filterConfigs.forEach(filter => { dispatch( - updateExtraFormData(filter.id, { + updateDataMask(filter.id, { nativeFilters: { currentState: { ...filterData[filter.id]?.currentState, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts index 316195d884316..cb5fe248b4c2e 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts @@ -17,13 +17,8 @@ * under the License. */ import { useSelector } from 'react-redux'; -import { getInitialFilterState } from 'src/dashboard/reducers/nativeFilters'; -import { - NativeFiltersState, - FilterState, - FilterSets, - FilterStates, -} from 'src/dashboard/reducers/types'; +import { NativeFiltersState, FilterSets } from 'src/dashboard/reducers/types'; +import { DataMaskStateWithId } from 'src/dataMask/types'; import { mergeExtraFormData } from '../utils'; import { Filter } from '../types'; @@ -31,12 +26,6 @@ export function useFilters() { return useSelector(state => state.nativeFilters.filters); } -export function useFiltersStateNative() { - return useSelector( - state => state.nativeFilters.filtersState.nativeFilters ?? {}, - ); -} - export function useFilterSets() { return useSelector( state => state.nativeFilters.filterSets ?? {}, @@ -44,27 +33,19 @@ export function useFilterSets() { } export function useCascadingFilters(id: string) { - const { - filters, - filtersState: { nativeFilters }, - } = useSelector(state => state.nativeFilters); + const { filters } = useSelector( + state => state.nativeFilters, + ); + const { nativeFilters } = useSelector( + state => state.dataMask, + ); const filter = filters[id]; const cascadeParentIds: string[] = filter?.cascadeParentIds ?? []; let cascadedFilters = {}; cascadeParentIds.forEach(parentId => { const parentState = nativeFilters[parentId] || {}; const { extraFormData: parentExtra = {} } = parentState; - cascadedFilters = { - nativeFilters: mergeExtraFormData(cascadedFilters, parentExtra), - }; + cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra); }); return cascadedFilters; } - -export function useFilterStateNative(id: string) { - return useSelector( - state => - state.nativeFilters.filtersState.nativeFilters[id] ?? - getInitialFilterState(id), - ); -} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts index 404eb419fc829..896aa47a55efd 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts @@ -24,7 +24,7 @@ export interface FilterProps { filter: Filter; icon?: React.ReactElement; directPathToChild?: string[]; - onFilterSelectionChange: (filter: Filter, filterState: DataMask) => void; + onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void; } export interface CascadeFilter extends Filter { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts index e63d484e52259..f3a21694b4e06 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts @@ -27,7 +27,7 @@ import { Charts } from 'src/dashboard/types'; import { RefObject } from 'react'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { Filter } from './types'; -import { NativeFiltersState } from '../../reducers/types'; +import { DataMaskStateWithId } from '../../../dataMask/types'; export const getFormData = ({ datasetId, @@ -117,21 +117,21 @@ export function isCrossFilter(vizType: string) { } export function getExtraFormData( - nativeFilters: NativeFiltersState, + dataMask: DataMaskStateWithId, charts: Charts, filterIdsAppliedOnChart: string[], ): ExtraFormData { let extraFormData: ExtraFormData = {}; filterIdsAppliedOnChart.forEach(key => { - const filterState = nativeFilters.filtersState.nativeFilters[key] || {}; - const { extraFormData: newExtra = {} } = filterState; + const singleDataMask = dataMask.nativeFilters[key] || {}; + const { extraFormData: newExtra = {} } = singleDataMask; extraFormData = mergeExtraFormData(extraFormData, newExtra); }); if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) { Object.entries(charts).forEach(([key, chart]) => { if (isCrossFilter(chart?.formData?.viz_type)) { - const filterState = nativeFilters.filtersState.crossFilters[key] || {}; - const { extraFormData: newExtra = {} } = filterState; + const singleDataMask = dataMask.crossFilters[key] || {}; + const { extraFormData: newExtra = {} } = singleDataMask; extraFormData = mergeExtraFormData(extraFormData, newExtra); } }); diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 0e39dc2f21ede..a56ad4cb69210 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -44,6 +44,7 @@ function mapStateToProps( dashboardInfo, dashboardState, dashboardLayout, + dataMask, datasources, sliceEntities, nativeFilters, @@ -65,7 +66,8 @@ function mapStateToProps( colorScheme, colorNamespace, sliceId: id, - nativeFilters, + nativeFilters: nativeFilters.filters, + dataMask, }); formData.dashboardId = dashboardInfo.id; @@ -82,7 +84,7 @@ function mapStateToProps( supersetCanExplore: !!dashboardInfo.superset_can_explore, supersetCanCSV: !!dashboardInfo.superset_can_csv, sliceCanEdit: !!dashboardInfo.slice_can_edit, - ownCurrentState: nativeFilters.filtersState.ownFilters?.[id]?.currentState, + ownCurrentState: dataMask.ownFilters?.[id]?.currentState, }; } diff --git a/superset-frontend/src/dashboard/containers/Dashboard.jsx b/superset-frontend/src/dashboard/containers/Dashboard.jsx index 90e7734729a25..e2bf97b8029ee 100644 --- a/superset-frontend/src/dashboard/containers/Dashboard.jsx +++ b/superset-frontend/src/dashboard/containers/Dashboard.jsx @@ -35,6 +35,7 @@ function mapStateToProps(state) { datasources, sliceEntities, charts, + dataMask, dashboardInfo, dashboardState, dashboardLayout, @@ -58,11 +59,12 @@ function mapStateToProps(state) { activeFilters: { ...getActiveFilters(), ...getActiveNativeFilters({ - nativeFilters, + filters: nativeFilters.filters, + dataMask, layout: dashboardLayout.present, }), }, - ownDataCharts: nativeFilters.filtersState.ownFilters ?? {}, + ownDataCharts: dataMask.ownFilters ?? {}, slices: sliceEntities.slices, layout: dashboardLayout.present, impressionId, diff --git a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx index 6a888f23ce08b..c38e320bae246 100644 --- a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx +++ b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx @@ -57,6 +57,7 @@ const mapStateToProps = ( dashboardFilters, nativeFilters, charts, + dataMask, dashboardLayout: { present }, }: any, { chartId }: FiltersBadgeProps, @@ -70,6 +71,7 @@ const mapStateToProps = ( const nativeIndicators = selectNativeIndicatorsForChart( nativeFilters, + dataMask, chartId, charts, present, diff --git a/superset-frontend/src/dashboard/reducers/index.js b/superset-frontend/src/dashboard/reducers/index.js index 61964de92e60e..481f1675ff4aa 100644 --- a/superset-frontend/src/dashboard/reducers/index.js +++ b/superset-frontend/src/dashboard/reducers/index.js @@ -18,7 +18,8 @@ */ import { combineReducers } from 'redux'; -import charts from '../../chart/chartReducer'; +import charts from 'src/chart/chartReducer'; +import dataMask from 'src/dataMask/reducer'; import dashboardInfo from './dashboardInfo'; import dashboardState from './dashboardState'; import dashboardFilters from './dashboardFilters'; @@ -35,6 +36,7 @@ export default combineReducers({ datasources, dashboardInfo, dashboardFilters, + dataMask, nativeFilters, dashboardState, dashboardLayout, diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index 59ed13576589c..92d0708ce4732 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -21,27 +21,10 @@ import { SAVE_FILTER_SETS, SET_FILTER_CONFIG_COMPLETE, SET_FILTER_SETS_CONFIG_COMPLETE, - SET_FILTERS_STATE, - UPDATE_EXTRA_FORM_DATA, - UpdateExtraFormData, } from 'src/dashboard/actions/nativeFilters'; -import { - FiltersSet, - FiltersState, - FilterState, - FilterStateType, - NativeFiltersState, -} from './types'; +import { FiltersSet, NativeFiltersState } from './types'; import { FilterConfiguration } from '../components/nativeFilters/types'; -export function getInitialFilterState(id: string): FilterState { - return { - id, - extraFormData: {}, - currentState: {}, - }; -} - export function getInitialState({ filterSetsConfig, filterConfig, @@ -53,30 +36,15 @@ export function getInitialState({ }): NativeFiltersState { const state: Partial = {}; - const emptyFiltersState = { - [FilterStateType.NativeFilters]: {}, - [FilterStateType.CrossFilters]: {}, - [FilterStateType.OwnFilters]: {}, - }; - const filters = {}; - const filtersState = { ...emptyFiltersState }; if (filterConfig) { filterConfig.forEach(filter => { const { id } = filter; filters[id] = filter; - filtersState.nativeFilters[id] = - prevState?.filtersState?.nativeFilters[id] || getInitialFilterState(id); }); state.filters = filters; - state.filtersState = { - ...emptyFiltersState, - ...prevState?.filtersState, - nativeFilters: filtersState.nativeFilters, - }; } else { state.filters = prevState?.filters ?? {}; - state.filtersState = prevState?.filtersState ?? { ...emptyFiltersState }; } if (filterSetsConfig) { @@ -92,59 +60,15 @@ export function getInitialState({ return state as NativeFiltersState; } -const getUnitState = ( - unitName: FilterStateType, - action: UpdateExtraFormData, - filtersState: FiltersState, -) => { - if (action[unitName]) - return { - ...filtersState[unitName], - [action.filterId]: { - ...filtersState[unitName][action.filterId], - ...action[unitName], - }, - }; - return { ...filtersState[unitName] }; -}; - export default function nativeFilterReducer( state: NativeFiltersState = { filters: {}, filterSets: {}, - filtersState: { - [FilterStateType.NativeFilters]: {}, - [FilterStateType.CrossFilters]: {}, - [FilterStateType.OwnFilters]: {}, - }, }, action: AnyFilterAction, ) { - const { filters, filtersState, filterSets } = state; + const { filterSets } = state; switch (action.type) { - case UPDATE_EXTRA_FORM_DATA: - return { - ...state, - filters, - filtersState: { - ...filtersState, - [FilterStateType.NativeFilters]: getUnitState( - FilterStateType.NativeFilters, - action, - filtersState, - ), - [FilterStateType.CrossFilters]: getUnitState( - FilterStateType.CrossFilters, - action, - filtersState, - ), - [FilterStateType.OwnFilters]: getUnitState( - FilterStateType.OwnFilters, - action, - filtersState, - ), - }, - }; case SAVE_FILTER_SETS: return { ...state, @@ -153,18 +77,10 @@ export default function nativeFilterReducer( [action.filtersSetId]: { id: action.filtersSetId, name: action.name, - filtersState: action.filtersState, + dataMask: action.dataMask, }, }, }; - case SET_FILTERS_STATE: - return { - ...state, - filtersState: { - ...filtersState, - ...action.filtersState, - }, - }; case SET_FILTER_CONFIG_COMPLETE: return getInitialState({ filterConfig: action.filterConfig, state }); diff --git a/superset-frontend/src/dashboard/reducers/types.ts b/superset-frontend/src/dashboard/reducers/types.ts index 03e273f9e44b5..78b3ea7812735 100644 --- a/superset-frontend/src/dashboard/reducers/types.ts +++ b/superset-frontend/src/dashboard/reducers/types.ts @@ -18,7 +18,7 @@ */ import componentTypes from 'src/dashboard/util/componentTypes'; -import { ExtraFormData, DataMaskCurrentState } from '@superset-ui/core'; +import { DataMaskStateWithId } from 'src/dataMask/types'; import { Filter } from '../components/nativeFilters/types'; export enum Scoping { @@ -67,43 +67,21 @@ export type LayoutItem = { }; }; -/** Current state of the filter, stored in `nativeFilters` in redux */ -export type FilterState = { - id: string; // ties this filter state to the config object - extraFormData?: ExtraFormData; - currentState: DataMaskCurrentState; -}; - export type FiltersSet = { id: string; name: string; - filtersState: Partial; + dataMask: Partial; }; export type FilterSets = { [filtersSetId: string]: FiltersSet; }; -export type FilterStates = { [filterId: string]: FilterState }; - -export enum FilterStateType { - NativeFilters = 'nativeFilters', - CrossFilters = 'crossFilters', - OwnFilters = 'ownFilters', -} - -export type FiltersState = { - [FilterStateType.NativeFilters]: FilterStates; - [FilterStateType.CrossFilters]: FilterStates; - [FilterStateType.OwnFilters]: FilterStates; -}; - export type Filters = { [filterId: string]: Filter; }; export type NativeFiltersState = { filters: Filters; - filtersState: FiltersState; filterSets: FilterSets; }; diff --git a/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts b/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts index af8954d43f612..aba57cf97b45b 100644 --- a/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts +++ b/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts @@ -19,8 +19,9 @@ import { CHART_TYPE } from './componentTypes'; import { Scope } from '../components/nativeFilters/types'; import { ActiveFilters, LayoutItem } from '../types'; -import { NativeFiltersState } from '../reducers/types'; +import { Filters } from '../reducers/types'; import { DASHBOARD_ROOT_ID } from './constants'; +import { DataMaskStateWithId } from '../../dataMask/types'; // Looking for affected chart scopes and values export const findAffectedCharts = ({ @@ -71,22 +72,24 @@ export const findAffectedCharts = ({ }; export const getActiveNativeFilters = ({ - nativeFilters, + filters, + dataMask, layout, }: { - nativeFilters: NativeFiltersState; + dataMask: DataMaskStateWithId; + filters: Filters; layout: { [key: string]: LayoutItem }; }): ActiveFilters => { const activeNativeFilters = {}; - if (!nativeFilters?.filtersState?.nativeFilters) { + if (!dataMask?.nativeFilters) { return activeNativeFilters; } Object.values({ - ...nativeFilters.filtersState.nativeFilters, - ...nativeFilters.filtersState.crossFilters, + ...dataMask.nativeFilters, + ...dataMask.crossFilters, }).forEach(({ id: filterId, extraFormData }) => { // TODO: for a case of a cross filters (should be updated will be added scope there) - const scope = nativeFilters?.filters?.[filterId]?.scope ?? { + const scope = filters?.[filterId]?.scope ?? { rootPath: [DASHBOARD_ROOT_ID], excluded: [], }; diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index b3d78272b1d4d..5130a6fd59cce 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -27,6 +27,7 @@ import { getExtraFormData, mergeExtraFormData, } from 'src/dashboard/components/nativeFilters/utils'; +import { DataMaskStateWithId } from 'src/dataMask/types'; import getEffectiveExtraFilters from './getEffectiveExtraFilters'; import { getActiveNativeFilters } from '../activeDashboardNativeFilters'; import { NativeFiltersState } from '../../reducers/types'; @@ -44,6 +45,7 @@ export interface GetFormDataWithExtraFiltersArguments { colorScheme?: string; colorNamespace?: string; sliceId: number; + dataMask: DataMaskStateWithId; nativeFilters: NativeFiltersState; } @@ -54,11 +56,12 @@ export default function getFormDataWithExtraFilters({ chart, charts, filters, + nativeFilters, colorScheme, colorNamespace, sliceId, layout, - nativeFilters, + dataMask, }: GetFormDataWithExtraFiltersArguments) { // Propagate color mapping to chart const scale = CategoricalColorNamespace.getScale(colorScheme, colorNamespace); @@ -72,20 +75,24 @@ export default function getFormDataWithExtraFilters({ cachedFormdataByChart[sliceId].color_namespace === colorNamespace && isEqual(cachedFormdataByChart[sliceId].label_colors, labelColors) && !!cachedFormdataByChart[sliceId] && - nativeFilters === undefined + dataMask === undefined ) { return cachedFormdataByChart[sliceId]; } let extraData: { extra_form_data?: JsonObject } = {}; - const activeNativeFilters = getActiveNativeFilters({ nativeFilters, layout }); + const activeNativeFilters = getActiveNativeFilters({ + dataMask, + layout, + filters: nativeFilters.filters, + }); const filterIdsAppliedOnChart = Object.entries(activeNativeFilters) .filter(([, { scope }]) => scope.includes(chart.id)) .map(([filterId]) => filterId); if (filterIdsAppliedOnChart.length) { extraData = { extra_form_data: getExtraFormData( - nativeFilters, + dataMask, charts, filterIdsAppliedOnChart, ), @@ -93,7 +100,7 @@ export default function getFormDataWithExtraFilters({ } const { extraFormData: newExtra = {} } = - nativeFilters.filtersState?.ownFilters?.[chart.id] ?? {}; + dataMask?.ownFilters?.[chart.id] ?? {}; extraData.extra_form_data = mergeExtraFormData( extraData?.extra_form_data, newExtra, diff --git a/superset-frontend/src/dataMask/actions.ts b/superset-frontend/src/dataMask/actions.ts new file mode 100644 index 0000000000000..667b6e7250cde --- /dev/null +++ b/superset-frontend/src/dataMask/actions.ts @@ -0,0 +1,62 @@ +/** + * 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 { MaskWithId } from './types'; +import { FilterConfiguration } from '../dashboard/components/nativeFilters/types'; + +export const UPDATE_DATA_MASK = 'UPDATE_DATA_MASK'; +export interface UpdateDataMask { + type: typeof UPDATE_DATA_MASK; + filterId: string; + nativeFilters?: Omit; + crossFilters?: Omit; + ownFilters?: Omit; +} + +export const SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE = + 'SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE'; +export interface SetDataMaskForFilterConfigComplete { + type: typeof SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE; + filterConfig: FilterConfiguration; +} +export const SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL = + 'SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL'; +export interface SetDataMaskForFilterConfigFail { + type: typeof SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL; + filterConfig: FilterConfiguration; +} + +export function updateDataMask( + filterId: string, + dataMask: { + nativeFilters?: Omit; + crossFilters?: Omit; + ownFilters?: Omit; + }, +): UpdateDataMask { + return { + type: UPDATE_DATA_MASK, + filterId, + ...dataMask, + }; +} + +export type AnyDataMaskAction = + | UpdateDataMask + | SetDataMaskForFilterConfigFail + | SetDataMaskForFilterConfigComplete; diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts new file mode 100644 index 0000000000000..261620f5e7c66 --- /dev/null +++ b/superset-frontend/src/dataMask/reducer.ts @@ -0,0 +1,85 @@ +/** + * 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. + */ + +/* eslint-disable no-param-reassign */ +// <- When we work with Immer, we need reassign, so disabling lint +import produce from 'immer'; +import { MaskWithId, DataMaskType, DataMaskStateWithId } from './types'; +import { + AnyDataMaskAction, + SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, + UPDATE_DATA_MASK, + UpdateDataMask, +} from './actions'; + +export function getInitialMask(id: string): MaskWithId { + return { + id, + extraFormData: {}, + currentState: {}, + }; +} + +const setUnitDataMask = ( + unitName: DataMaskType, + action: UpdateDataMask, + dataMaskState: DataMaskStateWithId, +) => { + if (action[unitName]) { + dataMaskState[unitName][action.filterId] = { + ...dataMaskState[unitName][action.filterId], + ...action[unitName], + id: action.filterId, + }; + } +}; + +const emptyDataMask = { + [DataMaskType.NativeFilters]: {}, + [DataMaskType.CrossFilters]: {}, + [DataMaskType.OwnFilters]: {}, +}; + +const dataMaskReducer = produce( + (draft: DataMaskStateWithId, action: AnyDataMaskAction) => { + switch (action.type) { + case UPDATE_DATA_MASK: + Object.values(DataMaskType).forEach(unitName => + setUnitDataMask(unitName, action, draft), + ); + break; + + case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE: + Object.values(DataMaskType).forEach(unitName => { + draft[unitName] = emptyDataMask[unitName]; + }); + (action.filterConfig ?? []).forEach(filter => { + draft[DataMaskType.NativeFilters][filter.id] = + draft[DataMaskType.NativeFilters][filter.id] ?? + getInitialMask(filter.id); + }); + break; + + default: + } + }, + emptyDataMask, +); + +export default dataMaskReducer; diff --git a/superset-frontend/src/dataMask/types.ts b/superset-frontend/src/dataMask/types.ts new file mode 100644 index 0000000000000..e73077aa79c11 --- /dev/null +++ b/superset-frontend/src/dataMask/types.ts @@ -0,0 +1,44 @@ +/** + * 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 { ExtraFormData, DataMaskCurrentState } from '@superset-ui/core'; + +export enum DataMaskType { + NativeFilters = 'nativeFilters', + CrossFilters = 'crossFilters', + OwnFilters = 'ownFilters', +} + +export type Mask = { + extraFormData?: ExtraFormData; + currentState: DataMaskCurrentState; +}; +export type DataMaskUnit = { [filterId: string]: Mask }; +export type DataMaskState = { + [DataMaskType.NativeFilters]: Mask; + [DataMaskType.CrossFilters]: Mask; + [DataMaskType.OwnFilters]: Mask; +}; + +export type MaskWithId = Mask & { id: string }; +export type DataMaskUnitWithId = { [filterId: string]: MaskWithId }; +export type DataMaskStateWithId = { + [DataMaskType.NativeFilters]: DataMaskUnitWithId; + [DataMaskType.CrossFilters]: DataMaskUnitWithId; + [DataMaskType.OwnFilters]: DataMaskUnitWithId; +}; diff --git a/superset-frontend/src/explore/components/ExploreChartPanel.jsx b/superset-frontend/src/explore/components/ExploreChartPanel.jsx index e84b5c4301459..779f515ab1d0f 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel.jsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel.jsx @@ -47,6 +47,7 @@ const propTypes = { table_name: PropTypes.string, vizType: PropTypes.string.isRequired, form_data: PropTypes.object, + ownCurrentState: PropTypes.object, standalone: PropTypes.number, timeout: PropTypes.number, refreshOverlayVisible: PropTypes.bool, @@ -189,6 +190,7 @@ const ExploreChartPanel = props => { { + props.actions.updateQueryFormData( + getFormDataFromControls(props.controls), + props.chart.id, + ); + props.actions.renderTriggered(new Date().getTime(), props.chart.id); + addHistory(); + }; + // effect to run when controls change useEffect(() => { if (previousControls) { @@ -320,15 +330,10 @@ function ExploreViewContainer(props) { key => props.controls[key].renderTrigger, ); if (hasDisplayControlChanged) { - props.actions.updateQueryFormData( - getFormDataFromControls(props.controls), - props.chart.id, - ); - props.actions.renderTriggered(new Date().getTime(), props.chart.id); - addHistory(); + reRenderChart(); } } - }, [props.controls]); + }, [props.controls, props.ownCurrentState]); const chartIsStale = useMemo(() => { if (previousControls) { @@ -350,6 +355,13 @@ function ExploreViewContainer(props) { return false; }, [previousControls, props.controls]); + useEffect(() => { + if (props.ownCurrentState !== undefined) { + onQuery(); + reRenderChart(); + } + }, [props.ownCurrentState]); + if (chartIsStale) { props.actions.logEvent(LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS); } @@ -540,8 +552,14 @@ function ExploreViewContainer(props) { ExploreViewContainer.propTypes = propTypes; function mapStateToProps(state) { - const { explore, charts, impressionId } = state; + const { explore, charts, impressionId, dataMask } = state; const form_data = getFormDataFromControls(explore.controls); + form_data.extra_form_data = mergeExtraFormData( + { ...form_data.extra_form_data }, + { + ...dataMask?.ownFilters?.[form_data.slice_id]?.extraFormData, + }, + ); const chartKey = Object.keys(charts)[0]; const chart = charts[chartKey]; return { @@ -571,6 +589,7 @@ function mapStateToProps(state) { forcedHeight: explore.forced_height, chart, timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT, + ownCurrentState: dataMask?.ownFilters?.[form_data.slice_id]?.currentState, impressionId, }; } diff --git a/superset-frontend/src/explore/reducers/index.js b/superset-frontend/src/explore/reducers/index.js index 6180d74b698c4..2a92b07d716e3 100644 --- a/superset-frontend/src/explore/reducers/index.js +++ b/superset-frontend/src/explore/reducers/index.js @@ -21,6 +21,7 @@ import { combineReducers } from 'redux'; import charts from '../../chart/chartReducer'; import saveModal from './saveModalReducer'; import explore from './exploreReducer'; +import dataMask from '../../dataMask/reducer'; import messageToasts from '../../messageToasts/reducers'; const impressionId = (state = '') => state; @@ -28,6 +29,7 @@ const impressionId = (state = '') => state; export default combineReducers({ charts, saveModal, + dataMask, explore, impressionId, messageToasts,