perf(native-filters): avoid unnecessary reloading of charts (#14408)

* fix:fix get permission function

* refactor: filter default value

* refactor: update default value loading

* refactor: apply defaultValues

* lint: fix lint

* lint: fix lint

* test: fix test

* refactor: use extraFormData for reload charts

* test: fix tests

* test: fix tests

* test: fix tests
This commit is contained in:
simcha90 2021-05-03 14:56:41 +03:00 committed by GitHub
parent abbf4bf05a
commit bbb1f2d757
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 260 additions and 243 deletions

View File

@ -21,7 +21,6 @@ import { NativeFiltersState } from 'src/dashboard/reducers/types';
import { DataMaskStateWithId } from '../../src/dataMask/types'; import { DataMaskStateWithId } from '../../src/dataMask/types';
export const nativeFilters: NativeFiltersState = { export const nativeFilters: NativeFiltersState = {
isInitialized: true,
filterSets: {}, filterSets: {},
filters: { filters: {
'NATIVE_FILTER-e7Q8zKixx': { 'NATIVE_FILTER-e7Q8zKixx': {
@ -36,7 +35,11 @@ export const nativeFilters: NativeFiltersState = {
}, },
}, },
], ],
defaultValue: null, defaultDataMask: {
filterState: {
value: null,
},
},
cascadeParentIds: [], cascadeParentIds: [],
scope: { scope: {
rootPath: ['ROOT_ID'], rootPath: ['ROOT_ID'],
@ -61,7 +64,11 @@ export const nativeFilters: NativeFiltersState = {
}, },
}, },
], ],
defaultValue: null, defaultDataMask: {
filterState: {
value: null,
},
},
cascadeParentIds: [], cascadeParentIds: [],
scope: { scope: {
rootPath: ['ROOT_ID'], rootPath: ['ROOT_ID'],
@ -115,14 +122,17 @@ export const extraFormData: ExtraFormData = {
export const NATIVE_FILTER_ID = 'NATIVE_FILTER-p4LImrSgA'; export const NATIVE_FILTER_ID = 'NATIVE_FILTER-p4LImrSgA';
export const singleNativeFiltersState = { export const singleNativeFiltersState = {
isInitialized: true,
filters: { filters: {
[NATIVE_FILTER_ID]: { [NATIVE_FILTER_ID]: {
id: [NATIVE_FILTER_ID], id: [NATIVE_FILTER_ID],
name: 'eth', name: 'eth',
type: 'text', type: 'text',
targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }], targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }],
defaultValue: null, defaultDataMask: {
filterState: {
value: null,
},
},
cascadeParentIds: [], cascadeParentIds: [],
scope: { rootPath: ['ROOT_ID'], excluded: [227, 229] }, scope: { rootPath: ['ROOT_ID'], excluded: [227, 229] },
inverseSelection: false, inverseSelection: false,

View File

@ -167,7 +167,7 @@ describe('Dashboard', () => {
...OVERRIDE_FILTERS, ...OVERRIDE_FILTERS,
[NATIVE_FILTER_ID]: { [NATIVE_FILTER_ID]: {
scope: [230], scope: [230],
values: [extraFormData], values: extraFormData,
}, },
}); });
}); });

View File

@ -30,7 +30,6 @@ export const mockDataMaskInfo: DataMaskStateWithId = {
}; };
export const nativeFiltersInfo: NativeFiltersState = { export const nativeFiltersInfo: NativeFiltersState = {
isInitialized: true,
filterSets: { filterSets: {
'set-id': { 'set-id': {
id: 'DefaultsID', id: 'DefaultsID',
@ -54,7 +53,11 @@ export const nativeFiltersInfo: NativeFiltersState = {
}, },
}, },
], ],
defaultValue: null, defaultDataMask: {
filterState: {
value: null,
},
},
scope: { scope: {
rootPath: [], rootPath: [],
excluded: [], excluded: [],

View File

@ -52,7 +52,6 @@ describe('getFormDataWithExtraFilters', () => {
}, },
sliceId: chartId, sliceId: chartId,
nativeFilters: { nativeFilters: {
isInitialized: true,
filterSets: {}, filterSets: {},
filters: { filters: {
[filterId]: ({ [filterId]: ({

View File

@ -129,21 +129,15 @@ describe('Filter utils', () => {
); );
}); });
it('getSelectExtraFormData - col: "testCol", value: [], emptyFilter: false, inverseSelection: false', () => { it('getSelectExtraFormData - col: "testCol", value: [], emptyFilter: false, inverseSelection: false', () => {
expect(getSelectExtraFormData('testCol', [], false, false)).toEqual({ expect(getSelectExtraFormData('testCol', [], false, false)).toEqual({});
filters: [],
});
}); });
it('getSelectExtraFormData - col: "testCol", value: undefined, emptyFilter: false, inverseSelection: false', () => { it('getSelectExtraFormData - col: "testCol", value: undefined, emptyFilter: false, inverseSelection: false', () => {
expect( expect(
getSelectExtraFormData('testCol', undefined, false, false), getSelectExtraFormData('testCol', undefined, false, false),
).toEqual({ ).toEqual({});
filters: [],
});
}); });
it('getSelectExtraFormData - col: "testCol", value: null, emptyFilter: false, inverseSelection: false', () => { it('getSelectExtraFormData - col: "testCol", value: null, emptyFilter: false, inverseSelection: false', () => {
expect(getSelectExtraFormData('testCol', null, false, false)).toEqual({ expect(getSelectExtraFormData('testCol', null, false, false)).toEqual({});
filters: [],
});
}); });
}); });

View File

@ -121,13 +121,6 @@ class Chart extends React.PureComponent {
} }
runQuery() { runQuery() {
if (
this.props.dashboardId && // we on dashboard screen
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
!this.props.isFiltersInitialized
) {
return;
}
if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) { if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) {
// Load saved chart with a GET request // Load saved chart with a GET request
this.props.actions.getSavedChart( this.props.actions.getSavedChart(

View File

@ -37,9 +37,4 @@ function mapDispatchToProps(dispatch) {
}; };
} }
export default connect( export default connect(null, mapDispatchToProps)(Chart);
({ nativeFilters }) => ({
isFiltersInitialized: nativeFilters?.isInitialized,
}),
mapDispatchToProps,
)(Chart);

View File

@ -22,8 +22,8 @@ import { Dispatch } from 'redux';
import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types'; import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types';
import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types'; import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types';
import { import {
SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
setDataMaskForFilterConfigComplete,
} from 'src/dataMask/actions'; } from 'src/dataMask/actions';
import { HYDRATE_DASHBOARD } from './hydrate'; import { HYDRATE_DASHBOARD } from './hydrate';
import { dashboardInfoChanged } from './dashboardInfo'; import { dashboardInfoChanged } from './dashboardInfo';
@ -65,14 +65,6 @@ export interface SetFilterSetsConfigFail {
type: typeof SET_FILTER_SETS_CONFIG_FAIL; type: typeof SET_FILTER_SETS_CONFIG_FAIL;
filterSetsConfig: FilterSet[]; filterSetsConfig: FilterSet[];
} }
export const SET_FILTERS_INITIALIZED = 'SET_FILTERS_INITIALIZED';
export interface SetFiltersInitialized {
type: typeof SET_FILTERS_INITIALIZED;
}
export const setFiltersInitialized = (): SetFiltersInitialized => ({
type: SET_FILTERS_INITIALIZED,
});
export const setFilterConfiguration = ( export const setFilterConfiguration = (
filterConfig: FilterConfiguration, filterConfig: FilterConfiguration,
@ -108,11 +100,7 @@ export const setFilterConfiguration = (
type: SET_FILTER_CONFIG_COMPLETE, type: SET_FILTER_CONFIG_COMPLETE,
filterConfig, filterConfig,
}); });
dispatch({ dispatch(setDataMaskForFilterConfigComplete(filterConfig));
type: SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
unitName: DataMaskType.NativeFilters,
filterConfig,
});
} catch (err) { } catch (err) {
dispatch({ type: SET_FILTER_CONFIG_FAIL, filterConfig }); dispatch({ type: SET_FILTER_CONFIG_FAIL, filterConfig });
dispatch({ type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, filterConfig }); dispatch({ type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, filterConfig });
@ -200,6 +188,5 @@ export type AnyFilterAction =
| SetFilterSetsConfigBegin | SetFilterSetsConfigBegin
| SetFilterSetsConfigComplete | SetFilterSetsConfigComplete
| SetFilterSetsConfigFail | SetFilterSetsConfigFail
| SetFiltersInitialized
| SaveFilterSets | SaveFilterSets
| SetBooststapData; | SetBooststapData;

View File

@ -18,7 +18,7 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { t } from '@superset-ui/core'; import { isFeatureEnabled, t, FeatureFlag } from '@superset-ui/core';
import { PluginContext } from 'src/components/DynamicPlugins'; import { PluginContext } from 'src/components/DynamicPlugins';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
@ -56,6 +56,7 @@ const propTypes = {
charts: PropTypes.objectOf(chartPropShape).isRequired, charts: PropTypes.objectOf(chartPropShape).isRequired,
slices: PropTypes.objectOf(slicePropShape).isRequired, slices: PropTypes.objectOf(slicePropShape).isRequired,
activeFilters: PropTypes.object.isRequired, activeFilters: PropTypes.object.isRequired,
chartConfiguration: PropTypes.object.isRequired,
datasources: PropTypes.object.isRequired, datasources: PropTypes.object.isRequired,
ownDataCharts: PropTypes.object.isRequired, ownDataCharts: PropTypes.object.isRequired,
layout: PropTypes.object.isRequired, layout: PropTypes.object.isRequired,
@ -120,6 +121,11 @@ class Dashboard extends React.PureComponent {
}; };
} }
window.addEventListener('visibilitychange', this.onVisibilityChange); window.addEventListener('visibilitychange', this.onVisibilityChange);
this.applyCharts();
}
componentDidUpdate() {
this.applyCharts();
} }
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
@ -147,15 +153,28 @@ class Dashboard extends React.PureComponent {
} }
} }
componentDidUpdate() { applyCharts() {
const { hasUnsavedChanges, editMode } = this.props.dashboardState; const { hasUnsavedChanges, editMode } = this.props.dashboardState;
const { appliedFilters, appliedOwnDataCharts } = this; const { appliedFilters, appliedOwnDataCharts } = this;
const { activeFilters, ownDataCharts } = this.props; const { activeFilters, ownDataCharts, chartConfiguration } = this.props;
if (
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) &&
!chartConfiguration
) {
// For a first loading we need to wait for cross filters charts data loaded to get all active filters
// for correct comparing of filters to avoid unnecessary requests
return;
}
if ( if (
!editMode && !editMode &&
(!areObjectsEqual(appliedOwnDataCharts, ownDataCharts) || (!areObjectsEqual(appliedOwnDataCharts, ownDataCharts, {
!areObjectsEqual(appliedFilters, activeFilters)) ignoreUndefined: true,
}) ||
!areObjectsEqual(appliedFilters, activeFilters, {
ignoreUndefined: true,
}))
) { ) {
this.applyFilters(); this.applyFilters();
} }
@ -223,6 +242,9 @@ class Dashboard extends React.PureComponent {
!areObjectsEqual( !areObjectsEqual(
appliedFilters[filterKey].values, appliedFilters[filterKey].values,
activeFilters[filterKey].values, activeFilters[filterKey].values,
{
ignoreUndefined: true,
},
) )
) { ) {
affectedChartIds.push(...activeFilters[filterKey].scope); affectedChartIds.push(...activeFilters[filterKey].scope);

View File

@ -28,6 +28,7 @@ import FilterControl from 'src/dashboard/components/nativeFilters/FilterBar/Filt
import CascadeFilterControl from 'src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl'; import CascadeFilterControl from 'src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl';
import { CascadeFilter } from 'src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types'; import { CascadeFilter } from 'src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types';
import { Filter } from 'src/dashboard/components/nativeFilters/types'; import { Filter } from 'src/dashboard/components/nativeFilters/types';
import { RootState } from 'src/dashboard/types';
interface CascadePopoverProps { interface CascadePopoverProps {
filter: CascadeFilter; filter: CascadeFilter;
@ -82,7 +83,7 @@ const CascadePopover: React.FC<CascadePopoverProps> = ({
directPathToChild, directPathToChild,
}) => { }) => {
const [currentPathToChild, setCurrentPathToChild] = useState<string[]>(); const [currentPathToChild, setCurrentPathToChild] = useState<string[]>();
const dataMask = useSelector<any, DataMaskWithId>( const dataMask = useSelector<RootState, DataMaskWithId>(
state => state.dataMask[filter.id] ?? getInitialDataMask(filter.id), state => state.dataMask[filter.id] ?? getInitialDataMask(filter.id),
); );

View File

@ -17,8 +17,9 @@
* under the License. * under the License.
*/ */
import { DataMask } from '@superset-ui/core';
import { Filter } from '../../types'; import { Filter } from '../../types';
export interface CascadeFilter extends Filter { export type CascadeFilter = Filter & { dataMask?: DataMask } & {
cascadeChildren: CascadeFilter[]; cascadeChildren: CascadeFilter[];
} };

View File

@ -84,7 +84,6 @@ const addFilterFlow = () => {
const addFilterSetFlow = async () => { const addFilterSetFlow = async () => {
// add filter set // add filter set
userEvent.click(screen.getByText('Filter Sets (0)')); userEvent.click(screen.getByText('Filter Sets (0)'));
expect(screen.getByTestId(getTestId('new-filter-set-button'))).toBeDisabled();
// check description // check description
expect(screen.getByText('Filters (1)')).toBeInTheDocument(); expect(screen.getByText('Filters (1)')).toBeInTheDocument();
@ -92,7 +91,6 @@ const addFilterSetFlow = async () => {
expect(screen.getAllByText('Last week').length).toBe(2); expect(screen.getAllByText('Last week').length).toBe(2);
// apply filters // apply filters
userEvent.click(screen.getByTestId(getTestId('apply-button')));
expect(screen.getByTestId(getTestId('new-filter-set-button'))).toBeEnabled(); expect(screen.getByTestId(getTestId('new-filter-set-button'))).toBeEnabled();
// create filter set // create filter set
@ -139,7 +137,7 @@ describe('FilterBar', () => {
"name":"${FILTER_NAME}", "name":"${FILTER_NAME}",
"filterType":"filter_time", "filterType":"filter_time",
"targets":[{"datasetId":11,"column":{"name":"color"}}], "targets":[{"datasetId":11,"column":{"name":"color"}}],
"defaultValue":null, "defaultDataMask":{"filterState":{"value":null}},
"controlValues":{}, "controlValues":{},
"cascadeParentIds":[], "cascadeParentIds":[],
"scope":{"rootPath":["ROOT_ID"],"excluded":[]}, "scope":{"rootPath":["ROOT_ID"],"excluded":[]},
@ -154,7 +152,7 @@ describe('FilterBar', () => {
"name":"${FILTER_NAME}", "name":"${FILTER_NAME}",
"filterType":"filter_time", "filterType":"filter_time",
"targets":[{}], "targets":[{}],
"defaultValue":"Last week", "defaultDataMask":{"filterState":{"value":"Last week"},"extraFormData":{"time_range":"Last week"}},
"controlValues":{}, "controlValues":{},
"cascadeParentIds":[], "cascadeParentIds":[],
"scope":{"rootPath":["ROOT_ID"],"excluded":[]}, "scope":{"rootPath":["ROOT_ID"],"excluded":[]},
@ -163,7 +161,7 @@ describe('FilterBar', () => {
}, },
"dataMask":{ "dataMask":{
"${filterId}":{ "${filterId}":{
"extraFormData":{"override_form_data":{"time_range":"Last week"}}, "extraFormData":{"time_range":"Last week"},
"filterState":{"value":"Last week"}, "filterState":{"value":"Last week"},
"ownState":{}, "ownState":{},
"id":"${filterId}" "id":"${filterId}"
@ -308,10 +306,6 @@ describe('FilterBar', () => {
addFilterFlow(); addFilterFlow();
await screen.findByText('All Filters (1)'); await screen.findByText('All Filters (1)');
// apply filter
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled();
userEvent.click(screen.getByTestId(getTestId('apply-button')));
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
}); });
@ -319,6 +313,7 @@ describe('FilterBar', () => {
it.skip('add and apply filter set', async () => { it.skip('add and apply filter set', async () => {
// @ts-ignore // @ts-ignore
global.featureFlags = { global.featureFlags = {
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true, [FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,
}; };
renderWrapper(openedBarProps, stateWithoutNativeFilters); renderWrapper(openedBarProps, stateWithoutNativeFilters);
@ -326,21 +321,23 @@ describe('FilterBar', () => {
addFilterFlow(); addFilterFlow();
await screen.findByText('All Filters (1)'); await screen.findByText('All Filters (1)');
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled();
await addFilterSetFlow(); await addFilterSetFlow();
// change filter // change filter
userEvent.click(screen.getByText('All Filters (1)'));
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
await changeFilterValue(); await changeFilterValue();
await waitFor(() => expect(screen.getAllByText('Last day').length).toBe(2)); await waitFor(() => expect(screen.getAllByText('Last day').length).toBe(2));
// apply new filter value // apply new filter value
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled(); await waitFor(() =>
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled(),
);
userEvent.click(screen.getByTestId(getTestId('apply-button'))); userEvent.click(screen.getByTestId(getTestId('apply-button')));
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); await waitFor(() =>
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(),
);
// applying filter set // applying filter set
userEvent.click(screen.getByText('Filter Sets (1)')); userEvent.click(screen.getByText('Filter Sets (1)'));
@ -357,7 +354,6 @@ describe('FilterBar', () => {
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
}); });
// TODO: fix flakiness and re-enable
it.skip('add and edit filter set', async () => { it.skip('add and edit filter set', async () => {
// @ts-ignore // @ts-ignore
global.featureFlags = { global.featureFlags = {
@ -369,7 +365,6 @@ describe('FilterBar', () => {
addFilterFlow(); addFilterFlow();
await screen.findByText('All Filters (1)'); await screen.findByText('All Filters (1)');
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled();
await addFilterSetFlow(); await addFilterSetFlow();

View File

@ -19,7 +19,7 @@
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { NativeFiltersState } from 'src/dashboard/reducers/types'; import { NativeFiltersState } from 'src/dashboard/reducers/types';
import { mergeExtraFormData } from '../../utils'; import { mergeExtraFormData } from '../../utils';
import { useDataMask } from '../state'; import { useNativeFiltersDataMask } from '../state';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function useCascadingFilters(id: string) { export function useCascadingFilters(id: string) {
@ -29,7 +29,7 @@ export function useCascadingFilters(id: string) {
const filter = filters[id]; const filter = filters[id];
const cascadeParentIds: string[] = filter?.cascadeParentIds ?? []; const cascadeParentIds: string[] = filter?.cascadeParentIds ?? [];
let cascadedFilters = {}; let cascadedFilters = {};
const nativeFiltersDataMask = useDataMask(); const nativeFiltersDataMask = useNativeFiltersDataMask();
cascadeParentIds.forEach(parentId => { cascadeParentIds.forEach(parentId => {
const parentState = nativeFiltersDataMask[parentId] || {}; const parentState = nativeFiltersDataMask[parentId] || {};
const { extraFormData: parentExtra = {} } = parentState; const { extraFormData: parentExtra = {} } = parentState;

View File

@ -21,7 +21,9 @@ import { DataMask } from '@superset-ui/core';
import { Filter } from '../../types'; import { Filter } from '../../types';
export interface FilterProps { export interface FilterProps {
filter: Filter; filter: Filter & {
dataMask?: DataMask;
};
icon?: React.ReactElement; icon?: React.ReactElement;
directPathToChild?: string[]; directPathToChild?: string[];
onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void; onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void;

View File

@ -25,7 +25,7 @@ import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters'
import { DataMaskState } from 'src/dataMask/types'; import { DataMaskState } from 'src/dataMask/types';
import { WarningOutlined } from '@ant-design/icons'; import { WarningOutlined } from '@ant-design/icons';
import { ActionButtons } from './Footer'; import { ActionButtons } from './Footer';
import { useDataMask, useFilters, useFilterSets } from '../state'; import { useNativeFiltersDataMask, useFilters, useFilterSets } from '../state';
import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils'; import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils';
import { useFilterSetNameDuplicated } from './state'; import { useFilterSetNameDuplicated } from './state';
import { getFilterBarTestId } from '../index'; import { getFilterBarTestId } from '../index';
@ -72,7 +72,7 @@ const EditSection: FC<EditSectionProps> = ({
dataMaskSelected, dataMaskSelected,
disabled, disabled,
}) => { }) => {
const dataMaskApplied = useDataMask(); const dataMaskApplied = useNativeFiltersDataMask();
const dispatch = useDispatch(); const dispatch = useDispatch();
const filterSets = useFilterSets(); const filterSets = useFilterSets();
const filters = useFilters(); const filters = useFilters();

View File

@ -26,7 +26,7 @@ import { Filters, FilterSet, FilterSets } from 'src/dashboard/reducers/types';
import { areObjectsEqual } from 'src/reduxUtils'; import { areObjectsEqual } from 'src/reduxUtils';
import { findExistingFilterSet, generateFiltersSetId } from './utils'; import { findExistingFilterSet, generateFiltersSetId } from './utils';
import { Filter } from '../../types'; import { Filter } from '../../types';
import { useFilters, useDataMask, useFilterSets } from '../state'; import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state';
import Footer from './Footer'; import Footer from './Footer';
import FilterSetUnit from './FilterSetUnit'; import FilterSetUnit from './FilterSetUnit';
import { getFilterBarTestId } from '..'; import { getFilterBarTestId } from '..';
@ -85,7 +85,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
const dispatch = useDispatch(); const dispatch = useDispatch();
const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME); const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const dataMaskApplied = useDataMask(); const dataMaskApplied = useNativeFiltersDataMask();
const filterSets = useFilterSets(); const filterSets = useFilterSets();
const filterSetFilterValues = Object.values(filterSets); const filterSetFilterValues = Object.values(filterSets);
const filters = useFilters(); const filters = useFilters();
@ -111,7 +111,9 @@ const FilterSets: React.FC<FilterSetsProps> = ({
filterSet?: FilterSet, filterSet?: FilterSet,
) => ) =>
!filterValues.find(filter => filter?.id === id) || !filterValues.find(filter => filter?.id === id) ||
!areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id]); !areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id], {
ignoreUndefined: true,
});
const takeFilterSet = (id: string, target?: HTMLElement) => { const takeFilterSet = (id: string, target?: HTMLElement) => {
const ignoreSelectorHeader = 'ant-collapse-header'; const ignoreSelectorHeader = 'ant-collapse-header';
@ -137,13 +139,15 @@ const FilterSets: React.FC<FilterSetsProps> = ({
const filterSet = filterSets[id]; const filterSet = filterSets[id];
Object.values(filterSet?.dataMask ?? []).forEach(dataMask => { (Object.values(filterSet?.dataMask) ?? []).forEach(
const { extraFormData, filterState, id } = dataMask as DataMaskWithId; (dataMask: DataMaskWithId) => {
if (isFilterMissingOrContainsInvalidMetadata(id, filterSet)) { const { extraFormData, filterState, id } = dataMask;
return; if (isFilterMissingOrContainsInvalidMetadata(id, filterSet)) {
} return;
onFilterSelectionChange({ id }, { extraFormData, filterState }); }
}); onFilterSelectionChange({ id }, { extraFormData, filterState });
},
);
}; };
const handleRebuild = (id: string) => { const handleRebuild = (id: string) => {
@ -168,7 +172,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
dataMask: Object.keys(newFilters).reduce( dataMask: Object.keys(newFilters).reduce(
(prev, nextFilterId) => ({ (prev, nextFilterId) => ({
...prev, ...prev,
[nextFilterId]: filterSet.dataMask?.nativeFilters?.[nextFilterId], [nextFilterId]: filterSet.dataMask?.[nextFilterId],
}), }),
{}, {},
), ),

View File

@ -19,31 +19,38 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { HandlerFunction, styled, t } from '@superset-ui/core'; import { HandlerFunction, styled, t } from '@superset-ui/core';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import cx from 'classnames'; import cx from 'classnames';
import Icon from 'src/components/Icon'; import Icon from 'src/components/Icon';
import { Tabs } from 'src/common/components'; import { Tabs } from 'src/common/components';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { updateDataMask } from 'src/dataMask/actions'; import { updateDataMask } from 'src/dataMask/actions';
import { DataMaskState } from 'src/dataMask/types'; import {
DataMaskState,
DataMaskStateWithId,
DataMaskWithId,
} from 'src/dataMask/types';
import { useImmer } from 'use-immer'; import { useImmer } from 'use-immer';
import { areObjectsEqual } from 'src/reduxUtils'; import { areObjectsEqual } from 'src/reduxUtils';
import { testWithId } from 'src/utils/testUtils'; import { testWithId } from 'src/utils/testUtils';
import { Filter } from 'src/dashboard/components/nativeFilters/types'; import { Filter } from 'src/dashboard/components/nativeFilters/types';
import { setFiltersInitialized } from 'src/dashboard/actions/nativeFilters'; import {
import { mapParentFiltersToChildren, TabIds } from './utils'; getOnlyExtraFormData,
mapParentFiltersToChildren,
TabIds,
} from './utils';
import FilterSets from './FilterSets'; import FilterSets from './FilterSets';
import { import {
useDataMask, useNativeFiltersDataMask,
useFilters, useFilters,
useFilterSets, useFilterSets,
useFiltersInitialisation,
useFilterUpdates, useFilterUpdates,
} from './state'; } from './state';
import EditSection from './FilterSets/EditSection'; import EditSection from './FilterSets/EditSection';
import Header from './Header'; import Header from './Header';
import FilterControls from './FilterControls/FilterControls'; import FilterControls from './FilterControls/FilterControls';
import { getInitialDataMask } from '../../../../dataMask/reducer';
const BAR_WIDTH = `250px`; const BAR_WIDTH = `250px`;
@ -155,18 +162,16 @@ const FilterBar: React.FC<FiltersBarProps> = ({
directPathToChild, directPathToChild,
}) => { }) => {
const [editFilterSetId, setEditFilterSetId] = useState<string | null>(null); const [editFilterSetId, setEditFilterSetId] = useState<string | null>(null);
const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskState>({}); const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskStateWithId>(
const [ {},
lastAppliedFilterData, );
setLastAppliedFilterData,
] = useImmer<DataMaskState>({});
const dispatch = useDispatch(); const dispatch = useDispatch();
const filterSets = useFilterSets(); const filterSets = useFilterSets();
const filterSetFilterValues = Object.values(filterSets); const filterSetFilterValues = Object.values(filterSets);
const [tab, setTab] = useState(TabIds.AllFilters); const [tab, setTab] = useState(TabIds.AllFilters);
const filters = useFilters(); const filters = useFilters();
const filterValues = Object.values<Filter>(filters); const filterValues = Object.values<Filter>(filters);
const dataMaskApplied = useDataMask(); const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false); const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
const cascadeChildren = useMemo( const cascadeChildren = useMemo(
() => mapParentFiltersToChildren(filterValues), () => mapParentFiltersToChildren(filterValues),
@ -185,7 +190,10 @@ const FilterBar: React.FC<FiltersBarProps> = ({
dispatch(updateDataMask(filter.id, dataMask)); dispatch(updateDataMask(filter.id, dataMask));
} }
draft[filter.id] = dataMask; draft[filter.id] = {
...(getInitialDataMask(filter.id) as DataMaskWithId),
...dataMask,
};
}); });
}; };
@ -196,29 +204,18 @@ const FilterBar: React.FC<FiltersBarProps> = ({
dispatch(updateDataMask(filterId, dataMaskSelected[filterId])); dispatch(updateDataMask(filterId, dataMaskSelected[filterId]));
} }
}); });
setLastAppliedFilterData(() => dataMaskSelected);
}; };
const { isInitialized } = useFiltersInitialisation( useFilterUpdates(dataMaskSelected, setDataMaskSelected);
dataMaskSelected,
handleApply,
);
useEffect(() => {
if (isInitialized) {
dispatch(setFiltersInitialized());
}
}, [dispatch, isInitialized]);
useFilterUpdates(
dataMaskSelected,
setDataMaskSelected,
setLastAppliedFilterData,
);
const dataSelectedValues = Object.values(dataMaskSelected);
const dataAppliedValues = Object.values(dataMaskApplied);
const isApplyDisabled = const isApplyDisabled =
!isInitialized || areObjectsEqual(dataMaskSelected, lastAppliedFilterData); areObjectsEqual(
getOnlyExtraFormData(dataMaskSelected),
getOnlyExtraFormData(dataMaskApplied),
{ ignoreUndefined: true },
) || dataSelectedValues.length !== dataAppliedValues.length;
return ( return (
<BarWrapper {...getFilterBarTestId()} className={cx({ open: filtersOpen })}> <BarWrapper {...getFilterBarTestId()} className={cx({ open: filtersOpen })}>
<CollapsedBar <CollapsedBar

View File

@ -22,10 +22,14 @@ import {
Filters, Filters,
FilterSets as FilterSetsType, FilterSets as FilterSetsType,
} from 'src/dashboard/reducers/types'; } from 'src/dashboard/reducers/types';
import { DataMaskState, DataMaskStateWithId } from 'src/dataMask/types'; import {
import { useEffect, useState } from 'react'; DataMaskState,
import { areObjectsEqual } from 'src/reduxUtils'; DataMaskStateWithId,
import { Filter } from '../types'; DataMaskWithId,
} from 'src/dataMask/types';
import { useEffect } from 'react';
import { RootState } from 'src/dashboard/types';
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
export const useFilterSets = () => export const useFilterSets = () =>
useSelector<any, FilterSetsType>( useSelector<any, FilterSetsType>(
@ -35,44 +39,27 @@ export const useFilterSets = () =>
export const useFilters = () => export const useFilters = () =>
useSelector<any, Filters>(state => state.nativeFilters.filters); useSelector<any, Filters>(state => state.nativeFilters.filters);
export const useDataMask = () => export const useNativeFiltersDataMask = () => {
useSelector<any, DataMaskStateWithId>(state => state.dataMask); const dataMask = useSelector<RootState, DataMaskStateWithId>(
state => state.dataMask,
);
export const useFiltersInitialisation = ( return Object.values(dataMask)
dataMaskSelected: DataMaskState, .filter((item: DataMaskWithId) =>
handleApply: () => void, String(item.id).startsWith(NATIVE_FILTER_PREFIX),
) => { )
const [isInitialized, setIsInitialized] = useState<boolean>(false); .reduce(
const filters = useFilters(); (prev, next: DataMaskWithId) => ({ ...prev, [next.id]: next }),
const filterValues = Object.values<Filter>(filters); {},
useEffect(() => { ) as DataMaskStateWithId;
if (isInitialized) {
return;
}
const areFiltersInitialized = filterValues.every(filterValue =>
areObjectsEqual(
filterValue?.defaultValue,
dataMaskSelected[filterValue?.id]?.filterState?.value,
),
);
if (areFiltersInitialized) {
handleApply();
setIsInitialized(true);
}
}, [filterValues, dataMaskSelected, isInitialized]);
return {
isInitialized,
};
}; };
export const useFilterUpdates = ( export const useFilterUpdates = (
dataMaskSelected: DataMaskState, dataMaskSelected: DataMaskState,
setDataMaskSelected: (arg0: (arg0: DataMaskState) => void) => void, setDataMaskSelected: (arg0: (arg0: DataMaskState) => void) => void,
setLastAppliedFilterData: (arg0: (arg0: DataMaskState) => void) => void,
) => { ) => {
const filters = useFilters(); const filters = useFilters();
const dataMaskApplied = useDataMask(); const dataMaskApplied = useNativeFiltersDataMask();
useEffect(() => { useEffect(() => {
// Remove deleted filters from local state // Remove deleted filters from local state
@ -83,18 +70,5 @@ export const useFilterUpdates = (
}); });
} }
}); });
Object.keys(dataMaskApplied).forEach(appliedId => { }, [dataMaskApplied, dataMaskSelected, filters, setDataMaskSelected]);
if (!filters[appliedId]) {
setLastAppliedFilterData(draft => {
delete draft[appliedId];
});
}
});
}, [
dataMaskApplied,
dataMaskSelected,
filters,
setDataMaskSelected,
setLastAppliedFilterData,
]);
}; };

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import { DataMaskStateWithId } from 'src/dataMask/types';
import { Filter } from '../types'; import { Filter } from '../types';
export enum TabIds { export enum TabIds {
@ -39,3 +40,9 @@ export function mapParentFiltersToChildren(
}); });
return cascadeChildren; return cascadeChildren;
} }
export const getOnlyExtraFormData = (data: DataMaskStateWithId) =>
Object.values(data).reduce(
(prev, next) => ({ ...prev, [next.id]: next.extraFormData }),
{},
);

View File

@ -76,7 +76,7 @@ const ControlItems: FC<ControlItemsProps> = ({
return; return;
} }
setNativeFilterFieldValues(form, filterId, { setNativeFilterFieldValues(form, filterId, {
defaultValue: null, defaultDataMask: null,
}); });
forceUpdate(); forceUpdate();
}} }}

View File

@ -126,9 +126,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
const [metrics, setMetrics] = useState<Metric[]>([]); const [metrics, setMetrics] = useState<Metric[]>([]);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const [datasetDetails, setDatasetDetails] = useState<Record<string, any>>(); const [datasetDetails, setDatasetDetails] = useState<Record<string, any>>();
const formFilter = form.getFieldValue('filters')?.[filterId] || {}; const formFilter = form.getFieldValue('filters')?.[filterId] || {};
const nativeFilterItems = getChartMetadataRegistry().items; const nativeFilterItems = getChartMetadataRegistry().items;
const nativeFilterVizTypes = Object.entries(nativeFilterItems) const nativeFilterVizTypes = Object.entries(nativeFilterItems)
// @ts-ignore // @ts-ignore
@ -191,7 +189,6 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
const formData = getFormData({ const formData = getFormData({
datasetId: formFilter?.dataset?.value, datasetId: formFilter?.dataset?.value,
groupby: formFilter?.column, groupby: formFilter?.column,
defaultValue: formFilter?.defaultValue,
...formFilter, ...formFilter,
}); });
setNativeFilterFieldValues(form, filterId, { setNativeFilterFieldValues(form, filterId, {
@ -242,7 +239,6 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
const newFormData = getFormData({ const newFormData = getFormData({
datasetId, datasetId,
groupby: hasColumn ? formFilter?.column : undefined, groupby: hasColumn ? formFilter?.column : undefined,
defaultValue: formFilter?.defaultValue,
...formFilter, ...formFilter,
}); });
@ -294,7 +290,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
onChange={({ value }: { value: string }) => { onChange={({ value }: { value: string }) => {
setNativeFilterFieldValues(form, filterId, { setNativeFilterFieldValues(form, filterId, {
filterType: value, filterType: value,
defaultValue: null, defaultDataMask: null,
}); });
forceUpdate(); forceUpdate();
}} }}
@ -324,7 +320,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
// We need reset column when dataset changed // We need reset column when dataset changed
if (datasetId && e?.value !== datasetId) { if (datasetId && e?.value !== datasetId) {
setNativeFilterFieldValues(form, filterId, { setNativeFilterFieldValues(form, filterId, {
defaultValue: null, defaultDataMask: null,
column: null, column: null,
}); });
} }
@ -346,10 +342,10 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
form={form} form={form}
filterId={filterId} filterId={filterId}
datasetId={datasetId} datasetId={datasetId}
onChange={e => { onChange={() => {
// We need reset default value when when column changed // We need reset default value when when column changed
setNativeFilterFieldValues(form, filterId, { setNativeFilterFieldValues(form, filterId, {
defaultValue: null, defaultDataMask: null,
}); });
forceUpdate(); forceUpdate();
}} }}
@ -435,16 +431,16 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
)} )}
</StyledFormItem> </StyledFormItem>
<StyledFormItem <StyledFormItem
name={['filters', filterId, 'defaultValue']} name={['filters', filterId, 'defaultDataMask']}
initialValue={filterToEdit?.defaultValue} initialValue={filterToEdit?.defaultDataMask}
data-test="default-input" data-test="default-input"
label={<StyledLabel>{t('Default Value')}</StyledLabel>} label={<StyledLabel>{t('Default Value')}</StyledLabel>}
> >
{(!hasDataset || (!isDataDirty && hasFilledDataset)) && ( {(!hasDataset || (!isDataDirty && hasFilledDataset)) && (
<DefaultValue <DefaultValue
setDataMask={({ filterState }) => { setDataMask={dataMask => {
setNativeFilterFieldValues(form, filterId, { setNativeFilterFieldValues(form, filterId, {
defaultValue: filterState?.value, defaultDataMask: dataMask,
}); });
forceUpdate(); forceUpdate();
}} }}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { AdhocFilter } from '@superset-ui/core'; import { AdhocFilter, DataMask } from '@superset-ui/core';
import { Scope } from '../types'; import { Scope } from '../types';
export interface NativeFiltersFormItem { export interface NativeFiltersFormItem {
@ -32,6 +32,7 @@ export interface NativeFiltersFormItem {
[key: string]: any; [key: string]: any;
}; };
defaultValue: any; defaultValue: any;
defaultDataMask: DataMask;
parentFilter: { parentFilter: {
value: string; value: string;
label: string; label: string;

View File

@ -18,6 +18,7 @@
*/ */
import { FormInstance } from 'antd/lib/form'; import { FormInstance } from 'antd/lib/form';
import shortid from 'shortid'; import shortid from 'shortid';
import { getInitialDataMask } from 'src/dataMask/reducer';
import { FilterRemoval, NativeFiltersForm } from './types'; import { FilterRemoval, NativeFiltersForm } from './types';
import { Filter, FilterConfiguration, Target } from '../types'; import { Filter, FilterConfiguration, Target } from '../types';
@ -148,7 +149,7 @@ export const createHandleSave = (
filterType: formInputs.filterType, filterType: formInputs.filterType,
// for now there will only ever be one target // for now there will only ever be one target
targets: [target], targets: [target],
defaultValue: formInputs.defaultValue || null, defaultDataMask: formInputs.defaultDataMask ?? getInitialDataMask(),
cascadeParentIds: formInputs.parentFilter cascadeParentIds: formInputs.parentFilter
? [formInputs.parentFilter.value] ? [formInputs.parentFilter.value]
: [], : [],

View File

@ -41,8 +41,7 @@ export interface Target {
export interface Filter { export interface Filter {
cascadeParentIds: string[]; cascadeParentIds: string[];
defaultValue: any; defaultDataMask: DataMask;
dataMask?: DataMask;
isInstant: boolean; isInstant: boolean;
id: string; // randomly generated at filter creation id: string; // randomly generated at filter creation
name: string; name: string;

View File

@ -35,7 +35,7 @@ export const getFormData = ({
cascadingFilters = {}, cascadingFilters = {},
groupby, groupby,
inputRef, inputRef,
defaultValue, defaultDataMask,
controlValues, controlValues,
filterType, filterType,
sortMetric, sortMetric,
@ -73,7 +73,7 @@ export const getFormData = ({
metrics: ['count'], metrics: ['count'],
row_limit: 10000, row_limit: 10000,
showSearch: true, showSearch: true,
defaultValue, defaultValue: defaultDataMask?.filterState?.value,
time_range, time_range,
time_range_endpoints: ['inclusive', 'exclusive'], time_range_endpoints: ['inclusive', 'exclusive'],
url_params: {}, url_params: {},

View File

@ -18,9 +18,7 @@
*/ */
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Dashboard from '../components/Dashboard'; import Dashboard from '../components/Dashboard';
import { import {
addSliceToDashboard, addSliceToDashboard,
removeSliceFromDashboard, removeSliceFromDashboard,
@ -66,11 +64,12 @@ function mapStateToProps(state: RootState) {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
chartConfiguration: dashboardInfo.metadata?.chart_configuration, chartConfiguration: dashboardInfo.metadata?.chart_configuration,
nativeFilters: nativeFilters.filters, nativeFilters: nativeFilters.filters,
dataMask: getRelevantDataMask(dataMask, 'isApplied'), dataMask,
layout: dashboardLayout.present, layout: dashboardLayout.present,
}), }),
}, },
ownDataCharts: getRelevantDataMask(dataMask, 'ownState', 'ownState'), chartConfiguration: dashboardInfo.metadata?.chart_configuration,
ownDataCharts: getRelevantDataMask(dataMask, 'ownState'),
slices: sliceEntities.slices, slices: sliceEntities.slices,
layout: dashboardLayout.present, layout: dashboardLayout.present,
impressionId, impressionId,

View File

@ -21,7 +21,6 @@ import {
SAVE_FILTER_SETS, SAVE_FILTER_SETS,
SET_FILTER_CONFIG_COMPLETE, SET_FILTER_CONFIG_COMPLETE,
SET_FILTER_SETS_CONFIG_COMPLETE, SET_FILTER_SETS_CONFIG_COMPLETE,
SET_FILTERS_INITIALIZED,
} from 'src/dashboard/actions/nativeFilters'; } from 'src/dashboard/actions/nativeFilters';
import { FilterSet, NativeFiltersState } from './types'; import { FilterSet, NativeFiltersState } from './types';
import { FilterConfiguration } from '../components/nativeFilters/types'; import { FilterConfiguration } from '../components/nativeFilters/types';
@ -36,9 +35,7 @@ export function getInitialState({
filterConfig?: FilterConfiguration; filterConfig?: FilterConfiguration;
state?: NativeFiltersState; state?: NativeFiltersState;
}): NativeFiltersState { }): NativeFiltersState {
const state: Partial<NativeFiltersState> = { const state: Partial<NativeFiltersState> = {};
isInitialized: prevState?.isInitialized,
};
const filters = {}; const filters = {};
if (filterConfig) { if (filterConfig) {
@ -66,7 +63,6 @@ export function getInitialState({
export default function nativeFilterReducer( export default function nativeFilterReducer(
state: NativeFiltersState = { state: NativeFiltersState = {
isInitialized: false,
filters: {}, filters: {},
filterSets: {}, filterSets: {},
}, },
@ -95,12 +91,6 @@ export default function nativeFilterReducer(
case SET_FILTER_CONFIG_COMPLETE: case SET_FILTER_CONFIG_COMPLETE:
return getInitialState({ filterConfig: action.filterConfig, state }); return getInitialState({ filterConfig: action.filterConfig, state });
case SET_FILTERS_INITIALIZED:
return {
...state,
isInitialized: true,
};
case SET_FILTER_SETS_CONFIG_COMPLETE: case SET_FILTER_SETS_CONFIG_COMPLETE:
return getInitialState({ return getInitialState({
filterSetsConfig: action.filterSetsConfig, filterSetsConfig: action.filterSetsConfig,

View File

@ -97,7 +97,6 @@ export type Filters = {
}; };
export type NativeFiltersState = { export type NativeFiltersState = {
isInitialized: boolean;
filters: Filters; filters: Filters;
filterSets: FilterSets; filterSets: FilterSets;
}; };

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { ChartProps, JsonObject } from '@superset-ui/core'; import { ChartProps, ExtraFormData, JsonObject } from '@superset-ui/core';
import { chart } from 'src/chart/chartReducer'; import { chart } from 'src/chart/chartReducer';
import componentTypes from 'src/dashboard/util/componentTypes'; import componentTypes from 'src/dashboard/util/componentTypes';
import { DataMaskStateWithId } from '../dataMask/types'; import { DataMaskStateWithId } from '../dataMask/types';
@ -95,7 +95,7 @@ export type LayoutItem = {
type ActiveFilter = { type ActiveFilter = {
scope: number[]; scope: number[];
values: any[]; values: ExtraFormData;
}; };
export type ActiveFilters = { export type ActiveFilters = {

View File

@ -20,7 +20,7 @@ import { DataMaskStateWithId } from 'src/dataMask/types';
import { JsonObject } from '@superset-ui/core'; import { JsonObject } from '@superset-ui/core';
import { CHART_TYPE } from './componentTypes'; import { CHART_TYPE } from './componentTypes';
import { Scope } from '../components/nativeFilters/types'; import { Scope } from '../components/nativeFilters/types';
import { ActiveFilters, LayoutItem } from '../types'; import { ActiveFilters, Layout, LayoutItem } from '../types';
import { ChartConfiguration, Filters } from '../reducers/types'; import { ChartConfiguration, Filters } from '../reducers/types';
import { DASHBOARD_ROOT_ID } from './constants'; import { DASHBOARD_ROOT_ID } from './constants';
@ -51,12 +51,11 @@ export const findAffectedCharts = ({
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
activeFilters[filterId] = { activeFilters[filterId] = {
scope: [], scope: [],
values: [], values: extraFormData,
}; };
} }
// Add not excluded chart scopes(to know what charts refresh) and values(refresh only if its value changed) // Add not excluded chart scopes(to know what charts refresh) and values(refresh only if its value changed)
activeFilters[filterId].scope.push(chartId); activeFilters[filterId].scope.push(chartId);
activeFilters[filterId].values.push(extraFormData);
return; return;
} }
// If child is not chart, recursive iterate over its children // If child is not chart, recursive iterate over its children
@ -74,11 +73,10 @@ export const findAffectedCharts = ({
export const getRelevantDataMask = ( export const getRelevantDataMask = (
dataMask: DataMaskStateWithId, dataMask: DataMaskStateWithId,
filterBy: string, prop: string,
prop?: string,
): JsonObject | DataMaskStateWithId => ): JsonObject | DataMaskStateWithId =>
Object.values(dataMask) Object.values(dataMask)
.filter(item => item[filterBy]) .filter(item => item[prop])
.reduce( .reduce(
(prev, next) => ({ ...prev, [next.id]: prop ? next[prop] : next }), (prev, next) => ({ ...prev, [next.id]: prop ? next[prop] : next }),
{}, {},
@ -93,7 +91,7 @@ export const getAllActiveFilters = ({
chartConfiguration: ChartConfiguration; chartConfiguration: ChartConfiguration;
dataMask: DataMaskStateWithId; dataMask: DataMaskStateWithId;
nativeFilters: Filters; nativeFilters: Filters;
layout: { [key: string]: LayoutItem }; layout: Layout;
}): ActiveFilters => { }): ActiveFilters => {
const activeFilters = {}; const activeFilters = {};

View File

@ -42,7 +42,14 @@ export interface SetDataMaskForFilterConfigFail {
type: typeof SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL; type: typeof SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL;
filterConfig: FilterConfiguration; filterConfig: FilterConfiguration;
} }
export function setDataMaskForFilterConfigComplete(
filterConfig: FilterConfiguration,
): SetDataMaskForFilterConfigComplete {
return {
type: SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
filterConfig,
};
}
export function updateDataMask( export function updateDataMask(
filterId: string, filterId: string,
dataMask: DataMask, dataMask: DataMask,

View File

@ -20,23 +20,57 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
// <- When we work with Immer, we need reassign, so disabling lint // <- When we work with Immer, we need reassign, so disabling lint
import produce from 'immer'; import produce from 'immer';
import { DataMask, FeatureFlag } from '@superset-ui/core';
import { NATIVE_FILTER_PREFIX } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/utils';
import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
import { isFeatureEnabled } from 'src/featureFlags';
import { DataMaskStateWithId, DataMaskWithId } from './types'; import { DataMaskStateWithId, DataMaskWithId } from './types';
import { import {
AnyDataMaskAction, AnyDataMaskAction,
SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
UPDATE_DATA_MASK, UPDATE_DATA_MASK,
} from './actions'; } from './actions';
import { NATIVE_FILTER_PREFIX } from '../dashboard/components/nativeFilters/FiltersConfigModal/utils'; import {
import { Filter } from '../dashboard/components/nativeFilters/types'; Filter,
FilterConfiguration,
} from '../dashboard/components/nativeFilters/types';
export function getInitialDataMask(id?: string): DataMask;
export function getInitialDataMask(id: string): DataMaskWithId { export function getInitialDataMask(id: string): DataMaskWithId {
let otherProps = {};
if (id) {
otherProps = {
id,
};
}
return { return {
id, ...otherProps,
extraFormData: {}, extraFormData: {},
filterState: {}, filterState: {
value: null,
},
ownState: {}, ownState: {},
isApplied: false, } as DataMaskWithId;
}; }
function fillNativeFilters(
data: FilterConfiguration,
cleanState: DataMaskStateWithId,
draft: DataMaskStateWithId,
) {
data.forEach((filter: Filter) => {
cleanState[filter.id] = {
...getInitialDataMask(filter.id), // take initial data
...filter.defaultDataMask, // if something new came from BE - take it
...draft[filter.id], // keep local filter data
};
});
// Get back all other non-native filters
Object.values(draft).forEach(filter => {
if (!String(filter?.id).startsWith(NATIVE_FILTER_PREFIX)) {
cleanState[filter?.id] = filter;
}
});
} }
const dataMaskReducer = produce( const dataMaskReducer = produce(
@ -48,21 +82,31 @@ const dataMaskReducer = produce(
...getInitialDataMask(action.filterId), ...getInitialDataMask(action.filterId),
...draft[action.filterId], ...draft[action.filterId],
...action.dataMask, ...action.dataMask,
isApplied: true,
}; };
return draft; return draft;
// TODO: update hydrate to .ts
// @ts-ignore
case HYDRATE_DASHBOARD:
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
Object.keys(
// @ts-ignore
action.data.dashboardInfo?.metadata?.chart_configuration,
).forEach(id => {
cleanState[id] = {
...getInitialDataMask(id), // take initial data
};
});
}
fillNativeFilters(
// @ts-ignore
action.data.dashboardInfo?.metadata?.native_filter_configuration ??
[],
cleanState,
draft,
);
return cleanState;
case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE: case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE:
(action.filterConfig ?? []).forEach((filter: Filter) => { fillNativeFilters(action.filterConfig ?? [], cleanState, draft);
cleanState[filter.id] =
draft[filter.id] ?? getInitialDataMask(filter.id);
});
// Get back all other non-native filters
Object.values(draft).forEach(filter => {
if (!String(filter?.id).startsWith(NATIVE_FILTER_PREFIX)) {
cleanState[filter?.id] = filter;
}
});
return cleanState; return cleanState;
default: default:

View File

@ -25,5 +25,5 @@ export enum DataMaskType {
export type DataMaskState = { [id: string]: DataMask }; export type DataMaskState = { [id: string]: DataMask };
export type DataMaskWithId = { id: string; isApplied?: boolean } & DataMask; export type DataMaskWithId = { id: string } & DataMask;
export type DataMaskStateWithId = { [filterId: string]: DataMaskWithId }; export type DataMaskStateWithId = { [filterId: string]: DataMaskWithId };

View File

@ -41,17 +41,14 @@ export const getSelectExtraFormData = (
sqlExpression: '1 = 0', sqlExpression: '1 = 0',
}, },
]; ];
} else { } else if (value !== undefined && value !== null && value.length !== 0) {
extra.filters = extra.filters = [
value === undefined || value === null || value.length === 0 {
? [] col,
: [ op: inverseSelection ? ('NOT IN' as const) : ('IN' as const),
{ val: value,
col, },
op: inverseSelection ? ('NOT IN' as const) : ('IN' as const), ];
val: value,
},
];
} }
return extra; return extra;
}; };
@ -69,9 +66,11 @@ export const getRangeExtraFormData = (
filters.push({ col, op: '<=', val: upper }); filters.push({ col, op: '<=', val: upper });
} }
return { return filters.length
filters, ? {
}; filters,
}
: {};
}; };
export interface DataRecordValueFormatter { export interface DataRecordValueFormatter {