perf: Optimize native filters and cross filters (#31243)
This commit is contained in:
parent
5006f97f70
commit
ce0e06a935
|
|
@ -60,6 +60,7 @@ import {
|
|||
ensureSyncedSharedLabelsColors,
|
||||
ensureSyncedLabelsColorMap,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { getColorNamespace, resetColors } from 'src/utils/colorScheme';
|
||||
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
|
||||
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
|
||||
|
|
@ -160,14 +161,18 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
|||
};
|
||||
}
|
||||
|
||||
const chartLayoutItems = Object.values(dashboardLayout).filter(
|
||||
item => item?.type === CHART_TYPE,
|
||||
);
|
||||
|
||||
const chartsInScope: number[] = getChartIdsInFilterScope(
|
||||
filterScope.scope,
|
||||
chartIds,
|
||||
dashboardLayout,
|
||||
chartLayoutItems,
|
||||
);
|
||||
|
||||
const tabsInScope = findTabsWithChartsInScope(
|
||||
dashboardLayout,
|
||||
chartLayoutItems,
|
||||
chartsInScope,
|
||||
);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
} from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState';
|
||||
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
||||
import Badge from 'src/components/Badge';
|
||||
import DetailsPanelPopover from './DetailsPanel';
|
||||
import {
|
||||
|
|
@ -47,7 +48,7 @@ import {
|
|||
selectIndicatorsForChart,
|
||||
selectNativeIndicatorsForChart,
|
||||
} from '../nativeFilters/selectors';
|
||||
import { Chart, DashboardLayout, RootState } from '../../types';
|
||||
import { Chart, RootState } from '../../types';
|
||||
|
||||
export interface FiltersBadgeProps {
|
||||
chartId: number;
|
||||
|
|
@ -126,9 +127,7 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
|||
state => state.dashboardInfo.metadata?.chart_configuration,
|
||||
);
|
||||
const chart = useSelector<RootState, Chart>(state => state.charts[chartId]);
|
||||
const present = useSelector<RootState, DashboardLayout>(
|
||||
state => state.dashboardLayout.present,
|
||||
);
|
||||
const chartLayoutItems = useChartLayoutItems();
|
||||
const dataMask = useSelector<RootState, DataMaskStateWithId>(
|
||||
state => state.dataMask,
|
||||
);
|
||||
|
|
@ -207,7 +206,7 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
|||
]);
|
||||
|
||||
const prevNativeFilters = usePrevious(nativeFilters);
|
||||
const prevDashboardLayout = usePrevious(present);
|
||||
const prevChartLayoutItems = usePrevious(chartLayoutItems);
|
||||
const prevDataMask = usePrevious(dataMask);
|
||||
const prevChartConfig = usePrevious(chartConfiguration);
|
||||
|
||||
|
|
@ -221,7 +220,7 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
|||
chart?.queriesResponse?.[0]?.applied_filters !==
|
||||
prevChart?.queriesResponse?.[0]?.applied_filters ||
|
||||
nativeFilters !== prevNativeFilters ||
|
||||
present !== prevDashboardLayout ||
|
||||
chartLayoutItems !== prevChartLayoutItems ||
|
||||
dataMask !== prevDataMask ||
|
||||
prevChartConfig !== chartConfiguration
|
||||
) {
|
||||
|
|
@ -231,7 +230,7 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
|||
dataMask,
|
||||
chartId,
|
||||
chart,
|
||||
present,
|
||||
chartLayoutItems,
|
||||
chartConfiguration,
|
||||
),
|
||||
);
|
||||
|
|
@ -244,14 +243,14 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
|||
dataMask,
|
||||
nativeFilters,
|
||||
nativeIndicators.length,
|
||||
present,
|
||||
prevChart?.queriesResponse,
|
||||
prevChartConfig,
|
||||
prevChartStatus,
|
||||
prevDashboardLayout,
|
||||
prevDataMask,
|
||||
prevNativeFilters,
|
||||
showIndicators,
|
||||
chartLayoutItems,
|
||||
prevChartLayoutItems,
|
||||
]);
|
||||
|
||||
const indicators = useMemo(
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ const INITIAL_STATE = {
|
|||
3: { id: 3 },
|
||||
4: { id: 4 },
|
||||
},
|
||||
dashboardState: {
|
||||
sliceIds: [1, 2, 3, 4],
|
||||
},
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
metadata: {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { isDefined, NativeFilterScope, t } from '@superset-ui/core';
|
|||
import Modal from 'src/components/Modal';
|
||||
import {
|
||||
ChartConfiguration,
|
||||
Layout,
|
||||
RootState,
|
||||
isCrossFilterScopeGlobal,
|
||||
GlobalChartCrossFilterConfig,
|
||||
|
|
@ -32,6 +31,7 @@ import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilter
|
|||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||
import { saveChartConfiguration } from 'src/dashboard/actions/dashboardInfo';
|
||||
import { DEFAULT_CROSS_FILTER_SCOPING } from 'src/dashboard/constants';
|
||||
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
||||
import { ScopingModalContent } from './ScopingModalContent';
|
||||
import { NEW_CHART_SCOPING_ID } from './constants';
|
||||
|
||||
|
|
@ -76,9 +76,7 @@ export const ScopingModal = ({
|
|||
closeModal,
|
||||
}: ScopingModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const layout = useSelector<RootState, Layout>(
|
||||
state => state.dashboardLayout.present,
|
||||
);
|
||||
const chartLayoutItems = useChartLayoutItems();
|
||||
const chartIds = useChartIds();
|
||||
const [currentChartId, setCurrentChartId] = useState(initialChartId);
|
||||
const initialChartConfig = useSelector<RootState, ChartConfiguration>(
|
||||
|
|
@ -154,7 +152,11 @@ export const ScopingModal = ({
|
|||
id: currentChartId,
|
||||
crossFilters: {
|
||||
scope,
|
||||
chartsInScope: getChartIdsInFilterScope(scope, chartIds, layout),
|
||||
chartsInScope: getChartIdsInFilterScope(
|
||||
scope,
|
||||
chartIds,
|
||||
chartLayoutItems,
|
||||
),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
@ -162,7 +164,7 @@ export const ScopingModal = ({
|
|||
const globalChartsInScope = getChartIdsInFilterScope(
|
||||
scope,
|
||||
chartIds,
|
||||
layout,
|
||||
chartLayoutItems,
|
||||
);
|
||||
setGlobalChartConfig({
|
||||
scope,
|
||||
|
|
@ -176,7 +178,7 @@ export const ScopingModal = ({
|
|||
);
|
||||
}
|
||||
},
|
||||
[currentChartId, chartIds, layout],
|
||||
[currentChartId, chartIds, chartLayoutItems],
|
||||
);
|
||||
|
||||
const removeCustomScope = useCallback(
|
||||
|
|
@ -241,7 +243,11 @@ export const ScopingModal = ({
|
|||
id: newChartId,
|
||||
crossFilters: {
|
||||
scope: newScope,
|
||||
chartsInScope: getChartIdsInFilterScope(newScope, chartIds, layout),
|
||||
chartsInScope: getChartIdsInFilterScope(
|
||||
newScope,
|
||||
chartIds,
|
||||
chartLayoutItems,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -275,7 +281,7 @@ export const ScopingModal = ({
|
|||
currentChartId,
|
||||
globalChartConfig.chartsInScope,
|
||||
globalChartConfig.scope,
|
||||
layout,
|
||||
chartLayoutItems,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { DataMaskStateWithId, JsonObject } from '@superset-ui/core';
|
||||
import { DataMaskStateWithId } from '@superset-ui/core';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DashboardLayout, RootState } from 'src/dashboard/types';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
||||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||
import crossFiltersSelector from './selectors';
|
||||
import VerticalCollapse from './VerticalCollapse';
|
||||
import { useChartsVerboseMaps } from '../utils';
|
||||
|
|
@ -28,17 +30,13 @@ const CrossFiltersVertical = () => {
|
|||
const dataMask = useSelector<RootState, DataMaskStateWithId>(
|
||||
state => state.dataMask,
|
||||
);
|
||||
const chartConfiguration = useSelector<RootState, JsonObject>(
|
||||
state => state.dashboardInfo.metadata?.chart_configuration,
|
||||
);
|
||||
const dashboardLayout = useSelector<RootState, DashboardLayout>(
|
||||
state => state.dashboardLayout.present,
|
||||
);
|
||||
const chartIds = useChartIds();
|
||||
const chartLayoutItems = useChartLayoutItems();
|
||||
const verboseMaps = useChartsVerboseMaps();
|
||||
const selectedCrossFilters = crossFiltersSelector({
|
||||
dataMask,
|
||||
chartConfiguration,
|
||||
dashboardLayout,
|
||||
chartIds,
|
||||
chartLayoutItems,
|
||||
verboseMaps,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,36 +21,37 @@ import {
|
|||
DataMaskStateWithId,
|
||||
getColumnLabel,
|
||||
isDefined,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
import { DashboardLayout } from 'src/dashboard/types';
|
||||
import { LayoutItem } from 'src/dashboard/types';
|
||||
import { CrossFilterIndicator, getCrossFilterIndicator } from '../../selectors';
|
||||
|
||||
export const crossFiltersSelector = (props: {
|
||||
dataMask: DataMaskStateWithId;
|
||||
chartConfiguration: JsonObject;
|
||||
dashboardLayout: DashboardLayout;
|
||||
chartIds: number[];
|
||||
chartLayoutItems: LayoutItem[];
|
||||
verboseMaps: { [key: string]: Record<string, string> };
|
||||
}): CrossFilterIndicator[] => {
|
||||
const { dataMask, chartConfiguration, dashboardLayout, verboseMaps } = props;
|
||||
const chartsIds = Object.keys(chartConfiguration || {});
|
||||
const { dataMask, chartIds, chartLayoutItems, verboseMaps } = props;
|
||||
|
||||
return chartsIds
|
||||
return chartIds
|
||||
.map(chartId => {
|
||||
const id = Number(chartId);
|
||||
const filterIndicator = getCrossFilterIndicator(
|
||||
id,
|
||||
dataMask[id],
|
||||
dashboardLayout,
|
||||
chartId,
|
||||
dataMask[chartId],
|
||||
chartLayoutItems,
|
||||
);
|
||||
if (
|
||||
isDefined(filterIndicator.column) &&
|
||||
isDefined(filterIndicator.value)
|
||||
) {
|
||||
const verboseColName =
|
||||
verboseMaps[id]?.[getColumnLabel(filterIndicator.column)] ||
|
||||
verboseMaps[chartId]?.[getColumnLabel(filterIndicator.column)] ||
|
||||
filterIndicator.column;
|
||||
return { ...filterIndicator, column: verboseColName, emitterId: id };
|
||||
return {
|
||||
...filterIndicator,
|
||||
column: verboseColName,
|
||||
emitterId: chartId,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import {
|
|||
isFeatureEnabled,
|
||||
FeatureFlag,
|
||||
isNativeFilterWithDataMask,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
createHtmlPortalNode,
|
||||
|
|
@ -49,15 +48,13 @@ import {
|
|||
useDashboardHasTabs,
|
||||
useSelectFiltersInScope,
|
||||
} from 'src/dashboard/components/nativeFilters/state';
|
||||
import {
|
||||
DashboardLayout,
|
||||
FilterBarOrientation,
|
||||
RootState,
|
||||
} from 'src/dashboard/types';
|
||||
import { FilterBarOrientation, RootState } from 'src/dashboard/types';
|
||||
import DropdownContainer, {
|
||||
Ref as DropdownContainerRef,
|
||||
} from 'src/components/DropdownContainer';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
||||
import { FiltersOutOfScopeCollapsible } from '../FiltersOutOfScopeCollapsible';
|
||||
import { useFilterControlFactory } from '../useFilterControlFactory';
|
||||
import { FiltersDropdownContent } from '../FiltersDropdownContent';
|
||||
|
|
@ -65,12 +62,15 @@ import crossFiltersSelector from '../CrossFilters/selectors';
|
|||
import CrossFilter from '../CrossFilters/CrossFilter';
|
||||
import { useFilterOutlined } from '../useFilterOutlined';
|
||||
import { useChartsVerboseMaps } from '../utils';
|
||||
import { CrossFilterIndicator } from '../../selectors';
|
||||
|
||||
type FilterControlsProps = {
|
||||
dataMaskSelected: DataMaskStateWithId;
|
||||
onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void;
|
||||
};
|
||||
|
||||
const EMPTY_ARRAY: CrossFilterIndicator[] = [];
|
||||
|
||||
const FilterControls: FC<FilterControlsProps> = ({
|
||||
dataMaskSelected,
|
||||
onFilterSelectionChange,
|
||||
|
|
@ -90,12 +90,8 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||
const dataMask = useSelector<RootState, DataMaskStateWithId>(
|
||||
state => state.dataMask,
|
||||
);
|
||||
const chartConfiguration = useSelector<RootState, JsonObject>(
|
||||
state => state.dashboardInfo.metadata?.chart_configuration,
|
||||
);
|
||||
const dashboardLayout = useSelector<RootState, DashboardLayout>(
|
||||
state => state.dashboardLayout.present,
|
||||
);
|
||||
const chartIds = useChartIds();
|
||||
const chartLayoutItems = useChartLayoutItems();
|
||||
const verboseMaps = useChartsVerboseMaps();
|
||||
|
||||
const isCrossFiltersEnabled = isFeatureEnabled(
|
||||
|
|
@ -106,12 +102,12 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||
isCrossFiltersEnabled
|
||||
? crossFiltersSelector({
|
||||
dataMask,
|
||||
chartConfiguration,
|
||||
dashboardLayout,
|
||||
chartIds,
|
||||
chartLayoutItems,
|
||||
verboseMaps,
|
||||
})
|
||||
: [],
|
||||
[chartConfiguration, dashboardLayout, dataMask, isCrossFiltersEnabled],
|
||||
: EMPTY_ARRAY,
|
||||
[chartIds, chartLayoutItems, dataMask, isCrossFiltersEnabled, verboseMaps],
|
||||
);
|
||||
const { filterControlFactory, filtersWithValues } = useFilterControlFactory(
|
||||
dataMaskSelected,
|
||||
|
|
@ -154,18 +150,27 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||
[filtersWithValues, portalNodes],
|
||||
);
|
||||
|
||||
const renderVerticalContent = () => (
|
||||
<>
|
||||
{filtersInScope.map(renderer)}
|
||||
{showCollapsePanel && (
|
||||
<FiltersOutOfScopeCollapsible
|
||||
filtersOutOfScope={filtersOutOfScope}
|
||||
forceRender={hasRequiredFirst}
|
||||
hasTopMargin={filtersInScope.length > 0}
|
||||
renderer={renderer}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
const renderVerticalContent = useCallback(
|
||||
() => (
|
||||
<>
|
||||
{filtersInScope.map(renderer)}
|
||||
{showCollapsePanel && (
|
||||
<FiltersOutOfScopeCollapsible
|
||||
filtersOutOfScope={filtersOutOfScope}
|
||||
forceRender={hasRequiredFirst}
|
||||
hasTopMargin={filtersInScope.length > 0}
|
||||
renderer={renderer}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[
|
||||
filtersInScope,
|
||||
renderer,
|
||||
showCollapsePanel,
|
||||
filtersOutOfScope,
|
||||
hasRequiredFirst,
|
||||
],
|
||||
);
|
||||
|
||||
const overflowedFiltersInScope = useMemo(
|
||||
|
|
@ -230,70 +235,84 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||
return [...crossFilters, ...nativeFiltersInScope];
|
||||
}, [filtersInScope, renderer, rendererCrossFilter, selectedCrossFilters]);
|
||||
|
||||
const renderHorizontalContent = () => (
|
||||
<div
|
||||
css={(theme: SupersetTheme) => css`
|
||||
padding: 0 ${theme.gridUnit * 4}px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
<DropdownContainer
|
||||
items={items}
|
||||
dropdownTriggerIcon={
|
||||
<Icons.FilterSmall
|
||||
css={css`
|
||||
&& {
|
||||
margin-right: -4px;
|
||||
display: flex;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
}
|
||||
dropdownTriggerText={t('More filters')}
|
||||
dropdownTriggerCount={activeOverflowedFiltersInScope.length}
|
||||
dropdownTriggerTooltip={
|
||||
activeOverflowedFiltersInScope.length === 0
|
||||
? t('No applied filters')
|
||||
: t(
|
||||
'Applied filters: %s',
|
||||
activeOverflowedFiltersInScope
|
||||
.map(filter => filter.name)
|
||||
.join(', '),
|
||||
)
|
||||
}
|
||||
dropdownContent={
|
||||
overflowedFiltersInScope.length ||
|
||||
overflowedCrossFilters.length ||
|
||||
(filtersOutOfScope.length && showCollapsePanel)
|
||||
? () => (
|
||||
<FiltersDropdownContent
|
||||
overflowedCrossFilters={overflowedCrossFilters}
|
||||
filtersInScope={overflowedFiltersInScope}
|
||||
filtersOutOfScope={filtersOutOfScope}
|
||||
renderer={renderer}
|
||||
rendererCrossFilter={rendererCrossFilter}
|
||||
showCollapsePanel={showCollapsePanel}
|
||||
forceRenderOutOfScope={hasRequiredFirst}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
forceRender={hasRequiredFirst}
|
||||
ref={popoverRef}
|
||||
onOverflowingStateChange={({ overflowed: nextOverflowedIds }) => {
|
||||
if (
|
||||
nextOverflowedIds.length !== overflowedIds.length ||
|
||||
overflowedIds.reduce(
|
||||
(a, b, i) => a || b !== nextOverflowedIds[i],
|
||||
false,
|
||||
)
|
||||
) {
|
||||
setOverflowedIds(nextOverflowedIds);
|
||||
const renderHorizontalContent = useCallback(
|
||||
() => (
|
||||
<div
|
||||
css={(theme: SupersetTheme) => css`
|
||||
padding: 0 ${theme.gridUnit * 4}px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
<DropdownContainer
|
||||
items={items}
|
||||
dropdownTriggerIcon={
|
||||
<Icons.FilterSmall
|
||||
css={css`
|
||||
&& {
|
||||
margin-right: -4px;
|
||||
display: flex;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
dropdownTriggerText={t('More filters')}
|
||||
dropdownTriggerCount={activeOverflowedFiltersInScope.length}
|
||||
dropdownTriggerTooltip={
|
||||
activeOverflowedFiltersInScope.length === 0
|
||||
? t('No applied filters')
|
||||
: t(
|
||||
'Applied filters: %s',
|
||||
activeOverflowedFiltersInScope
|
||||
.map(filter => filter.name)
|
||||
.join(', '),
|
||||
)
|
||||
}
|
||||
dropdownContent={
|
||||
overflowedFiltersInScope.length ||
|
||||
overflowedCrossFilters.length ||
|
||||
(filtersOutOfScope.length && showCollapsePanel)
|
||||
? () => (
|
||||
<FiltersDropdownContent
|
||||
overflowedCrossFilters={overflowedCrossFilters}
|
||||
filtersInScope={overflowedFiltersInScope}
|
||||
filtersOutOfScope={filtersOutOfScope}
|
||||
renderer={renderer}
|
||||
rendererCrossFilter={rendererCrossFilter}
|
||||
showCollapsePanel={showCollapsePanel}
|
||||
forceRenderOutOfScope={hasRequiredFirst}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
forceRender={hasRequiredFirst}
|
||||
ref={popoverRef}
|
||||
onOverflowingStateChange={({ overflowed: nextOverflowedIds }) => {
|
||||
if (
|
||||
nextOverflowedIds.length !== overflowedIds.length ||
|
||||
overflowedIds.reduce(
|
||||
(a, b, i) => a || b !== nextOverflowedIds[i],
|
||||
false,
|
||||
)
|
||||
) {
|
||||
setOverflowedIds(nextOverflowedIds);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[
|
||||
items,
|
||||
activeOverflowedFiltersInScope,
|
||||
overflowedFiltersInScope,
|
||||
overflowedCrossFilters,
|
||||
filtersOutOfScope,
|
||||
showCollapsePanel,
|
||||
renderer,
|
||||
rendererCrossFilter,
|
||||
hasRequiredFirst,
|
||||
overflowedIds,
|
||||
],
|
||||
);
|
||||
|
||||
const overflowedByIndex = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ const FilterValue: FC<FilterControlProps> = ({
|
|||
datasetId,
|
||||
groupby,
|
||||
handleFilterLoadFinish,
|
||||
JSON.stringify(filter),
|
||||
filter,
|
||||
hasDataSource,
|
||||
isRefreshing,
|
||||
shouldRefresh,
|
||||
|
|
|
|||
|
|
@ -17,18 +17,19 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { FC, memo } from 'react';
|
||||
import { FC, memo, useMemo } from 'react';
|
||||
import {
|
||||
DataMaskStateWithId,
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
JsonObject,
|
||||
styled,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import Loading from 'src/components/Loading';
|
||||
import { DashboardLayout, RootState } from 'src/dashboard/types';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
|
||||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||
import { useSelector } from 'react-redux';
|
||||
import FilterControls from './FilterControls/FilterControls';
|
||||
import { useChartsVerboseMaps, getFilterBarTestId } from './utils';
|
||||
|
|
@ -36,6 +37,7 @@ import { HorizontalBarProps } from './types';
|
|||
import FilterBarSettings from './FilterBarSettings';
|
||||
import FilterConfigurationLink from './FilterConfigurationLink';
|
||||
import crossFiltersSelector from './CrossFilters/selectors';
|
||||
import { CrossFilterIndicator } from '../selectors';
|
||||
|
||||
const HorizontalBar = styled.div`
|
||||
${({ theme }) => `
|
||||
|
|
@ -96,6 +98,7 @@ const FiltersLinkContainer = styled.div<{ hasFilters: boolean }>`
|
|||
`}
|
||||
`;
|
||||
|
||||
const EMPTY_ARRAY: CrossFilterIndicator[] = [];
|
||||
const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
||||
actions,
|
||||
canEdit,
|
||||
|
|
@ -108,25 +111,26 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
|
|||
const dataMask = useSelector<RootState, DataMaskStateWithId>(
|
||||
state => state.dataMask,
|
||||
);
|
||||
const chartConfiguration = useSelector<RootState, JsonObject>(
|
||||
state => state.dashboardInfo.metadata?.chart_configuration,
|
||||
);
|
||||
const dashboardLayout = useSelector<RootState, DashboardLayout>(
|
||||
state => state.dashboardLayout.present,
|
||||
);
|
||||
const chartIds = useChartIds();
|
||||
const chartLayoutItems = useChartLayoutItems();
|
||||
const isCrossFiltersEnabled = isFeatureEnabled(
|
||||
FeatureFlag.DashboardCrossFilters,
|
||||
);
|
||||
const verboseMaps = useChartsVerboseMaps();
|
||||
|
||||
const selectedCrossFilters = isCrossFiltersEnabled
|
||||
? crossFiltersSelector({
|
||||
dataMask,
|
||||
chartConfiguration,
|
||||
dashboardLayout,
|
||||
verboseMaps,
|
||||
})
|
||||
: [];
|
||||
const selectedCrossFilters = useMemo(
|
||||
() =>
|
||||
isCrossFiltersEnabled
|
||||
? crossFiltersSelector({
|
||||
dataMask,
|
||||
chartIds,
|
||||
chartLayoutItems,
|
||||
verboseMaps,
|
||||
})
|
||||
: EMPTY_ARRAY,
|
||||
[chartIds, chartLayoutItems, dataMask, isCrossFiltersEnabled, verboseMaps],
|
||||
);
|
||||
|
||||
const hasFilters = filterValues.length > 0 || selectedCrossFilters.length > 0;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import {
|
|||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
createContext,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
|
@ -129,7 +129,6 @@ const publishDataMask = debounce(
|
|||
SLOW_DEBOUNCE,
|
||||
);
|
||||
|
||||
export const FilterBarScrollContext = createContext(false);
|
||||
const FilterBar: FC<FiltersBarProps> = ({
|
||||
orientation = FilterBarOrientation.Vertical,
|
||||
verticalConfig,
|
||||
|
|
@ -144,8 +143,11 @@ const FilterBar: FC<FiltersBarProps> = ({
|
|||
const tabId = useTabId();
|
||||
const filters = useFilters();
|
||||
const previousFilters = usePrevious(filters);
|
||||
const filterValues = Object.values(filters);
|
||||
const nativeFilterValues = filterValues.filter(isNativeFilter);
|
||||
const filterValues = useMemo(() => Object.values(filters), [filters]);
|
||||
const nativeFilterValues = useMemo(
|
||||
() => filterValues.filter(isNativeFilter),
|
||||
[filterValues],
|
||||
);
|
||||
const dashboardId = useSelector<any, number>(
|
||||
({ dashboardInfo }) => dashboardInfo?.id,
|
||||
);
|
||||
|
|
@ -212,14 +214,9 @@ const FilterBar: FC<FiltersBarProps> = ({
|
|||
|
||||
if (!isEmpty(updates)) {
|
||||
setDataMaskSelected(draft => ({ ...draft, ...updates }));
|
||||
Object.keys(updates).forEach(key => dispatch(clearDataMask(key)));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(previousFilters),
|
||||
previousDashboardId,
|
||||
]);
|
||||
}, [dashboardId, filters, previousDashboardId, setDataMaskSelected]);
|
||||
|
||||
const dataMaskAppliedText = JSON.stringify(dataMaskApplied);
|
||||
|
||||
|
|
@ -276,16 +273,27 @@ const FilterBar: FC<FiltersBarProps> = ({
|
|||
);
|
||||
const isInitialized = useInitialization();
|
||||
|
||||
const actions = (
|
||||
<ActionButtons
|
||||
filterBarOrientation={orientation}
|
||||
width={verticalConfig?.width}
|
||||
onApply={handleApply}
|
||||
onClearAll={handleClearAll}
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
dataMaskApplied={dataMaskApplied}
|
||||
isApplyDisabled={isApplyDisabled}
|
||||
/>
|
||||
const actions = useMemo(
|
||||
() => (
|
||||
<ActionButtons
|
||||
filterBarOrientation={orientation}
|
||||
width={verticalConfig?.width}
|
||||
onApply={handleApply}
|
||||
onClearAll={handleClearAll}
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
dataMaskApplied={dataMaskApplied}
|
||||
isApplyDisabled={isApplyDisabled}
|
||||
/>
|
||||
),
|
||||
[
|
||||
orientation,
|
||||
verticalConfig?.width,
|
||||
handleApply,
|
||||
handleClearAll,
|
||||
dataMaskSelected,
|
||||
dataMaskAppliedText,
|
||||
isApplyDisabled,
|
||||
],
|
||||
);
|
||||
|
||||
const filterBarComponent =
|
||||
|
|
|
|||
|
|
@ -18,17 +18,26 @@
|
|||
*/
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import getChartAndLabelComponentIdFromPath from 'src/dashboard/util/getChartAndLabelComponentIdFromPath';
|
||||
|
||||
const filterOutlinedSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.dashboardState.directPathToChild,
|
||||
(state: RootState) => state.dashboardState.directPathLastUpdated,
|
||||
],
|
||||
(directPathToChild, directPathLastUpdated) => ({
|
||||
outlinedFilterId: (
|
||||
getChartAndLabelComponentIdFromPath(directPathToChild || []) as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)?.native_filter,
|
||||
lastUpdated: directPathLastUpdated,
|
||||
}),
|
||||
);
|
||||
export const useFilterOutlined = () =>
|
||||
useSelector<RootState, { outlinedFilterId: string; lastUpdated: number }>(
|
||||
state => ({
|
||||
outlinedFilterId: (
|
||||
getChartAndLabelComponentIdFromPath(
|
||||
state.dashboardState.directPathToChild || [],
|
||||
) as Record<string, string>
|
||||
)?.native_filter,
|
||||
lastUpdated: state.dashboardState.directPathLastUpdated,
|
||||
}),
|
||||
filterOutlinedSelector,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { DataMaskStateWithId, Filter, FilterState } from '@superset-ui/core';
|
|||
import { testWithId } from 'src/utils/testUtils';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
export const getOnlyExtraFormData = (data: DataMaskStateWithId) =>
|
||||
Object.values(data).reduce(
|
||||
|
|
@ -64,20 +65,26 @@ export const checkIsApplyDisabled = (
|
|||
);
|
||||
};
|
||||
|
||||
const chartsVerboseMapSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.sliceEntities.slices,
|
||||
(state: RootState) => state.datasources,
|
||||
],
|
||||
(slices, datasources) =>
|
||||
Object.keys(slices).reduce((chartsVerboseMaps, chartId) => {
|
||||
const chartDatasource = slices[chartId]?.datasource
|
||||
? datasources[slices[chartId].datasource]
|
||||
: undefined;
|
||||
return {
|
||||
...chartsVerboseMaps,
|
||||
[chartId]: chartDatasource ? chartDatasource.verbose_map : {},
|
||||
};
|
||||
}, {}),
|
||||
);
|
||||
|
||||
export const useChartsVerboseMaps = () =>
|
||||
useSelector<RootState, { [chartId: string]: Record<string, string> }>(
|
||||
state => {
|
||||
const { charts, datasources } = state;
|
||||
|
||||
return Object.keys(state.charts).reduce((chartsVerboseMaps, chartId) => {
|
||||
const chartDatasource =
|
||||
datasources[charts[chartId]?.form_data?.datasource];
|
||||
return {
|
||||
...chartsVerboseMaps,
|
||||
[chartId]: chartDatasource ? chartDatasource.verbose_map : {},
|
||||
};
|
||||
}, {});
|
||||
},
|
||||
chartsVerboseMapSelector,
|
||||
);
|
||||
|
||||
export const FILTER_BAR_TEST_ID = 'filter-bar';
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ const baseInitialState = {
|
|||
id: 3,
|
||||
},
|
||||
},
|
||||
dashboardState: {
|
||||
sliceIds: [1, 2, 3],
|
||||
},
|
||||
dashboardLayout: {
|
||||
past: [],
|
||||
future: [],
|
||||
|
|
|
|||
|
|
@ -32,11 +32,7 @@ import {
|
|||
} from '@superset-ui/core';
|
||||
import { TIME_FILTER_MAP } from 'src/explore/constants';
|
||||
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import {
|
||||
ChartConfiguration,
|
||||
DashboardLayout,
|
||||
Layout,
|
||||
} from 'src/dashboard/types';
|
||||
import { ChartConfiguration, LayoutItem } from 'src/dashboard/types';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
|
||||
export enum IndicatorStatus {
|
||||
|
|
@ -170,7 +166,7 @@ export type CrossFilterIndicator = Indicator & { emitterId: number };
|
|||
export const getCrossFilterIndicator = (
|
||||
chartId: number,
|
||||
dataMask: DataMask,
|
||||
dashboardLayout: DashboardLayout,
|
||||
chartLayoutItems: LayoutItem[],
|
||||
) => {
|
||||
const filterState = dataMask?.filterState;
|
||||
const filters = dataMask?.extraFormData?.filters;
|
||||
|
|
@ -179,19 +175,17 @@ export const getCrossFilterIndicator = (
|
|||
const column =
|
||||
filters?.[0]?.col || (filtersState && Object.keys(filtersState)[0]);
|
||||
|
||||
const dashboardLayoutItem = Object.values(dashboardLayout).find(
|
||||
const chartLayoutItem = chartLayoutItems.find(
|
||||
layoutItem => layoutItem?.meta?.chartId === chartId,
|
||||
);
|
||||
|
||||
const filterObject: Indicator = {
|
||||
column,
|
||||
name:
|
||||
dashboardLayoutItem?.meta?.sliceNameOverride ||
|
||||
dashboardLayoutItem?.meta?.sliceName ||
|
||||
chartLayoutItem?.meta?.sliceNameOverride ||
|
||||
chartLayoutItem?.meta?.sliceName ||
|
||||
'',
|
||||
path: [
|
||||
...(dashboardLayoutItem?.parents ?? []),
|
||||
dashboardLayoutItem?.id || '',
|
||||
],
|
||||
path: [...(chartLayoutItem?.parents ?? []), chartLayoutItem?.id || ''],
|
||||
value: label,
|
||||
};
|
||||
return filterObject;
|
||||
|
|
@ -288,7 +282,7 @@ const defaultChartConfig = {};
|
|||
export const selectChartCrossFilters = (
|
||||
dataMask: DataMaskStateWithId,
|
||||
chartId: number,
|
||||
dashboardLayout: Layout,
|
||||
chartLayoutItems: LayoutItem[],
|
||||
chartConfiguration: ChartConfiguration = defaultChartConfig,
|
||||
appliedColumns: Set<string>,
|
||||
rejectedColumns: Set<string>,
|
||||
|
|
@ -312,7 +306,7 @@ export const selectChartCrossFilters = (
|
|||
const filterIndicator = getCrossFilterIndicator(
|
||||
Number(chartConfig.id),
|
||||
dataMask[chartConfig.id],
|
||||
dashboardLayout,
|
||||
chartLayoutItems,
|
||||
);
|
||||
const filterStatus = getStatus({
|
||||
label: filterIndicator.value,
|
||||
|
|
@ -339,7 +333,7 @@ export const selectNativeIndicatorsForChart = (
|
|||
dataMask: DataMaskStateWithId,
|
||||
chartId: number,
|
||||
chart: any,
|
||||
dashboardLayout: Layout,
|
||||
chartLayoutItems: LayoutItem[],
|
||||
chartConfiguration: ChartConfiguration = defaultChartConfig,
|
||||
): Indicator[] => {
|
||||
const appliedColumns = getAppliedColumns(chart);
|
||||
|
|
@ -351,7 +345,7 @@ export const selectNativeIndicatorsForChart = (
|
|||
areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
|
||||
areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
|
||||
cachedFilterData?.nativeFilters === nativeFilters &&
|
||||
cachedFilterData?.dashboardLayout === dashboardLayout &&
|
||||
cachedFilterData?.chartLayoutItems === chartLayoutItems &&
|
||||
cachedFilterData?.chartConfiguration === chartConfiguration &&
|
||||
cachedFilterData?.dataMask === dataMask
|
||||
) {
|
||||
|
|
@ -389,7 +383,7 @@ export const selectNativeIndicatorsForChart = (
|
|||
crossFilterIndicators = selectChartCrossFilters(
|
||||
dataMask,
|
||||
chartId,
|
||||
dashboardLayout,
|
||||
chartLayoutItems,
|
||||
chartConfiguration,
|
||||
appliedColumns,
|
||||
rejectedColumns,
|
||||
|
|
@ -399,7 +393,7 @@ export const selectNativeIndicatorsForChart = (
|
|||
cachedNativeIndicatorsForChart[chartId] = indicators;
|
||||
cachedNativeFilterDataForChart[chartId] = {
|
||||
nativeFilters,
|
||||
dashboardLayout,
|
||||
chartLayoutItems,
|
||||
chartConfiguration,
|
||||
dataMask,
|
||||
appliedColumns,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Filter,
|
||||
FilterConfiguration,
|
||||
|
|
@ -25,7 +25,7 @@ import {
|
|||
isFilterDivider,
|
||||
} from '@superset-ui/core';
|
||||
import { ActiveTabs, DashboardLayout, RootState } from '../../types';
|
||||
import { TAB_TYPE } from '../../util/componentTypes';
|
||||
import { CHART_TYPE, TAB_TYPE } from '../../util/componentTypes';
|
||||
|
||||
const defaultFilterConfiguration: Filter[] = [];
|
||||
|
||||
|
|
@ -79,34 +79,45 @@ function useActiveDashboardTabs() {
|
|||
|
||||
function useSelectChartTabParents() {
|
||||
const dashboardLayout = useDashboardLayout();
|
||||
return (chartId: number) => {
|
||||
const chartLayoutItem = Object.values(dashboardLayout).find(
|
||||
layoutItem => layoutItem.meta?.chartId === chartId,
|
||||
);
|
||||
return chartLayoutItem?.parents?.filter(
|
||||
(parent: string) => dashboardLayout[parent]?.type === TAB_TYPE,
|
||||
);
|
||||
};
|
||||
const layoutChartItems = useMemo(
|
||||
() =>
|
||||
Object.values(dashboardLayout).filter(item => item.type === CHART_TYPE),
|
||||
[dashboardLayout],
|
||||
);
|
||||
return useCallback(
|
||||
(chartId: number) => {
|
||||
const chartLayoutItem = layoutChartItems.find(
|
||||
layoutItem => layoutItem.meta?.chartId === chartId,
|
||||
);
|
||||
return chartLayoutItem?.parents?.filter(
|
||||
(parent: string) => dashboardLayout[parent]?.type === TAB_TYPE,
|
||||
);
|
||||
},
|
||||
[dashboardLayout, layoutChartItems],
|
||||
);
|
||||
}
|
||||
|
||||
export function useIsFilterInScope() {
|
||||
const activeTabs = useActiveDashboardTabs();
|
||||
const selectChartTabParents = useSelectChartTabParents();
|
||||
|
||||
// Filter is in scope if any of it's charts is visible.
|
||||
// Filter is in scope if any of its charts is visible.
|
||||
// Chart is visible if it's placed in an active tab tree or if it's not attached to any tab.
|
||||
// Chart is in an active tab tree if all of it's ancestors of type TAB are active
|
||||
// Chart is in an active tab tree if all of its ancestors of type TAB are active
|
||||
// Dividers are always in scope
|
||||
return (filter: Filter | Divider) =>
|
||||
isFilterDivider(filter) ||
|
||||
('chartsInScope' in filter &&
|
||||
filter.chartsInScope?.some((chartId: number) => {
|
||||
const tabParents = selectChartTabParents(chartId);
|
||||
return (
|
||||
tabParents?.length === 0 ||
|
||||
tabParents?.every(tab => activeTabs.includes(tab))
|
||||
);
|
||||
}));
|
||||
return useCallback(
|
||||
(filter: Filter | Divider) =>
|
||||
isFilterDivider(filter) ||
|
||||
('chartsInScope' in filter &&
|
||||
filter.chartsInScope?.some((chartId: number) => {
|
||||
const tabParents = selectChartTabParents(chartId);
|
||||
return (
|
||||
tabParents?.length === 0 ||
|
||||
tabParents?.every(tab => activeTabs.includes(tab))
|
||||
);
|
||||
})),
|
||||
[selectChartTabParents, activeTabs],
|
||||
);
|
||||
}
|
||||
|
||||
export function useSelectFiltersInScope(filters: (Filter | Divider)[]) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import { Behavior, FeatureFlag } from '@superset-ui/core';
|
||||
import * as uiCore from '@superset-ui/core';
|
||||
import { DashboardLayout } from 'src/dashboard/types';
|
||||
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { nativeFilterGate, findTabsWithChartsInScope } from './utils';
|
||||
|
||||
let isFeatureEnabledMock: jest.MockInstance<boolean, [feature: FeatureFlag]>;
|
||||
|
|
@ -119,7 +120,10 @@ test('findTabsWithChartsInScope should handle a recursive layout structure', ()
|
|||
},
|
||||
} as any as DashboardLayout;
|
||||
|
||||
expect(Array.from(findTabsWithChartsInScope(dashboardLayout, []))).toEqual(
|
||||
const chartLayoutItems = Object.values(dashboardLayout).filter(
|
||||
item => item.type === CHART_TYPE,
|
||||
);
|
||||
expect(Array.from(findTabsWithChartsInScope(chartLayoutItems, []))).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,10 +30,9 @@ import {
|
|||
QueryFormData,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { DashboardLayout } from 'src/dashboard/types';
|
||||
import { LayoutItem } from 'src/dashboard/types';
|
||||
import extractUrlParams from 'src/dashboard/util/extractUrlParams';
|
||||
import { CHART_TYPE, TAB_TYPE } from '../../util/componentTypes';
|
||||
import { DASHBOARD_GRID_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
|
||||
import { TAB_TYPE } from '../../util/componentTypes';
|
||||
import getBootstrapData from '../../../utils/getBootstrapData';
|
||||
|
||||
const getDefaultRowLimit = (): number => {
|
||||
|
|
@ -156,84 +155,20 @@ export function nativeFilterGate(behaviors: Behavior[]): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
const isComponentATab = (
|
||||
dashboardLayout: DashboardLayout,
|
||||
componentId: string,
|
||||
) => dashboardLayout?.[componentId]?.type === TAB_TYPE;
|
||||
|
||||
const findTabsWithChartsInScopeHelper = (
|
||||
dashboardLayout: DashboardLayout,
|
||||
chartsInScope: number[],
|
||||
componentId: string,
|
||||
tabIds: string[],
|
||||
tabsToHighlight: Set<string>,
|
||||
visited: Set<string>,
|
||||
) => {
|
||||
if (visited.has(componentId)) {
|
||||
return;
|
||||
}
|
||||
visited.add(componentId);
|
||||
if (
|
||||
dashboardLayout?.[componentId]?.type === CHART_TYPE &&
|
||||
chartsInScope.includes(dashboardLayout[componentId]?.meta?.chartId)
|
||||
) {
|
||||
tabIds.forEach(tabsToHighlight.add, tabsToHighlight);
|
||||
}
|
||||
if (
|
||||
dashboardLayout?.[componentId]?.children?.length === 0 ||
|
||||
(isComponentATab(dashboardLayout, componentId) &&
|
||||
tabsToHighlight.has(componentId))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
dashboardLayout[componentId]?.children.forEach(childId =>
|
||||
findTabsWithChartsInScopeHelper(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
childId,
|
||||
isComponentATab(dashboardLayout, childId) ? [...tabIds, childId] : tabIds,
|
||||
tabsToHighlight,
|
||||
visited,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const findTabsWithChartsInScope = (
|
||||
dashboardLayout: DashboardLayout,
|
||||
chartLayoutItems: LayoutItem[],
|
||||
chartsInScope: number[],
|
||||
) => {
|
||||
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
|
||||
const rootChildId = dashboardRoot.children[0];
|
||||
const hasTopLevelTabs = rootChildId !== DASHBOARD_GRID_ID;
|
||||
const tabsInScope = new Set<string>();
|
||||
const visited = new Set<string>();
|
||||
if (hasTopLevelTabs) {
|
||||
dashboardLayout[rootChildId]?.children?.forEach(tabId =>
|
||||
findTabsWithChartsInScopeHelper(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
tabId,
|
||||
[tabId],
|
||||
tabsInScope,
|
||||
visited,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Object.values(dashboardLayout)
|
||||
.filter(element => element?.type === TAB_TYPE)
|
||||
.forEach(element =>
|
||||
findTabsWithChartsInScopeHelper(
|
||||
dashboardLayout,
|
||||
chartsInScope,
|
||||
element.id,
|
||||
[element.id],
|
||||
tabsInScope,
|
||||
visited,
|
||||
),
|
||||
);
|
||||
}
|
||||
return tabsInScope;
|
||||
};
|
||||
) =>
|
||||
new Set<string>(
|
||||
chartsInScope
|
||||
.map(chartId =>
|
||||
chartLayoutItems
|
||||
.find(item => item?.meta?.chartId === chartId)
|
||||
?.parents?.filter(parent => parent.startsWith(`${TAB_TYPE}-`)),
|
||||
)
|
||||
.filter(id => id !== undefined)
|
||||
.flat() as string[],
|
||||
);
|
||||
|
||||
export const getFilterValueForDisplay = (
|
||||
value?: string[] | null | string | number | object,
|
||||
|
|
|
|||
|
|
@ -17,20 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { useMemoCompare } from 'src/hooks/useMemoCompare';
|
||||
|
||||
const chartIdsSelector = createSelector(
|
||||
(state: RootState) => state.charts,
|
||||
charts => Object.values(charts).map(chart => chart.id),
|
||||
);
|
||||
|
||||
export const useChartIds = () => {
|
||||
const chartIds = useSelector<RootState, number[]>(chartIdsSelector);
|
||||
return useMemoCompare(
|
||||
chartIds,
|
||||
(prev, next) => prev === next || isEqual(prev, next),
|
||||
);
|
||||
};
|
||||
export const useChartIds = () =>
|
||||
useSelector<RootState, number[]>(state => state.dashboardState.sliceIds);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
isCrossFilterScopeGlobal,
|
||||
} from '../types';
|
||||
import { DEFAULT_CROSS_FILTER_SCOPING } from '../constants';
|
||||
import { CHART_TYPE } from './componentTypes';
|
||||
|
||||
export const isCrossFiltersEnabled = (
|
||||
metadataCrossFiltersEnabled: boolean | undefined,
|
||||
|
|
@ -52,13 +53,17 @@ export const getCrossFiltersConfiguration = (
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const chartLayoutItems = Object.values(dashboardLayout).filter(
|
||||
item => item?.type === CHART_TYPE,
|
||||
);
|
||||
|
||||
const globalChartConfiguration = metadata.global_chart_configuration?.scope
|
||||
? {
|
||||
scope: metadata.global_chart_configuration.scope,
|
||||
chartsInScope: getChartIdsInFilterScope(
|
||||
metadata.global_chart_configuration.scope,
|
||||
Object.values(charts).map(chart => chart.id),
|
||||
dashboardLayout,
|
||||
chartLayoutItems,
|
||||
),
|
||||
}
|
||||
: {
|
||||
|
|
@ -69,7 +74,7 @@ export const getCrossFiltersConfiguration = (
|
|||
// If user just added cross filter to dashboard it's not saving its scope on server,
|
||||
// so we tweak it until user will update scope and will save it in server
|
||||
const chartConfiguration = {};
|
||||
Object.values(dashboardLayout).forEach(layoutItem => {
|
||||
chartLayoutItems.forEach(layoutItem => {
|
||||
const chartId = layoutItem.meta?.chartId;
|
||||
|
||||
if (!isDefined(chartId)) {
|
||||
|
|
@ -105,7 +110,7 @@ export const getCrossFiltersConfiguration = (
|
|||
: getChartIdsInFilterScope(
|
||||
chartConfiguration[chartId].crossFilters.scope,
|
||||
Object.values(charts).map(chart => chart.id),
|
||||
dashboardLayout,
|
||||
chartLayoutItems,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,14 +18,13 @@
|
|||
*/
|
||||
import { NativeFilterScope } from '@superset-ui/core';
|
||||
import { CHART_TYPE } from './componentTypes';
|
||||
import { Layout } from '../types';
|
||||
import { LayoutItem } from '../types';
|
||||
|
||||
export function getChartIdsInFilterScope(
|
||||
filterScope: NativeFilterScope,
|
||||
chartIds: number[],
|
||||
layout: Layout,
|
||||
layoutItems: LayoutItem[],
|
||||
) {
|
||||
const layoutItems = Object.values(layout);
|
||||
return chartIds.filter(
|
||||
chartId =>
|
||||
!filterScope.excluded.includes(chartId) &&
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* 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 { createSelector } from '@reduxjs/toolkit';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../types';
|
||||
import { CHART_TYPE } from './componentTypes';
|
||||
|
||||
const chartLayoutItemsSelector = createSelector(
|
||||
(state: RootState) => state.dashboardLayout.present,
|
||||
layout => Object.values(layout).filter(item => item?.type === CHART_TYPE),
|
||||
);
|
||||
|
||||
export const useChartLayoutItems = () => useSelector(chartLayoutItemsSelector);
|
||||
|
|
@ -141,54 +141,4 @@ describe('useFilterFocusHighlightStyles', () => {
|
|||
const styles = getComputedStyle(container);
|
||||
expect(parseFloat(styles.opacity)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return unfocused styles if focusedFilterField is targeting a different chart', async () => {
|
||||
const chartId = 18;
|
||||
mockGetRelatedCharts.mockReturnValue([]);
|
||||
const store = createMockStore({
|
||||
dashboardState: {
|
||||
focusedFilterField: {
|
||||
chartId: 10,
|
||||
column: 'test',
|
||||
},
|
||||
},
|
||||
dashboardFilters: {
|
||||
10: {
|
||||
scopes: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWrapper(chartId, store);
|
||||
|
||||
const container = screen.getByTestId('test-component');
|
||||
|
||||
const styles = getComputedStyle(container);
|
||||
expect(parseFloat(styles.opacity)).toBe(0.3);
|
||||
});
|
||||
|
||||
it('should return focused styles if focusedFilterField chart equals our own', async () => {
|
||||
const chartId = 18;
|
||||
mockGetRelatedCharts.mockReturnValue([chartId]);
|
||||
const store = createMockStore({
|
||||
dashboardState: {
|
||||
focusedFilterField: {
|
||||
chartId,
|
||||
column: 'test',
|
||||
},
|
||||
},
|
||||
dashboardFilters: {
|
||||
[chartId]: {
|
||||
scopes: {
|
||||
otherColumn: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWrapper(chartId, store);
|
||||
|
||||
const container = screen.getByTestId('test-component');
|
||||
|
||||
const styles = getComputedStyle(container);
|
||||
expect(parseFloat(styles.opacity)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,40 +16,30 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { Filter, useTheme } from '@superset-ui/core';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { DashboardState, RootState } from 'src/dashboard/types';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { getRelatedCharts } from './getRelatedCharts';
|
||||
|
||||
const selectFocusedFilterScope = (
|
||||
dashboardState: DashboardState,
|
||||
dashboardFilters: any,
|
||||
) => {
|
||||
if (!dashboardState.focusedFilterField) return null;
|
||||
const { chartId, column } = dashboardState.focusedFilterField;
|
||||
return {
|
||||
chartId,
|
||||
scope: dashboardFilters[chartId].scopes[column],
|
||||
};
|
||||
};
|
||||
const unfocusedChartStyles = { opacity: 0.3, pointerEvents: 'none' };
|
||||
const EMPTY = {};
|
||||
|
||||
const useFilterFocusHighlightStyles = (chartId: number) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const nativeFilters = useSelector((state: RootState) => state.nativeFilters);
|
||||
const dashboardState = useSelector(
|
||||
(state: RootState) => state.dashboardState,
|
||||
const focusedChartStyles = useMemo(
|
||||
() => ({
|
||||
borderColor: theme.colors.primary.light2,
|
||||
opacity: 1,
|
||||
boxShadow: `0px 0px ${theme.gridUnit * 2}px ${theme.colors.primary.base}`,
|
||||
pointerEvents: 'auto',
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
|
||||
const dashboardFilters = useSelector(
|
||||
(state: RootState) => state.dashboardFilters,
|
||||
);
|
||||
const focusedFilterScope = selectFocusedFilterScope(
|
||||
dashboardState,
|
||||
dashboardFilters,
|
||||
);
|
||||
const nativeFilters = useSelector((state: RootState) => state.nativeFilters);
|
||||
|
||||
const slices =
|
||||
useSelector((state: RootState) => state.sliceEntities.slices) || {};
|
||||
|
|
@ -57,8 +47,8 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
|
|||
const highlightedFilterId =
|
||||
nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
|
||||
|
||||
if (!(focusedFilterScope || highlightedFilterId)) {
|
||||
return {};
|
||||
if (!highlightedFilterId) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const relatedCharts = getRelatedCharts(
|
||||
|
|
@ -67,29 +57,7 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
|
|||
slices,
|
||||
);
|
||||
|
||||
// we use local styles here instead of a conditionally-applied class,
|
||||
// because adding any conditional class to this container
|
||||
// causes performance issues in Chrome.
|
||||
|
||||
// default to the "de-emphasized" state
|
||||
const unfocusedChartStyles = { opacity: 0.3, pointerEvents: 'none' };
|
||||
const focusedChartStyles = {
|
||||
borderColor: theme.colors.primary.light2,
|
||||
opacity: 1,
|
||||
boxShadow: `0px 0px ${theme.gridUnit * 2}px ${theme.colors.primary.base}`,
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
|
||||
if (highlightedFilterId) {
|
||||
if (relatedCharts.includes(chartId)) {
|
||||
return focusedChartStyles;
|
||||
}
|
||||
} else if (
|
||||
chartId === focusedFilterScope?.chartId ||
|
||||
getChartIdsInFilterScope({
|
||||
filterScope: focusedFilterScope?.scope,
|
||||
}).includes(chartId)
|
||||
) {
|
||||
if (highlightedFilterId && relatedCharts.includes(chartId)) {
|
||||
return focusedChartStyles;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue