perf(native-filters): Decrease number of unnecessary rerenders in native filters (#17115)

* perf(native-filters): decrease number of redundant rerenders

* More perf improvements

* lint fix
This commit is contained in:
Kamil Gabryjelski 2021-10-18 14:56:52 +02:00 committed by GitHub
parent 57f869cf22
commit 2ad9101d1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 321 additions and 232 deletions

View File

@ -45,9 +45,9 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
);
const nativeFilters =
useSelector<RootState, Filters>(state => state.nativeFilters?.filters) ??
{};
const nativeFilters = useSelector<RootState, Filters>(
state => state.nativeFilters?.filters,
);
const directPathToChild = useSelector<RootState, string[]>(
state => state.dashboardState.directPathToChild,
);
@ -68,7 +68,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
}, [getLeafComponentIdFromPath(directPathToChild)]);
// recalculate charts and tabs in scopes of native filters only when a scope or dashboard layout changes
const filterScopes = Object.values(nativeFilters).map(filter => ({
const filterScopes = Object.values(nativeFilters ?? {}).map(filter => ({
id: filter.id,
scope: filter.scope,
}));

View File

@ -18,7 +18,7 @@
*/
import { useSelector } from 'react-redux';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { RootState } from 'src/dashboard/types';
@ -64,9 +64,12 @@ export const useNativeFilters = () => {
)
);
const toggleDashboardFiltersOpen = (visible?: boolean) => {
setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen);
};
const toggleDashboardFiltersOpen = useCallback(
(visible?: boolean) => {
setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen);
},
[dashboardFiltersOpen],
);
useEffect(() => {
if (

View File

@ -225,5 +225,4 @@ const CascadePopover: React.FC<CascadePopoverProps> = ({
</Popover>
);
};
export default CascadePopover;
export default React.memo(CascadePopover);

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useMemo } from 'react';
import { styled } from '@superset-ui/core';
import { Form, FormItem } from 'src/components/Form';
import FilterValue from './FilterValue';
@ -67,17 +67,22 @@ const FilterControl: React.FC<FilterProps> = ({
filter.dataMask?.filterState,
);
const label = useMemo(
() => (
<StyledFilterControlTitleBox>
<StyledFilterControlTitle data-test="filter-control-name">
{name}
</StyledFilterControlTitle>
<StyledIcon data-test="filter-icon">{icon}</StyledIcon>
</StyledFilterControlTitleBox>
),
[icon, name],
);
return (
<StyledFilterControlContainer layout="vertical">
<FormItem
label={
<StyledFilterControlTitleBox>
<StyledFilterControlTitle data-test="filter-control-name">
{name}
</StyledFilterControlTitle>
<StyledIcon data-test="filter-icon">{icon}</StyledIcon>
</StyledFilterControlTitleBox>
}
label={label}
required={filter?.controlValues?.enableEmptyFilter}
validateStatus={isMissingRequiredValue ? 'error' : undefined}
>
@ -92,5 +97,4 @@ const FilterControl: React.FC<FilterProps> = ({
</StyledFilterControlContainer>
);
};
export default FilterControl;
export default React.memo(FilterControl);

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { FC, useMemo, useState } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import { DataMask, styled, t } from '@superset-ui/core';
import {
@ -55,8 +55,8 @@ const FilterControls: FC<FilterControlsProps> = ({
}) => {
const [visiblePopoverId, setVisiblePopoverId] = useState<string | null>(null);
const filters = useFilters();
const filterValues = Object.values<Filter>(filters);
const portalNodes = React.useMemo(() => {
const filterValues = useMemo(() => Object.values<Filter>(filters), [filters]);
const portalNodes = useMemo(() => {
const nodes = new Array(filterValues.length);
for (let i = 0; i < filterValues.length; i += 1) {
nodes[i] = createHtmlPortalNode();
@ -70,8 +70,7 @@ const FilterControls: FC<FilterControlsProps> = ({
dataMask: dataMaskSelected[filter.id],
}));
return buildCascadeFiltersTree(filtersWithValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(filterValues), dataMaskSelected]);
}, [filterValues, dataMaskSelected]);
const cascadeFilterIds = new Set(cascadeFilters.map(item => item.id));
const [filtersInScope, filtersOutOfScope] = useSelectFiltersInScope(
@ -80,26 +79,36 @@ const FilterControls: FC<FilterControlsProps> = ({
const dashboardHasTabs = useDashboardHasTabs();
const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0;
const cascadePopoverFactory = useCallback(
index => (
<CascadePopover
data-test="cascade-filters-control"
key={cascadeFilters[index].id}
dataMaskSelected={dataMaskSelected}
visible={visiblePopoverId === cascadeFilters[index].id}
onVisibleChange={visible =>
setVisiblePopoverId(visible ? cascadeFilters[index].id : null)
}
filter={cascadeFilters[index]}
onFilterSelectionChange={onFilterSelectionChange}
directPathToChild={directPathToChild}
inView={false}
/>
),
[
cascadeFilters,
JSON.stringify(dataMaskSelected),
directPathToChild,
onFilterSelectionChange,
visiblePopoverId,
],
);
return (
<Wrapper>
{portalNodes
.filter((node, index) => cascadeFilterIds.has(filterValues[index].id))
.map((node, index) => (
<InPortal node={node}>
<CascadePopover
data-test="cascade-filters-control"
key={cascadeFilters[index].id}
dataMaskSelected={dataMaskSelected}
visible={visiblePopoverId === cascadeFilters[index].id}
onVisibleChange={visible =>
setVisiblePopoverId(visible ? cascadeFilters[index].id : null)
}
filter={cascadeFilters[index]}
onFilterSelectionChange={onFilterSelectionChange}
directPathToChild={directPathToChild}
inView={false}
/>
</InPortal>
<InPortal node={node}>{cascadePopoverFactory(index)}</InPortal>
))}
{filtersInScope.map(filter => {
const index = filterValues.findIndex(f => f.id === filter.id);
@ -150,4 +159,4 @@ const FilterControls: FC<FilterControlsProps> = ({
);
};
export default FilterControls;
export default React.memo(FilterControls);

View File

@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
QueryFormData,
SuperChart,
@ -53,6 +59,9 @@ const StyledDiv = styled.div`
}
`;
const queriesDataPlaceholder = [{ data: [{}] }];
const behaviors = [Behavior.NATIVE_FILTER];
const FilterValue: React.FC<FilterProps> = ({
dataMaskSelected,
filter,
@ -193,11 +202,36 @@ const FilterValue: React.FC<FilterProps> = ({
return undefined;
}, [inputRef, directPathToChild, filter.id]);
const setDataMask = (dataMask: DataMask) =>
onFilterSelectionChange(filter, dataMask);
const setDataMask = useCallback(
(dataMask: DataMask) => onFilterSelectionChange(filter, dataMask),
[filter, onFilterSelectionChange],
);
const setFocusedFilter = () => dispatchFocusAction(dispatch, id);
const unsetFocusedFilter = () => dispatchFocusAction(dispatch);
const setFocusedFilter = useCallback(
() => dispatchFocusAction(dispatch, id),
[dispatch, id],
);
const unsetFocusedFilter = useCallback(() => dispatchFocusAction(dispatch), [
dispatch,
]);
const hooks = useMemo(
() => ({ setDataMask, setFocusedFilter, unsetFocusedFilter }),
[setDataMask, setFocusedFilter, unsetFocusedFilter],
);
const isMissingRequiredValue = checkIsMissingRequiredValue(
filter,
filter.dataMask?.filterState,
);
const filterState = useMemo(
() => ({
...filter.dataMask?.filterState,
validateStatus: isMissingRequiredValue && 'error',
}),
[filter.dataMask?.filterState, isMissingRequiredValue],
);
if (error) {
return (
@ -208,14 +242,6 @@ const FilterValue: React.FC<FilterProps> = ({
/>
);
}
const isMissingRequiredValue = checkIsMissingRequiredValue(
filter,
filter.dataMask?.filterState,
);
const filterState = {
...filter.dataMask?.filterState,
validateStatus: isMissingRequiredValue && 'error',
};
return (
<StyledDiv data-test="form-item-value">
@ -227,18 +253,17 @@ const FilterValue: React.FC<FilterProps> = ({
width="100%"
formData={formData}
// For charts that don't have datasource we need workaround for empty placeholder
queriesData={hasDataSource ? state : [{ data: [{}] }]}
queriesData={hasDataSource ? state : queriesDataPlaceholder}
chartType={filterType}
behaviors={[Behavior.NATIVE_FILTER]}
behaviors={behaviors}
filterState={filterState}
ownState={filter.dataMask?.ownState}
enableNoResults={metadata?.enableNoResults}
isRefreshing={isRefreshing}
hooks={{ setDataMask, setFocusedFilter, unsetFocusedFilter }}
hooks={hooks}
/>
)}
</StyledDiv>
);
};
export default FilterValue;

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { NativeFiltersState } from 'src/dashboard/reducers/types';
import { DataMaskStateWithId } from 'src/dataMask/types';
@ -31,14 +32,16 @@ export function useCascadingFilters(
state => state.nativeFilters,
);
const filter = filters[id];
const cascadeParentIds: string[] = filter?.cascadeParentIds ?? [];
let cascadedFilters = {};
cascadeParentIds.forEach(parentId => {
const parentState = dataMaskSelected?.[parentId];
cascadedFilters = mergeExtraFormData(
cascadedFilters,
parentState?.extraFormData,
);
});
return cascadedFilters;
return useMemo(() => {
const cascadeParentIds: string[] = filter?.cascadeParentIds ?? [];
let cascadedFilters = {};
cascadeParentIds.forEach(parentId => {
const parentState = dataMaskSelected?.[parentId];
cascadedFilters = mergeExtraFormData(
cascadedFilters,
parentState?.extraFormData,
);
});
return cascadedFilters;
}, [dataMaskSelected, filter?.cascadeParentIds]);
}

View File

@ -19,7 +19,7 @@
/* eslint-disable no-param-reassign */
import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core';
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import cx from 'classnames';
import Icons from 'src/components/Icons';
@ -162,27 +162,30 @@ const FilterBar: React.FC<FiltersBarProps> = ({
const previousFilters = usePrevious(filters);
const filterValues = Object.values<Filter>(filters);
const handleFilterSelectionChange = (
filter: Pick<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>,
) => {
setDataMaskSelected(draft => {
// force instant updating on initialization for filters with `requiredFirst` is true or instant filters
if (
// filterState.value === undefined - means that value not initialized
dataMask.filterState?.value !== undefined &&
dataMaskSelected[filter.id]?.filterState?.value === undefined &&
filter.requiredFirst
) {
dispatch(updateDataMask(filter.id, dataMask));
}
const handleFilterSelectionChange = useCallback(
(
filter: Pick<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>,
) => {
setDataMaskSelected(draft => {
// force instant updating on initialization for filters with `requiredFirst` is true or instant filters
if (
// filterState.value === undefined - means that value not initialized
dataMask.filterState?.value !== undefined &&
dataMaskSelected[filter.id]?.filterState?.value === undefined &&
filter.requiredFirst
) {
dispatch(updateDataMask(filter.id, dataMask));
}
draft[filter.id] = {
...(getInitialDataMask(filter.id) as DataMaskWithId),
...dataMask,
};
});
};
draft[filter.id] = {
...(getInitialDataMask(filter.id) as DataMaskWithId),
...dataMask,
};
});
},
[dataMaskSelected, dispatch, setDataMaskSelected, tab],
);
const publishDataMask = useCallback(
(dataMaskSelected: DataMaskStateWithId) => {
@ -246,23 +249,27 @@ const FilterBar: React.FC<FiltersBarProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataMaskAppliedText, publishDataMask]);
const handleApply = () => {
const handleApply = useCallback(() => {
const filterIds = Object.keys(dataMaskSelected);
filterIds.forEach(filterId => {
if (dataMaskSelected[filterId]) {
dispatch(updateDataMask(filterId, dataMaskSelected[filterId]));
}
});
};
}, [dataMaskSelected, dispatch]);
const handleClearAll = () => {
const handleClearAll = useCallback(() => {
const filterIds = Object.keys(dataMaskSelected);
filterIds.forEach(filterId => {
if (dataMaskSelected[filterId]) {
dispatch(clearDataMask(filterId));
}
});
};
}, [dataMaskSelected, dispatch]);
const openFiltersBar = useCallback(() => toggleFiltersBar(true), [
toggleFiltersBar,
]);
useFilterUpdates(dataMaskSelected, setDataMaskSelected);
const isApplyDisabled = checkIsApplyDisabled(
@ -272,6 +279,8 @@ const FilterBar: React.FC<FiltersBarProps> = ({
);
const isInitialized = useInitialization();
const tabPaneStyle = useMemo(() => ({ overflow: 'auto', height }), [height]);
return (
<BarWrapper
{...getFilterBarTestId()}
@ -281,7 +290,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
<CollapsedBar
{...getFilterBarTestId('collapsable')}
className={cx({ open: !filtersOpen })}
onClick={() => toggleFiltersBar(true)}
onClick={openFiltersBar}
offset={offset}
>
<StyledCollapseIcon
@ -313,7 +322,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
<Tabs.TabPane
tab={t(`All Filters (${filterValues.length})`)}
key={TabIds.AllFilters}
css={{ overflow: 'auto', height }}
css={tabPaneStyle}
>
{editFilterSetId && (
<EditSection
@ -333,7 +342,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
disabled={!!editFilterSetId}
tab={t(`Filter Sets (${filterSetFilterValues.length})`)}
key={TabIds.FilterSets}
css={{ overflow: 'auto', height }}
css={tabPaneStyle}
>
<FilterSets
onEditFilterSet={setEditFilterSetId}
@ -345,7 +354,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
</Tabs.TabPane>
</StyledTabs>
) : (
<div css={{ overflow: 'auto', height }}>
<div css={tabPaneStyle}>
<FilterControls
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
@ -357,5 +366,4 @@ const FilterBar: React.FC<FiltersBarProps> = ({
</BarWrapper>
);
};
export default FilterBar;
export default React.memo(FilterBar);

View File

@ -27,7 +27,7 @@ import {
DataMaskStateWithId,
DataMaskWithId,
} from 'src/dataMask/types';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { ChartsState, RootState } from 'src/dashboard/types';
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
import { Filter } from '../types';
@ -37,35 +37,46 @@ export const useFilterSets = () =>
state => state.nativeFilters.filterSets || {},
);
export const useFilters = () =>
useSelector<any, Filters>(state => {
const preselectNativeFilters =
state.dashboardState?.preselectNativeFilters || {};
return Object.entries(state.nativeFilters.filters).reduce(
(acc, [filterId, filter]: [string, Filter]) => ({
...acc,
[filterId]: {
...filter,
preselect: preselectNativeFilters[filterId],
},
}),
{} as Filters,
);
});
export const useFilters = () => {
const preselectedNativeFilters = useSelector<any, Filters>(
state => state.dashboardState?.preselectNativeFilters,
);
const nativeFilters = useSelector<any, Filters>(
state => state.nativeFilters.filters,
);
return useMemo(
() =>
Object.entries(nativeFilters).reduce(
(acc, [filterId, filter]: [string, Filter]) => ({
...acc,
[filterId]: {
...filter,
preselect: preselectedNativeFilters?.[filterId],
},
}),
{} as Filters,
),
[nativeFilters, preselectedNativeFilters],
);
};
export const useNativeFiltersDataMask = () => {
const dataMask = useSelector<RootState, DataMaskStateWithId>(
state => state.dataMask,
);
return Object.values(dataMask)
.filter((item: DataMaskWithId) =>
String(item.id).startsWith(NATIVE_FILTER_PREFIX),
)
.reduce(
(prev, next: DataMaskWithId) => ({ ...prev, [next.id]: next }),
{},
) as DataMaskStateWithId;
return useMemo(
() =>
Object.values(dataMask)
.filter((item: DataMaskWithId) =>
String(item.id).startsWith(NATIVE_FILTER_PREFIX),
)
.reduce(
(prev, next: DataMaskWithId) => ({ ...prev, [next.id]: next }),
{},
) as DataMaskStateWithId,
[dataMask],
);
};
export const useFilterUpdates = (

View File

@ -22,10 +22,9 @@ import {
styled,
t,
} from '@superset-ui/core';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Slider } from 'src/common/components';
import { rgba } from 'emotion-rgba';
import { FormItemProps } from 'antd/lib/form';
import { PluginFilterRangeProps } from './types';
import { StatusMessage, StyledFormItem, FilterPluginStyle } from '../common';
import { getRangeExtraFormData } from '../../utils';
@ -74,6 +73,37 @@ const Wrapper = styled.div<{ validateStatus?: 'error' | 'warning' | 'info' }>`
`}
`;
const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER);
const tipFormatter = (value: number) => numberFormatter(value);
const getLabel = (lower: number | null, upper: number | null): string => {
if (lower !== null && upper !== null) {
return `${numberFormatter(lower)} ≤ x ≤ ${numberFormatter(upper)}`;
}
if (lower !== null) {
return `x ≥ ${numberFormatter(lower)}`;
}
if (upper !== null) {
return `x ≤ ${numberFormatter(upper)}`;
}
return '';
};
const getMarks = (
lower: number | null,
upper: number | null,
): { [key: number]: string } => {
const newMarks: { [key: number]: string } = {};
if (lower !== null) {
newMarks[lower] = numberFormatter(lower);
}
if (upper !== null) {
newMarks[upper] = numberFormatter(upper);
}
return newMarks;
};
export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
const {
data,
@ -85,8 +115,6 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
unsetFocusedFilter,
filterState,
} = props;
const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER);
const [row] = data;
// @ts-ignore
const { min, max }: { min: number; max: number } = row;
@ -97,60 +125,39 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
);
const [marks, setMarks] = useState<{ [key: number]: string }>({});
const getBounds = (
value: [number, number],
): { lower: number | null; upper: number | null } => {
const [lowerRaw, upperRaw] = value;
return {
lower: lowerRaw > min ? lowerRaw : null,
upper: upperRaw < max ? upperRaw : null,
};
};
const getBounds = useCallback(
(
value: [number, number],
): { lower: number | null; upper: number | null } => {
const [lowerRaw, upperRaw] = value;
return {
lower: lowerRaw > min ? lowerRaw : null,
upper: upperRaw < max ? upperRaw : null,
};
},
[max, min],
);
const getLabel = (lower: number | null, upper: number | null): string => {
if (lower !== null && upper !== null) {
return `${numberFormatter(lower)} ≤ x ≤ ${numberFormatter(upper)}`;
}
if (lower !== null) {
return `x ≥ ${numberFormatter(lower)}`;
}
if (upper !== null) {
return `x ≤ ${numberFormatter(upper)}`;
}
return '';
};
const handleAfterChange = useCallback(
(value: [number, number]): void => {
setValue(value);
const { lower, upper } = getBounds(value);
setMarks(getMarks(lower, upper));
const getMarks = (
lower: number | null,
upper: number | null,
): { [key: number]: string } => {
const newMarks: { [key: number]: string } = {};
if (lower !== null) {
newMarks[lower] = numberFormatter(lower);
}
if (upper !== null) {
newMarks[upper] = numberFormatter(upper);
}
return newMarks;
};
setDataMask({
extraFormData: getRangeExtraFormData(col, lower, upper),
filterState: {
value: lower !== null || upper !== null ? value : null,
label: getLabel(lower, upper),
},
});
},
[col, getBounds, setDataMask],
);
const handleAfterChange = (value: [number, number]): void => {
const handleChange = useCallback((value: [number, number]) => {
setValue(value);
const { lower, upper } = getBounds(value);
setMarks(getMarks(lower, upper));
setDataMask({
extraFormData: getRangeExtraFormData(col, lower, upper),
filterState: {
value: lower !== null || upper !== null ? value : null,
label: getLabel(lower, upper),
},
});
};
const handleChange = (value: [number, number]) => {
setValue(value);
};
}, []);
useEffect(() => {
// when switch filter type and queriesData still not updated we need ignore this case (in FilterBar)
@ -160,20 +167,25 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
handleAfterChange(filterState.value ?? [min, max]);
}, [JSON.stringify(filterState.value), JSON.stringify(data)]);
const formItemData: FormItemProps = {};
if (filterState.validateMessage) {
formItemData.extra = (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
const formItemExtra = useMemo(() => {
if (filterState.validateMessage) {
return (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
return undefined;
}, [filterState.validateMessage, filterState.validateStatus]);
const minMax = useMemo(() => value ?? [min, max], [max, min, value]);
return (
<FilterPluginStyle height={height} width={width}>
{Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
<h4>{t('Chosen non-numeric column')}</h4>
) : (
<StyledFormItem {...formItemData}>
<StyledFormItem extra={formItemExtra}>
<Wrapper
tabIndex={-1}
ref={inputRef}
@ -187,10 +199,10 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
range
min={min}
max={max}
value={value ?? [min, max]}
value={minMax}
onAfterChange={handleAfterChange}
onChange={handleChange}
tipFormatter={value => numberFormatter(value)}
tipFormatter={tipFormatter}
marks={marks}
/>
</Wrapper>

View File

@ -34,7 +34,6 @@ import { Select } from 'src/components';
import debounce from 'lodash/debounce';
import { SLOW_DEBOUNCE } from 'src/constants';
import { useImmerReducer } from 'use-immer';
import { FormItemProps } from 'antd/lib/form';
import { PluginFilterSelectProps, SelectValue } from './types';
import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common';
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
@ -174,13 +173,16 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
[],
);
const searchWrapper = (val: string) => {
if (searchAllOptions) {
debouncedOwnStateFunc(val);
}
};
const searchWrapper = useCallback(
(val: string) => {
if (searchAllOptions) {
debouncedOwnStateFunc(val);
}
},
[debouncedOwnStateFunc, searchAllOptions],
);
const clearSuggestionSearch = () => {
const clearSuggestionSearch = useCallback(() => {
if (searchAllOptions) {
dispatchDataMask({
type: 'ownState',
@ -190,21 +192,24 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
},
});
}
};
}, [dispatchDataMask, initialColtypeMap, searchAllOptions]);
const handleBlur = () => {
const handleBlur = useCallback(() => {
clearSuggestionSearch();
unsetFocusedFilter();
};
}, [clearSuggestionSearch, unsetFocusedFilter]);
const handleChange = (value?: SelectValue | number | string) => {
const values = ensureIsArray(value);
if (values.length === 0) {
updateDataMask(null);
} else {
updateDataMask(values);
}
};
const handleChange = useCallback(
(value?: SelectValue | number | string) => {
const values = ensureIsArray(value);
if (values.length === 0) {
updateDataMask(null);
} else {
updateDataMask(values);
}
},
[updateDataMask],
);
useEffect(() => {
if (defaultToFirstItem && filterState.value === undefined) {
@ -245,14 +250,16 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
? t('No data')
: tn('%s option', '%s options', data.length, data.length);
const formItemData: FormItemProps = {};
if (filterState.validateMessage) {
formItemData.extra = (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
const formItemExtra = useMemo(() => {
if (filterState.validateMessage) {
return (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
return undefined;
}, [filterState.validateMessage, filterState.validateStatus]);
const options = useMemo(() => {
const options: { label: string; value: DataRecordValue }[] = [];
@ -270,7 +277,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
<FilterPluginStyle height={height} width={width}>
<StyledFormItem
validateStatus={filterState.validateStatus}
{...formItemData}
extra={formItemExtra}
>
<Select
allowClear

View File

@ -16,8 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { styled } from '@superset-ui/core';
import React, { useEffect } from 'react';
import { styled, TimeRangeEndpoint } from '@superset-ui/core';
import React, { useCallback, useEffect } from 'react';
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
import { NO_TIME_RANGE } from 'src/explore/constants';
import { PluginFilterTimeProps } from './types';
@ -55,6 +55,11 @@ const ControlContainer = styled.div<{
}
`;
const endpoints = ['inclusive', 'exclusive'] as [
TimeRangeEndpoint,
TimeRangeEndpoint,
];
export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
const {
setDataMask,
@ -66,19 +71,22 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
formData: { inputRef },
} = props;
const handleTimeRangeChange = (timeRange?: string): void => {
const isSet = timeRange && timeRange !== NO_TIME_RANGE;
setDataMask({
extraFormData: isSet
? {
time_range: timeRange,
}
: {},
filterState: {
value: isSet ? timeRange : undefined,
},
});
};
const handleTimeRangeChange = useCallback(
(timeRange?: string): void => {
const isSet = timeRange && timeRange !== NO_TIME_RANGE;
setDataMask({
extraFormData: isSet
? {
time_range: timeRange,
}
: {},
filterState: {
value: isSet ? timeRange : undefined,
},
});
},
[setDataMask],
);
useEffect(() => {
handleTimeRangeChange(filterState.value);
@ -97,7 +105,7 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
onMouseLeave={unsetFocusedFilter}
>
<DateFilterControl
endpoints={['inclusive', 'exclusive']}
endpoints={endpoints}
value={filterState.value || NO_TIME_RANGE}
name="time_range"
onChange={handleTimeRangeChange}