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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 212 additions and 17 deletions

View File

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

View File

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

View File

@ -24,7 +24,7 @@ import {
FilterConfiguration,
} from 'src/dashboard/components/nativeFilters/types';
import { dashboardInfoChanged } from './dashboardInfo';
import { CurrentFilterState } from '../reducers/types';
import { CurrentFilterState, NativeFilterState } from '../reducers/types';
import { SelectedValues } from '../components/nativeFilters/FilterConfigModal/types';
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
@ -103,6 +103,20 @@ export interface SetExtraFormData {
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(
selectedValues: SelectedValues,
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 =
| SetFilterConfigBegin
| SetFilterConfigComplete
| SetFilterConfigFail
| SetFiltersState
| SetExtraFormData
| SaveFilterSets
| SetFilterState;

View File

@ -16,18 +16,34 @@
* specific language governing permissions and limitations
* under the License.
*/
import { styled, t, ExtraFormData } from '@superset-ui/core';
import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { styled, t, tn, ExtraFormData } from '@superset-ui/core';
import React, { useState, useEffect, useMemo, ChangeEvent } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import cx from 'classnames';
import Button from 'src/components/Button';
import Icon from 'src/components/Icon';
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 { useFilters, useSetExtraFormData } from './state';
import {
useFilters,
useFilterSets,
useFiltersState,
useSetExtraFormData,
} from './state';
import { useFilterConfiguration } from '../state';
import { Filter } from '../types';
import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils';
import {
buildCascadeFiltersTree,
generateFiltersSetId,
mapParentFiltersToChildren,
} from './utils';
import CascadePopover from './CascadePopover';
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`
position: absolute;
top: 0;
@ -102,6 +129,15 @@ const StyledCollapseIcon = styled(Icon)`
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`
display: flex;
flex-direction: row;
@ -152,9 +188,13 @@ const FilterBar: React.FC<FiltersBarProps> = ({
currentState: CurrentFilterState;
};
}>({});
const dispatch = useDispatch();
const setExtraFormData = useSetExtraFormData();
const filtersState = useFiltersState();
const filterSets = useFilterSets();
const filterConfigs = useFilterConfiguration();
const filters = useFilters();
const [filtersSetName, setFiltersSetName] = useState('');
const canEdit = useSelector<any, boolean>(
({ 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 = () => {
filterConfigs.forEach(filter => {
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 (
<BarWrapper data-test="filter-bar" className={cx({ open: filtersOpen })}>
<CollapsedBar
@ -269,6 +324,50 @@ const FilterBar: React.FC<FiltersBarProps> = ({
{t('Apply')}
</Button>
</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>
{cascadeFilters.map(filter => (
<CascadePopover

View File

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

View File

@ -25,12 +25,24 @@ import {
CurrentFilterState,
NativeFilterState,
NativeFiltersState,
FilterSets,
} from 'src/dashboard/reducers/types';
import { mergeExtraFormData } from '../utils';
import { Filter } from '../types';
export function useFilters() {
return useSelector<any, Filter>(state => state.nativeFilters.filters);
}
export function useFiltersState() {
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 { CascadeFilter } from './types';
@ -51,3 +52,5 @@ export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] {
.filter(filter => !filter.cascadeParentIds?.length)
.map(getCascadeFilter);
}
export const generateFiltersSetId = () => `FILTERS_SET-${shortid.generate()}`;

View File

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

View File

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

View File

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

View File

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

View File

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