feat(native-filters): Highlight charts affected by focused native filter (#14693)

* Highlight charts affected by focused native filter

* Remove tabs animation on dashboard

* Remove a test that checks for "animated={true}" prop on tabs

* Move hooks types to a separate interface
This commit is contained in:
Kamil Gabryjelski 2021-05-21 15:54:33 +02:00 committed by GitHub
parent 29828f8552
commit c831655913
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 220 additions and 74 deletions

View File

@ -128,12 +128,6 @@ describe('DashboardBuilder', () => {
expect(parentSize.find(Tabs.TabPane)).toHaveLength(2);
});
it('should have default animated=true on Tabs for perf', () => {
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
const tabProps = wrapper.find(ParentSize).find(Tabs).props();
expect(tabProps.animated).toEqual(true);
});
it('should render a TabPane and DashboardGrid for first Tab', () => {
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
const parentSize = wrapper.find(ParentSize);

View File

@ -116,7 +116,7 @@ type BootstrapData = {
};
};
export interface SetBooststapData {
export interface SetBootstrapData {
type: typeof HYDRATE_DASHBOARD;
data: BootstrapData;
}
@ -182,6 +182,28 @@ export function saveFilterSets(
};
}
export const SET_FOCUSED_NATIVE_FILTER = 'SET_FOCUSED_NATIVE_FILTER';
export interface SetFocusedNativeFilter {
type: typeof SET_FOCUSED_NATIVE_FILTER;
id: string;
}
export const UNSET_FOCUSED_NATIVE_FILTER = 'UNSET_FOCUSED_NATIVE_FILTER';
export interface UnsetFocusedNativeFilter {
type: typeof UNSET_FOCUSED_NATIVE_FILTER;
}
export function setFocusedNativeFilter(id: string): SetFocusedNativeFilter {
return {
type: SET_FOCUSED_NATIVE_FILTER,
id,
};
}
export function unsetFocusedNativeFilter(): UnsetFocusedNativeFilter {
return {
type: UNSET_FOCUSED_NATIVE_FILTER,
};
}
export type AnyFilterAction =
| SetFilterConfigBegin
| SetFilterConfigComplete
@ -190,4 +212,6 @@ export type AnyFilterAction =
| SetFilterSetsConfigComplete
| SetFilterSetsConfigFail
| SaveFilterSets
| SetBooststapData;
| SetBootstrapData
| SetFocusedNativeFilter
| UnsetFocusedNativeFilter;

View File

@ -72,7 +72,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
activeKey={activeKey}
renderTabBar={() => <></>}
fullWidth={false}
animated
animated={false}
allowOverflow
>
{childIds.map((id, index) => (

View File

@ -107,7 +107,7 @@ const FilterFocusHighlight = React.forwardRef(
styles = {
borderColor: theme.colors.primary.light2,
opacity: 1,
boxShadow: `0px 0px ${({ theme }) => theme.gridUnit * 2}px ${
boxShadow: `0px 0px ${theme.gridUnit * 2}px ${
theme.colors.primary.light2
}`,
pointerEvents: 'auto',

View File

@ -26,12 +26,17 @@ import {
Behavior,
ChartDataResponseResult,
} from '@superset-ui/core';
import { useDispatch } from 'react-redux';
import { areObjectsEqual } from 'src/reduxUtils';
import { getChartDataRequest } from 'src/chart/chartAction';
import Loading from 'src/components/Loading';
import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { waitForAsyncData } from 'src/middleware/asyncEvent';
import {
setFocusedNativeFilter,
unsetFocusedNativeFilter,
} from 'src/dashboard/actions/nativeFilters';
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
import { FilterProps } from './types';
import { getFormData } from '../../utils';
@ -61,6 +66,7 @@ const FilterValue: React.FC<FilterProps> = ({
const { name: groupby } = column;
const hasDataSource = !!datasetId;
const [loading, setLoading] = useState<boolean>(hasDataSource);
const dispatch = useDispatch();
useEffect(() => {
const newFormData = getFormData({
...filter,
@ -130,6 +136,9 @@ const FilterValue: React.FC<FilterProps> = ({
const setDataMask = (dataMask: DataMask) =>
onFilterSelectionChange(filter, dataMask);
const setFocusedFilter = () => dispatch(setFocusedNativeFilter(id));
const unsetFocusedFilter = () => dispatch(unsetFocusedNativeFilter());
if (error) {
return (
<BasicErrorAlert
@ -155,7 +164,7 @@ const FilterValue: React.FC<FilterProps> = ({
behaviors={[Behavior.NATIVE_FILTER]}
filterState={filter.dataMask?.filterState}
ownState={filter.dataMask?.ownState}
hooks={{ setDataMask }}
hooks={{ setDataMask, setFocusedFilter, unsetFocusedFilter }}
/>
)}
</FilterItem>

View File

@ -22,7 +22,7 @@ import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core';
import { useDispatch } from 'react-redux';
import { DataMaskState, DataMaskWithId } from 'src/dataMask/types';
import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters';
import { Filters, FilterSet, FilterSets } from 'src/dashboard/reducers/types';
import { Filters, FilterSet } from 'src/dashboard/reducers/types';
import { areObjectsEqual } from 'src/reduxUtils';
import { findExistingFilterSet, generateFiltersSetId } from './utils';
import { Filter } from '../../types';

View File

@ -79,12 +79,26 @@ function selectFocusedFilterScope(dashboardState, dashboardFilters) {
};
}
function selectFocusedNativeFilterScope(nativeFilters) {
if (!nativeFilters.focusedFilterId) return null;
const id = nativeFilters.focusedFilterId;
const focusedFilterScope = nativeFilters.filters[id].scope;
return {
chartId: id,
scope: {
scope: focusedFilterScope.rootPath,
immune: focusedFilterScope.excluded,
},
};
}
function mapStateToProps(
{
dashboardLayout: undoableLayout,
dashboardState,
dashboardInfo,
dashboardFilters,
nativeFilters,
},
ownProps,
) {
@ -101,10 +115,9 @@ function mapStateToProps(
directPathToChild: dashboardState.directPathToChild,
directPathLastUpdated: dashboardState.directPathLastUpdated,
dashboardId: dashboardInfo.id,
focusedFilterScope: selectFocusedFilterScope(
dashboardState,
dashboardFilters,
),
focusedFilterScope:
selectFocusedFilterScope(dashboardState, dashboardFilters) ||
selectFocusedNativeFilterScope(nativeFilters),
};
// rows and columns need more data about their child dimensions

View File

@ -21,6 +21,8 @@ import {
SAVE_FILTER_SETS,
SET_FILTER_CONFIG_COMPLETE,
SET_FILTER_SETS_CONFIG_COMPLETE,
SET_FOCUSED_NATIVE_FILTER,
UNSET_FOCUSED_NATIVE_FILTER,
} from 'src/dashboard/actions/nativeFilters';
import { FilterSet, NativeFiltersState } from './types';
import { FilterConfiguration } from '../components/nativeFilters/types';
@ -58,6 +60,7 @@ export function getInitialState({
} else {
state.filterSets = prevState?.filterSets ?? {};
}
state.focusedFilterId = undefined;
return state as NativeFiltersState;
}
@ -97,6 +100,17 @@ export default function nativeFilterReducer(
state,
});
case SET_FOCUSED_NATIVE_FILTER:
return {
...state,
focusedFilterId: action.id,
};
case UNSET_FOCUSED_NATIVE_FILTER:
return {
...state,
focusedFilterId: undefined,
};
// TODO handle SET_FILTER_CONFIG_FAIL action
default:
return state;

View File

@ -99,4 +99,5 @@ export type Filters = {
export type NativeFiltersState = {
filters: Filters;
filterSets: FilterSets;
focusedFilterId?: string;
};

View File

@ -47,13 +47,15 @@
border-radius: @border-radius-large;
box-shadow: inset 0 0 0 2px @shadow-highlight,
0 0 0 3px fade(@shadow-highlight, @opacity-light);
transition: box-shadow 1s ease-in-out;
transition: box-shadow 0.2s ease-in-out, opacity 0.2s ease-in-out,
border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
&.fade-out {
border-radius: @border-radius-large;
box-shadow: none;
transition: box-shadow 1s ease-in-out;
transition: box-shadow 0.2s ease-in-out, opacity 0.2s ease-in-out,
border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
}

View File

@ -25,7 +25,16 @@ import { PluginFilterGroupByProps } from './types';
const { Option } = Select;
export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) {
const { data, formData, height, width, setDataMask, filterState } = props;
const {
data,
formData,
height,
width,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
filterState,
} = props;
const { defaultValue, inputRef, multiSelect } = formData;
const [value, setValue] = useState<string[]>(defaultValue ?? []);
@ -68,6 +77,8 @@ export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) {
mode={multiSelect ? 'multiple' : undefined}
// @ts-ignore
onChange={handleChange}
onBlur={unsetFocusedFilter}
onFocus={setFocusedFilter}
ref={inputRef}
>
{columns.map(

View File

@ -29,7 +29,11 @@ export default function transformProps(chartProps: ChartProps) {
width,
filterState,
} = chartProps;
const { setDataMask = () => {} } = hooks;
const {
setDataMask = () => {},
setFocusedFilter = () => {},
unsetFocusedFilter = () => {},
} = hooks;
const { data } = queriesData[0];
@ -41,5 +45,7 @@ export default function transformProps(chartProps: ChartProps) {
data,
formData: { ...DEFAULT_FORM_DATA, ...formData },
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
};
}

View File

@ -21,10 +21,9 @@ import {
DataRecord,
FilterState,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { RefObject } from 'react';
import { PluginFilterStylesProps } from '../types';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterGroupByCustomizeProps {
defaultValue?: string[] | null;
@ -39,10 +38,9 @@ export type PluginFilterGroupByQueryFormData = QueryFormData &
export type PluginFilterGroupByProps = PluginFilterStylesProps & {
behaviors: Behavior[];
data: DataRecord[];
setDataMask: SetDataMaskHook;
filterState: FilterState;
formData: PluginFilterGroupByQueryFormData;
};
} & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterGroupByCustomizeProps = {
defaultValue: null,

View File

@ -30,6 +30,8 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
height,
width,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
inputRef,
filterState,
} = props;
@ -73,15 +75,17 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
{Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
<h4>{t('Chosen non-numeric column')}</h4>
) : (
<Slider
range
min={min}
max={max}
value={value}
onAfterChange={handleAfterChange}
onChange={handleChange}
ref={inputRef}
/>
<div onMouseEnter={setFocusedFilter} onMouseLeave={unsetFocusedFilter}>
<Slider
range
min={min}
max={max}
value={value}
onAfterChange={handleAfterChange}
onChange={handleChange}
ref={inputRef}
/>
</div>
)}
</Styles>
);

View File

@ -28,7 +28,11 @@ export default function transformProps(chartProps: ChartProps) {
behaviors,
filterState,
} = chartProps;
const { setDataMask } = hooks;
const {
setDataMask = () => {},
setFocusedFilter = () => {},
unsetFocusedFilter = () => {},
} = hooks;
const { data } = queriesData[0];
return {
@ -39,5 +43,7 @@ export default function transformProps(chartProps: ChartProps) {
setDataMask,
filterState,
width,
setFocusedFilter,
unsetFocusedFilter,
};
}

View File

@ -21,10 +21,9 @@ import {
DataRecord,
FilterState,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { RefObject } from 'react';
import { PluginFilterStylesProps } from '../types';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterSelectCustomizeProps {
max?: number;
@ -38,8 +37,7 @@ export type PluginFilterRangeQueryFormData = QueryFormData &
export type PluginFilterRangeProps = PluginFilterStylesProps & {
data: DataRecord[];
formData: PluginFilterRangeQueryFormData;
setDataMask: SetDataMaskHook;
filterState: FilterState;
behaviors: Behavior[];
inputRef: RefObject<any>;
};
} & PluginFilterHooks;

View File

@ -40,6 +40,8 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
height,
width,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
filterState,
appSection,
} = props;
@ -78,6 +80,11 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
setCurrentSuggestionSearch('');
};
const handleBlur = () => {
clearSuggestionSearch();
unsetFocusedFilter();
};
const [col] = groupby;
const datatype: GenericDataType = coltypeMap[col];
const labelFormatter = getDataRecordFormatter({
@ -157,7 +164,8 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
placeholder={placeholderText}
onSearch={setCurrentSuggestionSearch}
onSelect={clearSuggestionSearch}
onBlur={clearSuggestionSearch}
onBlur={handleBlur}
onFocus={setFocusedFilter}
// @ts-ignore
onChange={handleChange}
ref={inputRef}

View File

@ -33,7 +33,11 @@ export default function transformProps(
filterState,
} = chartProps;
const newFormData = { ...DEFAULT_FORM_DATA, ...formData };
const { setDataMask = () => {} } = hooks;
const {
setDataMask = () => {},
setFocusedFilter = () => {},
unsetFocusedFilter = () => {},
} = hooks;
const [queryData] = queriesData;
const { colnames = [], coltypes = [], data = [] } = queryData || {};
const coltypeMap: Record<string, GenericDataType> = colnames.reduce(
@ -51,5 +55,7 @@ export default function transformProps(
data,
formData: newFormData,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
};
}

View File

@ -24,11 +24,10 @@ import {
FilterState,
GenericDataType,
QueryFormData,
SetDataMaskHook,
ChartDataResponseResult,
} from '@superset-ui/core';
import { RefObject } from 'react';
import { PluginFilterStylesProps } from '../types';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
export const FIRST_VALUE = '__FIRST_VALUE__';
export type SelectValue = (number | string)[] | null;
@ -55,12 +54,11 @@ export interface PluginFilterSelectChartProps extends ChartProps {
export type PluginFilterSelectProps = PluginFilterStylesProps & {
coltypeMap: Record<string, GenericDataType>;
data: DataRecord[];
setDataMask: SetDataMaskHook;
behaviors: Behavior[];
appSection: AppSection;
formData: PluginFilterSelectQueryFormData;
filterState: FilterState;
};
} & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterSelectCustomizeProps = {
defaultValue: null,

View File

@ -28,8 +28,19 @@ const TimeFilterStyles = styled(Styles)`
overflow-x: scroll;
`;
const ControlContainer = styled.div`
display: inline-block;
`;
export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
const { formData, setDataMask, width, filterState } = props;
const {
formData,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
width,
filterState,
} = props;
const { defaultValue } = formData;
const [value, setValue] = useState<string>(defaultValue ?? DEFAULT_VALUE);
@ -56,11 +67,16 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
return (
// @ts-ignore
<TimeFilterStyles width={width}>
<DateFilterControl
value={value}
name="time_range"
onChange={handleTimeRangeChange}
/>
<ControlContainer
onMouseEnter={setFocusedFilter}
onMouseLeave={unsetFocusedFilter}
>
<DateFilterControl
value={value}
name="time_range"
onChange={handleTimeRangeChange}
/>
</ControlContainer>
</TimeFilterStyles>
);
}

View File

@ -29,7 +29,11 @@ export default function transformProps(chartProps: ChartProps) {
behaviors,
filterState,
} = chartProps;
const { setDataMask = () => {} } = hooks;
const {
setDataMask = () => {},
setFocusedFilter = () => {},
unsetFocusedFilter = () => {},
} = hooks;
const { data } = queriesData[0];
return {
@ -42,6 +46,8 @@ export default function transformProps(chartProps: ChartProps) {
height,
behaviors,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
width,
};
}

View File

@ -21,9 +21,8 @@ import {
DataRecord,
FilterState,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { PluginFilterStylesProps } from '../types';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterTimeCustomizeProps {
defaultValue?: string | null;
@ -36,10 +35,9 @@ export type PluginFilterSelectQueryFormData = QueryFormData &
export type PluginFilterTimeProps = PluginFilterStylesProps & {
behaviors: Behavior[];
data: DataRecord[];
setDataMask: SetDataMaskHook;
formData: PluginFilterSelectQueryFormData;
filterState: FilterState;
};
} & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterTimeCustomizeProps = {
defaultValue: null,

View File

@ -33,7 +33,16 @@ const { Option } = Select;
export default function PluginFilterTimeColumn(
props: PluginFilterTimeColumnProps,
) {
const { data, formData, height, width, setDataMask, filterState } = props;
const {
data,
formData,
height,
width,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
filterState,
} = props;
const { defaultValue, inputRef } = formData;
const [value, setValue] = useState<string[]>(defaultValue ?? []);
@ -80,6 +89,8 @@ export default function PluginFilterTimeColumn(
placeholder={placeholderText}
// @ts-ignore
onChange={handleChange}
onBlur={unsetFocusedFilter}
onFocus={setFocusedFilter}
ref={inputRef}
>
{timeColumns.map(

View File

@ -29,7 +29,11 @@ export default function transformProps(chartProps: ChartProps) {
width,
filterState,
} = chartProps;
const { setDataMask = () => {} } = hooks;
const {
setDataMask = () => {},
setFocusedFilter = () => {},
unsetFocusedFilter = () => {},
} = hooks;
const { data } = queriesData[0];
@ -41,5 +45,7 @@ export default function transformProps(chartProps: ChartProps) {
data,
formData: { ...DEFAULT_FORM_DATA, ...formData },
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
};
}

View File

@ -21,10 +21,9 @@ import {
DataRecord,
FilterState,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { RefObject } from 'react';
import { PluginFilterStylesProps } from '../types';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterTimeColumnCustomizeProps {
defaultValue?: string[] | null;
@ -38,10 +37,9 @@ export type PluginFilterTimeColumnQueryFormData = QueryFormData &
export type PluginFilterTimeColumnProps = PluginFilterStylesProps & {
behaviors: Behavior[];
data: DataRecord[];
setDataMask: SetDataMaskHook;
filterState: FilterState;
formData: PluginFilterTimeColumnQueryFormData;
};
} & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterTimeColumnCustomizeProps = {
defaultValue: null,

View File

@ -33,7 +33,16 @@ const { Option } = Select;
export default function PluginFilterTimegrain(
props: PluginFilterTimeGrainProps,
) {
const { data, formData, height, width, setDataMask, filterState } = props;
const {
data,
formData,
height,
width,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
filterState,
} = props;
const { defaultValue, inputRef } = formData;
const [value, setValue] = useState<string[]>(defaultValue ?? []);
@ -77,6 +86,8 @@ export default function PluginFilterTimegrain(
placeholder={placeholderText}
// @ts-ignore
onChange={handleChange}
onBlur={unsetFocusedFilter}
onFocus={setFocusedFilter}
ref={inputRef}
>
{(data || []).map((row: { name: string; duration: string }) => {

View File

@ -28,7 +28,11 @@ export default function transformProps(chartProps: ChartProps) {
width,
filterState,
} = chartProps;
const { setDataMask = () => {} } = hooks;
const {
setDataMask = () => {},
setFocusedFilter = () => {},
unsetFocusedFilter = () => {},
} = hooks;
const { data } = queriesData[0];
@ -39,5 +43,7 @@ export default function transformProps(chartProps: ChartProps) {
data,
formData: { ...DEFAULT_FORM_DATA, ...formData },
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
};
}

View File

@ -16,14 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
FilterState,
QueryFormData,
DataRecord,
SetDataMaskHook,
} from '@superset-ui/core';
import { FilterState, QueryFormData, DataRecord } from '@superset-ui/core';
import { RefObject } from 'react';
import { PluginFilterStylesProps } from '../types';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterTimeGrainCustomizeProps {
defaultValue?: string[] | null;
@ -36,10 +31,9 @@ export type PluginFilterTimeGrainQueryFormData = QueryFormData &
export type PluginFilterTimeGrainProps = PluginFilterStylesProps & {
data: DataRecord[];
setDataMask: SetDataMaskHook;
filterState: FilterState;
formData: PluginFilterTimeGrainQueryFormData;
};
} & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterTimeGrainCustomizeProps = {
defaultValue: null,

View File

@ -21,8 +21,8 @@ import { Select } from 'src/common/components';
import { PluginFilterStylesProps } from './types';
export const Styles = styled.div<PluginFilterStylesProps>`
height: ${({ height }) => height};
width: ${({ width }) => width};
height: ${({ height }) => height}px;
width: ${({ width }) => width}px;
`;
export const StyledSelect = styled(Select)`

View File

@ -1,3 +1,5 @@
import { SetDataMaskHook } from '@superset-ui/core';
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@ -20,3 +22,9 @@ export interface PluginFilterStylesProps {
height: number;
width: number;
}
export interface PluginFilterHooks {
setDataMask: SetDataMaskHook;
setFocusedFilter: () => void;
unsetFocusedFilter: () => void;
}