feat(filters-set): basic implementation for managing user filter sets (#13031)

* feat: POC adding filters set feature

* lint: fix TS

* fix: fix FF name

* refactor: fix CR notes

* fix: fix update values in filter bar
This commit is contained in:
simcha90
2021-02-17 21:06:12 +02:00
committed by GitHub
parent 450215549f
commit 5cbe2ac760
12 changed files with 212 additions and 17 deletions

View File

@@ -19,6 +19,7 @@
import { NativeFiltersState } from 'src/dashboard/reducers/types'; import { NativeFiltersState } from 'src/dashboard/reducers/types';
export const nativeFilters: NativeFiltersState = { export const nativeFilters: NativeFiltersState = {
filterSets: {},
filters: { filters: {
'NATIVE_FILTER-e7Q8zKixx': { 'NATIVE_FILTER-e7Q8zKixx': {
id: 'NATIVE_FILTER-e7Q8zKixx', id: 'NATIVE_FILTER-e7Q8zKixx',

View File

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

View File

@@ -24,7 +24,7 @@ import {
FilterConfiguration, FilterConfiguration,
} from 'src/dashboard/components/nativeFilters/types'; } from 'src/dashboard/components/nativeFilters/types';
import { dashboardInfoChanged } from './dashboardInfo'; import { dashboardInfoChanged } from './dashboardInfo';
import { CurrentFilterState } from '../reducers/types'; import { CurrentFilterState, NativeFilterState } from '../reducers/types';
import { SelectedValues } from '../components/nativeFilters/FilterConfigModal/types'; import { SelectedValues } from '../components/nativeFilters/FilterConfigModal/types';
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN'; export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
@@ -103,6 +103,20 @@ export interface SetExtraFormData {
currentState: CurrentFilterState; currentState: CurrentFilterState;
} }
export const SAVE_FILTER_SETS = 'SAVE_FILTER_SETS';
export interface SaveFilterSets {
type: typeof SAVE_FILTER_SETS;
name: string;
filtersState: NativeFilterState;
filtersSetId: string;
}
export const SET_FILTERS_STATE = 'SET_FILTERS_STATE';
export interface SetFiltersState {
type: typeof SET_FILTERS_STATE;
filtersState: NativeFilterState;
}
export function setFilterState( export function setFilterState(
selectedValues: SelectedValues, selectedValues: SelectedValues,
filter: Filter, filter: Filter,
@@ -134,9 +148,33 @@ export function setExtraFormData(
}; };
} }
export function saveFilterSets(
name: string,
filtersSetId: string,
filtersState: NativeFilterState,
): SaveFilterSets {
return {
type: SAVE_FILTER_SETS,
name,
filtersSetId,
filtersState,
};
}
export function setFiltersState(
filtersState: NativeFilterState,
): SetFiltersState {
return {
type: SET_FILTERS_STATE,
filtersState,
};
}
export type AnyFilterAction = export type AnyFilterAction =
| SetFilterConfigBegin | SetFilterConfigBegin
| SetFilterConfigComplete | SetFilterConfigComplete
| SetFilterConfigFail | SetFilterConfigFail
| SetFiltersState
| SetExtraFormData | SetExtraFormData
| SaveFilterSets
| SetFilterState; | SetFilterState;

View File

@@ -16,18 +16,34 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { styled, t, ExtraFormData } from '@superset-ui/core'; import { styled, t, tn, ExtraFormData } from '@superset-ui/core';
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo, ChangeEvent } from 'react';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import cx from 'classnames'; import cx from 'classnames';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import Icon from 'src/components/Icon'; import Icon from 'src/components/Icon';
import { CurrentFilterState } from 'src/dashboard/reducers/types'; import { CurrentFilterState } from 'src/dashboard/reducers/types';
import { Input, Select } from 'src/common/components';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import {
saveFilterSets,
setFiltersState,
} from 'src/dashboard/actions/nativeFilters';
import { SelectValue } from 'antd/lib/select';
import FilterConfigurationLink from './FilterConfigurationLink'; import FilterConfigurationLink from './FilterConfigurationLink';
import { useFilters, useSetExtraFormData } from './state'; import {
useFilters,
useFilterSets,
useFiltersState,
useSetExtraFormData,
} from './state';
import { useFilterConfiguration } from '../state'; import { useFilterConfiguration } from '../state';
import { Filter } from '../types'; import { Filter } from '../types';
import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils'; import {
buildCascadeFiltersTree,
generateFiltersSetId,
mapParentFiltersToChildren,
} from './utils';
import CascadePopover from './CascadePopover'; import CascadePopover from './CascadePopover';
const barWidth = `250px`; const barWidth = `250px`;
@@ -65,6 +81,17 @@ const Bar = styled.div`
} }
`; `;
const StyledTitle = styled.h4`
width: 100%;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
color: ${({ theme }) => theme.colors.grayscale.dark1};
margin: 0;
overflow-wrap: break-word;
& > .ant-select {
width: 100%;
}
`;
const CollapsedBar = styled.div` const CollapsedBar = styled.div`
position: absolute; position: absolute;
top: 0; top: 0;
@@ -102,6 +129,15 @@ const StyledCollapseIcon = styled(Icon)`
margin-bottom: ${({ theme }) => theme.gridUnit * 3}px; margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
`; `;
const FilterSet = styled.div`
display: grid;
align-items: center;
justify-content: center;
grid-template-columns: 1fr;
grid-gap: 10px;
padding-top: 10px;
`;
const TitleArea = styled.h4` const TitleArea = styled.h4`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -152,9 +188,13 @@ const FilterBar: React.FC<FiltersBarProps> = ({
currentState: CurrentFilterState; currentState: CurrentFilterState;
}; };
}>({}); }>({});
const dispatch = useDispatch();
const setExtraFormData = useSetExtraFormData(); const setExtraFormData = useSetExtraFormData();
const filtersState = useFiltersState();
const filterSets = useFilterSets();
const filterConfigs = useFilterConfiguration(); const filterConfigs = useFilterConfiguration();
const filters = useFilters(); const filters = useFilters();
const [filtersSetName, setFiltersSetName] = useState('');
const canEdit = useSelector<any, boolean>( const canEdit = useSelector<any, boolean>(
({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
); );
@@ -218,6 +258,17 @@ const FilterBar: React.FC<FiltersBarProps> = ({
}); });
}; };
const handleSaveFilterSets = () => {
dispatch(
saveFilterSets(
filtersSetName.trim(),
generateFiltersSetId(),
filtersState,
),
);
setFiltersSetName('');
};
const handleResetAll = () => { const handleResetAll = () => {
filterConfigs.forEach(filter => { filterConfigs.forEach(filter => {
setExtraFormData(filter.id, filterData[filter.id]?.extraFormData, { setExtraFormData(filter.id, filterData[filter.id]?.extraFormData, {
@@ -227,6 +278,10 @@ const FilterBar: React.FC<FiltersBarProps> = ({
}); });
}; };
const takeFiltersSet = (value: SelectValue) => {
dispatch(setFiltersState(filterSets[String(value)]?.filtersState));
};
return ( return (
<BarWrapper data-test="filter-bar" className={cx({ open: filtersOpen })}> <BarWrapper data-test="filter-bar" className={cx({ open: filtersOpen })}>
<CollapsedBar <CollapsedBar
@@ -269,6 +324,50 @@ const FilterBar: React.FC<FiltersBarProps> = ({
{t('Apply')} {t('Apply')}
</Button> </Button>
</ActionButtons> </ActionButtons>
{isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) && (
<ActionButtons>
<FilterSet>
<StyledTitle>
<div>{t('Choose filters set')}</div>
<Select
size="small"
allowClear
placeholder={tn(
'Available %d sets',
Object.keys(filterSets).length,
)}
onChange={takeFiltersSet}
>
{Object.values(filterSets).map(({ name, id }) => (
<Select.Option value={id}>{name}</Select.Option>
))}
</Select>
</StyledTitle>
<StyledTitle>
<div>{t('Name')}</div>
<Input
size="small"
placeholder={t('Enter filter set name')}
value={filtersSetName}
onChange={({
target: { value },
}: ChangeEvent<HTMLInputElement>) => {
setFiltersSetName(value);
}}
/>
</StyledTitle>
<Button
buttonStyle="secondary"
buttonSize="small"
disabled={filtersSetName.trim() === ''}
onClick={handleSaveFilterSets}
data-test="filter-save-filters-set-button"
>
{t('Save Filters Set')}
</Button>
</FilterSet>
</ActionButtons>
)}
<FilterControls> <FilterControls>
{cascadeFilters.map(filter => ( {cascadeFilters.map(filter => (
<CascadePopover <CascadePopover

View File

@@ -66,12 +66,12 @@ const FilterValue: React.FC<FilterProps> = ({
const [loading, setLoading] = useState<boolean>(hasDataSource); const [loading, setLoading] = useState<boolean>(hasDataSource);
useEffect(() => { useEffect(() => {
const newFormData = getFormData({ const newFormData = getFormData({
...filter,
datasetId, datasetId,
cascadingFilters, cascadingFilters,
groupby, groupby,
currentValue, currentValue,
inputRef, inputRef,
...filter,
}); });
if (!areObjectsEqual(formData || {}, newFormData)) { if (!areObjectsEqual(formData || {}, newFormData)) {
setFormData(newFormData); setFormData(newFormData);

View File

@@ -25,12 +25,24 @@ import {
CurrentFilterState, CurrentFilterState,
NativeFilterState, NativeFilterState,
NativeFiltersState, NativeFiltersState,
FilterSets,
} from 'src/dashboard/reducers/types'; } from 'src/dashboard/reducers/types';
import { mergeExtraFormData } from '../utils'; import { mergeExtraFormData } from '../utils';
import { Filter } from '../types';
export function useFilters() { export function useFilters() {
return useSelector<any, Filter>(state => state.nativeFilters.filters);
}
export function useFiltersState() {
return useSelector<any, NativeFilterState>( return useSelector<any, NativeFilterState>(
state => state.nativeFilters.filters, state => state.nativeFilters.filtersState,
);
}
export function useFilterSets() {
return useSelector<any, FilterSets>(
state => state.nativeFilters.filterSets ?? {},
); );
} }

View File

@@ -1,3 +1,4 @@
import shortid from 'shortid';
import { Filter } from '../types'; import { Filter } from '../types';
import { CascadeFilter } from './types'; import { CascadeFilter } from './types';
@@ -51,3 +52,5 @@ export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] {
.filter(filter => !filter.cascadeParentIds?.length) .filter(filter => !filter.cascadeParentIds?.length)
.map(getCascadeFilter); .map(getCascadeFilter);
} }
export const generateFiltersSetId = () => `FILTERS_SET-${shortid.generate()}`;

View File

@@ -52,6 +52,8 @@ export const getFormData = ({
}; };
} }
return { return {
...controlValues,
...otherProps,
adhoc_filters: [], adhoc_filters: [],
extra_filters: [], extra_filters: [],
extra_form_data: cascadingFilters, extra_form_data: cascadingFilters,
@@ -66,8 +68,6 @@ export const getFormData = ({
url_params: {}, url_params: {},
viz_type: filterType, viz_type: filterType,
inputRef, inputRef,
...controlValues,
...otherProps,
}; };
}; };

View File

@@ -17,9 +17,11 @@
* under the License. * under the License.
*/ */
import { import {
SET_EXTRA_FORM_DATA,
AnyFilterAction, AnyFilterAction,
SAVE_FILTER_SETS,
SET_EXTRA_FORM_DATA,
SET_FILTER_CONFIG_COMPLETE, SET_FILTER_CONFIG_COMPLETE,
SET_FILTERS_STATE,
} from 'src/dashboard/actions/nativeFilters'; } from 'src/dashboard/actions/nativeFilters';
import { NativeFiltersState, NativeFilterState } from './types'; import { NativeFiltersState, NativeFilterState } from './types';
import { FilterConfiguration } from '../components/nativeFilters/types'; import { FilterConfiguration } from '../components/nativeFilters/types';
@@ -33,27 +35,33 @@ export function getInitialFilterState(id: string): NativeFilterState {
export function getInitialState( export function getInitialState(
filterConfig: FilterConfiguration, filterConfig: FilterConfiguration,
prevFiltersState: { [filterId: string]: NativeFilterState }, prevState: NativeFiltersState,
): NativeFiltersState { ): NativeFiltersState {
const filters = {}; const filters = {};
const filtersState = {}; const filtersState = {};
const state = { filters, filtersState }; const state = {
filters,
filtersState,
filterSets: prevState?.filterSets ?? {},
};
filterConfig.forEach(filter => { filterConfig.forEach(filter => {
const { id } = filter; const { id } = filter;
filters[id] = filter; filters[id] = filter;
filtersState[id] = prevFiltersState?.[id] || getInitialFilterState(id); filtersState[id] =
prevState?.filtersState?.[id] || getInitialFilterState(id);
}); });
return state; return state;
} }
export default function nativeFilterReducer( export default function nativeFilterReducer(
state: NativeFiltersState = { filters: {}, filtersState: {} }, state: NativeFiltersState = { filters: {}, filtersState: {}, filterSets: {} },
action: AnyFilterAction, action: AnyFilterAction,
) { ) {
const { filters, filtersState } = state; const { filters, filtersState, filterSets } = state;
switch (action.type) { switch (action.type) {
case SET_EXTRA_FORM_DATA: case SET_EXTRA_FORM_DATA:
return { return {
...state,
filters, filters,
filtersState: { filtersState: {
...filtersState, ...filtersState,
@@ -64,9 +72,29 @@ export default function nativeFilterReducer(
}, },
}, },
}; };
case SAVE_FILTER_SETS:
return {
...state,
filterSets: {
...filterSets,
[action.filtersSetId]: {
id: action.filtersSetId,
name: action.name,
filtersState: action.filtersState,
},
},
};
case SET_FILTERS_STATE:
return {
...state,
filtersState: {
...filtersState,
...action.filtersState,
},
};
case SET_FILTER_CONFIG_COMPLETE: case SET_FILTER_CONFIG_COMPLETE:
return getInitialState(action.filterConfig, filtersState); return getInitialState(action.filterConfig, state);
// TODO handle SET_FILTER_CONFIG_FAIL action // TODO handle SET_FILTER_CONFIG_FAIL action
default: default:

View File

@@ -79,10 +79,21 @@ export type NativeFilterState = {
currentState?: CurrentFilterState; currentState?: CurrentFilterState;
}; };
export type FiltersSet = {
id: string;
name: string;
filtersState: NativeFilterState;
};
export type FilterSets = {
[filtersSetId: string]: FiltersSet;
};
export type NativeFiltersState = { export type NativeFiltersState = {
filters: { filters: {
[filterId: string]: Filter; [filterId: string]: Filter;
}; };
filterSets: FilterSets;
filtersState: { filtersState: {
[filterId: string]: NativeFilterState; [filterId: string]: NativeFilterState;
}; };

View File

@@ -36,6 +36,7 @@ export enum FeatureFlag {
ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML', ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML',
DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS', DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS',
DASHBOARD_CROSS_FILTERS = 'DASHBOARD_CROSS_FILTERS', DASHBOARD_CROSS_FILTERS = 'DASHBOARD_CROSS_FILTERS',
DASHBOARD_NATIVE_FILTERS_SET = 'DASHBOARD_NATIVE_FILTERS_SET',
VERSIONED_EXPORT = 'VERSIONED_EXPORT', VERSIONED_EXPORT = 'VERSIONED_EXPORT',
GLOBAL_ASYNC_QUERIES = 'GLOBAL_ASYNC_QUERIES', GLOBAL_ASYNC_QUERIES = 'GLOBAL_ASYNC_QUERIES',
ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING', ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING',

View File

@@ -329,6 +329,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
"ESCAPE_MARKDOWN_HTML": False, "ESCAPE_MARKDOWN_HTML": False,
"DASHBOARD_NATIVE_FILTERS": False, "DASHBOARD_NATIVE_FILTERS": False,
"DASHBOARD_CROSS_FILTERS": False, "DASHBOARD_CROSS_FILTERS": False,
"DASHBOARD_NATIVE_FILTERS_SET": False,
"GLOBAL_ASYNC_QUERIES": False, "GLOBAL_ASYNC_QUERIES": False,
"VERSIONED_EXPORT": False, "VERSIONED_EXPORT": False,
# Note that: RowLevelSecurityFilter is only given by default to the Admin role # Note that: RowLevelSecurityFilter is only given by default to the Admin role