perf(dashboard): decrease number of rerenders of FiltersBadge (#16545)
* perf(dashboard): decrease rerenders in FiltersBadge
* implement caching of dashboard filter indicators
* Implement caching for native filter indicators
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
index b9c2b5f..317666b 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
@@ -57,6 +57,8 @@
);
};
+const indicatorsInitialState: Indicator[] = [];
+
export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
const dispatch = useDispatch();
const datasources = useSelector<RootState, any>(state => state.datasources);
@@ -77,9 +79,11 @@
state => state.dataMask,
);
- const [nativeIndicators, setNativeIndicators] = useState<Indicator[]>([]);
+ const [nativeIndicators, setNativeIndicators] = useState<Indicator[]>(
+ indicatorsInitialState,
+ );
const [dashboardIndicators, setDashboardIndicators] = useState<Indicator[]>(
- [],
+ indicatorsInitialState,
);
const onHighlightFilterSource = useCallback(
@@ -90,46 +94,79 @@
);
const chart = charts[chartId];
- const prevChartStatus = usePrevious(chart?.chartStatus);
+ const prevChart = usePrevious(chart);
+ const prevChartStatus = prevChart?.chartStatus;
+ const prevDashboardFilters = usePrevious(dashboardFilters);
+ const prevDatasources = usePrevious(datasources);
+ const showIndicators =
+ chart?.chartStatus && ['rendered', 'success'].includes(chart.chartStatus);
- const showIndicators = useCallback(
- () =>
- chart?.chartStatus && ['rendered', 'success'].includes(chart.chartStatus),
- [chart.chartStatus],
- );
useEffect(() => {
- if (!showIndicators) {
- setDashboardIndicators([]);
- }
- if (prevChartStatus !== 'success') {
- setDashboardIndicators(
- selectIndicatorsForChart(chartId, dashboardFilters, datasources, chart),
- );
+ if (!showIndicators && dashboardIndicators.length > 0) {
+ setDashboardIndicators(indicatorsInitialState);
+ } else if (prevChartStatus !== 'success') {
+ if (
+ chart?.queriesResponse?.[0]?.rejected_filters !==
+ prevChart?.queriesResponse?.[0]?.rejected_filters ||
+ chart?.queriesResponse?.[0]?.applied_filters !==
+ prevChart?.queriesResponse?.[0]?.applied_filters ||
+ dashboardFilters !== prevDashboardFilters ||
+ datasources !== prevDatasources
+ ) {
+ setDashboardIndicators(
+ selectIndicatorsForChart(
+ chartId,
+ dashboardFilters,
+ datasources,
+ chart,
+ ),
+ );
+ }
}
}, [
chart,
chartId,
dashboardFilters,
+ dashboardIndicators.length,
datasources,
+ prevChart?.queriesResponse,
prevChartStatus,
+ prevDashboardFilters,
+ prevDatasources,
showIndicators,
]);
+ const prevNativeFilters = usePrevious(nativeFilters);
+ const prevDashboardLayout = usePrevious(present);
+ const prevDataMask = usePrevious(dataMask);
+ const prevChartConfig = usePrevious(
+ dashboardInfo.metadata?.chart_configuration,
+ );
useEffect(() => {
- if (!showIndicators) {
- setNativeIndicators([]);
- }
- if (prevChartStatus !== 'success') {
- setNativeIndicators(
- selectNativeIndicatorsForChart(
- nativeFilters,
- dataMask,
- chartId,
- chart,
- present,
- dashboardInfo.metadata?.chart_configuration,
- ),
- );
+ if (!showIndicators && nativeIndicators.length > 0) {
+ setNativeIndicators(indicatorsInitialState);
+ } else if (prevChartStatus !== 'success') {
+ if (
+ chart?.queriesResponse?.[0]?.rejected_filters !==
+ prevChart?.queriesResponse?.[0]?.rejected_filters ||
+ chart?.queriesResponse?.[0]?.applied_filters !==
+ prevChart?.queriesResponse?.[0]?.applied_filters ||
+ nativeFilters !== prevNativeFilters ||
+ present !== prevDashboardLayout ||
+ dataMask !== prevDataMask ||
+ prevChartConfig !== dashboardInfo.metadata?.chart_configuration
+ ) {
+ setNativeIndicators(
+ selectNativeIndicatorsForChart(
+ nativeFilters,
+ dataMask,
+ chartId,
+ chart,
+ present,
+ dashboardInfo.metadata?.chart_configuration,
+ ),
+ );
+ }
}
}, [
chart,
@@ -137,8 +174,14 @@
dashboardInfo.metadata?.chart_configuration,
dataMask,
nativeFilters,
+ nativeIndicators.length,
present,
+ prevChart?.queriesResponse,
+ prevChartConfig,
prevChartStatus,
+ prevDashboardLayout,
+ prevDataMask,
+ prevNativeFilters,
showIndicators,
]);
@@ -155,17 +198,33 @@
[dashboardIndicators, nativeIndicators],
);
- const appliedCrossFilterIndicators = indicators.filter(
- indicator => indicator.status === IndicatorStatus.CrossFilterApplied,
+ const appliedCrossFilterIndicators = useMemo(
+ () =>
+ indicators.filter(
+ indicator => indicator.status === IndicatorStatus.CrossFilterApplied,
+ ),
+ [indicators],
);
- const appliedIndicators = indicators.filter(
- indicator => indicator.status === IndicatorStatus.Applied,
+ const appliedIndicators = useMemo(
+ () =>
+ indicators.filter(
+ indicator => indicator.status === IndicatorStatus.Applied,
+ ),
+ [indicators],
);
- const unsetIndicators = indicators.filter(
- indicator => indicator.status === IndicatorStatus.Unset,
+ const unsetIndicators = useMemo(
+ () =>
+ indicators.filter(
+ indicator => indicator.status === IndicatorStatus.Unset,
+ ),
+ [indicators],
);
- const incompatibleIndicators = indicators.filter(
- indicator => indicator.status === IndicatorStatus.Incompatible,
+ const incompatibleIndicators = useMemo(
+ () =>
+ indicators.filter(
+ indicator => indicator.status === IndicatorStatus.Incompatible,
+ ),
+ [indicators],
);
if (
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
index 34bdfce..a6aec8e 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
@@ -16,16 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { NO_TIME_RANGE, TIME_FILTER_MAP } from 'src/explore/constants';
-import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
-import { ChartConfiguration, Filters } from 'src/dashboard/reducers/types';
-import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
import {
ensureIsArray,
FeatureFlag,
FilterState,
isFeatureEnabled,
} from '@superset-ui/core';
+import { NO_TIME_RANGE, TIME_FILTER_MAP } from 'src/explore/constants';
+import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
+import { ChartConfiguration, Filters } from 'src/dashboard/reducers/types';
+import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
+import { areObjectsEqual } from 'src/reduxUtils';
import { Layout } from '../../types';
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
@@ -154,6 +155,8 @@
path?: string[];
};
+const cachedIndicatorsForChart = {};
+const cachedDashboardFilterDataForChart = {};
// inspects redux state to find what the filter indicators should be shown for a given chart
export const selectIndicatorsForChart = (
chartId: number,
@@ -165,37 +168,75 @@
// so grab the columns from the applied/rejected filters
const appliedColumns = getAppliedColumns(chart);
const rejectedColumns = getRejectedColumns(chart);
+ const matchingFilters = Object.values(filters).filter(
+ filter => filter.chartId !== chartId,
+ );
+ const matchingDatasources = Object.entries(datasources)
+ .filter(([key]) =>
+ matchingFilters.find(filter => filter.datasourceId === key),
+ )
+ .map(([, datasource]) => datasource);
- const indicators = Object.values(filters)
- .filter(filter => filter.chartId !== chartId)
- .reduce(
- (acc, filter) =>
- acc.concat(
- selectIndicatorsForChartFromFilter(
- chartId,
- filter,
- datasources[filter.datasourceId] || {},
- appliedColumns,
- rejectedColumns,
- ),
+ const cachedFilterData = cachedDashboardFilterDataForChart[chartId];
+ if (
+ cachedIndicatorsForChart[chartId] &&
+ areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
+ areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
+ areObjectsEqual(cachedFilterData?.matchingFilters, matchingFilters) &&
+ areObjectsEqual(cachedFilterData?.matchingDatasources, matchingDatasources)
+ ) {
+ return cachedIndicatorsForChart[chartId];
+ }
+ const indicators = matchingFilters.reduce(
+ (acc, filter) =>
+ acc.concat(
+ selectIndicatorsForChartFromFilter(
+ chartId,
+ filter,
+ datasources[filter.datasourceId] || {},
+ appliedColumns,
+ rejectedColumns,
),
- [] as Indicator[],
- );
+ ),
+ [] as Indicator[],
+ );
indicators.sort((a, b) => a.name.localeCompare(b.name));
+ cachedIndicatorsForChart[chartId] = indicators;
+ cachedDashboardFilterDataForChart[chartId] = {
+ appliedColumns,
+ rejectedColumns,
+ matchingFilters,
+ matchingDatasources,
+ };
return indicators;
};
+const cachedNativeIndicatorsForChart = {};
+let cachedNativeFilterDataForChart: any = {};
+const defaultChartConfig = {};
export const selectNativeIndicatorsForChart = (
nativeFilters: Filters,
dataMask: DataMaskStateWithId,
chartId: number,
chart: any,
dashboardLayout: Layout,
- chartConfiguration: ChartConfiguration = {},
+ chartConfiguration: ChartConfiguration = defaultChartConfig,
): Indicator[] => {
const appliedColumns = getAppliedColumns(chart);
const rejectedColumns = getRejectedColumns(chart);
+ const cachedFilterData = cachedNativeFilterDataForChart[chartId];
+ if (
+ cachedNativeIndicatorsForChart[chartId] &&
+ areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
+ areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
+ cachedNativeFilterDataForChart?.nativeFilters === nativeFilters &&
+ cachedNativeFilterDataForChart?.dashboardLayout === dashboardLayout &&
+ cachedNativeFilterDataForChart?.chartConfiguration === chartConfiguration &&
+ cachedNativeFilterDataForChart?.dataMask === dataMask
+ ) {
+ return cachedNativeIndicatorsForChart[chartId];
+ }
const getStatus = ({
label,
column,
@@ -283,5 +324,18 @@
})
.filter(filter => filter.status === IndicatorStatus.CrossFilterApplied);
}
- return crossFilterIndicators.concat(nativeFilterIndicators);
+ const indicators = crossFilterIndicators.concat(nativeFilterIndicators);
+ cachedNativeIndicatorsForChart[chartId] = indicators;
+ cachedNativeFilterDataForChart = {
+ ...cachedNativeFilterDataForChart,
+ nativeFilters,
+ dashboardLayout,
+ chartConfiguration,
+ dataMask,
+ };
+ cachedNativeFilterDataForChart[chartId] = {
+ appliedColumns,
+ rejectedColumns,
+ };
+ return indicators;
};
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index a0cdb94..9b42c81 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { FC } from 'react';
+import React, { FC, useMemo } from 'react';
import { styled, t } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { useDispatch, useSelector } from 'react-redux';
@@ -89,6 +89,14 @@
state => state.dataMask[slice?.slice_id]?.filterState?.value,
);
+ const indicator = useMemo(
+ () => ({
+ value: crossFilterValue,
+ name: t('Emitted values'),
+ }),
+ [crossFilterValue],
+ );
+
return (
<div className="chart-header" data-test="slice-header" ref={innerRef}>
<div className="header-title">
@@ -139,10 +147,7 @@
placement="top"
title={
<FilterIndicator
- indicator={{
- value: crossFilterValue,
- name: t('Emitted values'),
- }}
+ indicator={indicator}
text={t('Click to clear emitted filters')}
/>
}