From 5cbe2ac7603447ef0e426bf7aef3e2bcb355c22c Mon Sep 17 00:00:00 2001 From: simcha90 <56388545+simcha90@users.noreply.github.com> Date: Wed, 17 Feb 2021 21:06:12 +0200 Subject: [PATCH] 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 --- .../spec/fixtures/mockNativeFilters.ts | 1 + .../util/getFormDataWithExtraFilters_spec.ts | 1 + .../src/dashboard/actions/nativeFilters.ts | 40 ++++++- .../nativeFilters/FilterBar/FilterBar.tsx | 109 +++++++++++++++++- .../nativeFilters/FilterBar/FilterValue.tsx | 2 +- .../nativeFilters/FilterBar/state.ts | 14 ++- .../nativeFilters/FilterBar/utils.ts | 3 + .../components/nativeFilters/utils.ts | 4 +- .../src/dashboard/reducers/nativeFilters.ts | 42 +++++-- .../src/dashboard/reducers/types.ts | 11 ++ superset-frontend/src/featureFlags.ts | 1 + superset/config.py | 1 + 12 files changed, 212 insertions(+), 17 deletions(-) diff --git a/superset-frontend/spec/fixtures/mockNativeFilters.ts b/superset-frontend/spec/fixtures/mockNativeFilters.ts index 3cc5a9f16..f0c3a32ef 100644 --- a/superset-frontend/spec/fixtures/mockNativeFilters.ts +++ b/superset-frontend/spec/fixtures/mockNativeFilters.ts @@ -19,6 +19,7 @@ import { NativeFiltersState } from 'src/dashboard/reducers/types'; export const nativeFilters: NativeFiltersState = { + filterSets: {}, filters: { 'NATIVE_FILTER-e7Q8zKixx': { id: 'NATIVE_FILTER-e7Q8zKixx', diff --git a/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts b/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts index 54b41090f..fd8ae3577 100644 --- a/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts +++ b/superset-frontend/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.ts @@ -51,6 +51,7 @@ describe('getFormDataWithExtraFilters', () => { }, sliceId: chartId, nativeFilters: { + filterSets: {}, filters: { [filterId]: ({ id: filterId, diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 09a2e2c0a..29227b0cf 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -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; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index 6e4dadade..97837e953 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -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 = ({ 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( ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ); @@ -218,6 +258,17 @@ const FilterBar: React.FC = ({ }); }; + 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 = ({ }); }; + const takeFiltersSet = (value: SelectValue) => { + dispatch(setFiltersState(filterSets[String(value)]?.filtersState)); + }; + return ( = ({ {t('Apply')} + {isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) && ( + + + +
{t('Choose filters set')}
+ +
+ +
{t('Name')}
+ ) => { + setFiltersSetName(value); + }} + /> +
+ +
+
+ )} {cascadeFilters.map(filter => ( = ({ const [loading, setLoading] = useState(hasDataSource); useEffect(() => { const newFormData = getFormData({ + ...filter, datasetId, cascadingFilters, groupby, currentValue, inputRef, - ...filter, }); if (!areObjectsEqual(formData || {}, newFormData)) { setFormData(newFormData); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts index af3c473b6..484987136 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts @@ -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(state => state.nativeFilters.filters); +} + +export function useFiltersState() { return useSelector( - state => state.nativeFilters.filters, + state => state.nativeFilters.filtersState, + ); +} + +export function useFilterSets() { + return useSelector( + state => state.nativeFilters.filterSets ?? {}, ); } diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts index 4052cd931..c7f466869 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts @@ -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()}`; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts index 33afaeebf..a3396d2fc 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts @@ -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, }; }; diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index f0cdd8ace..66b33f2f2 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -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: diff --git a/superset-frontend/src/dashboard/reducers/types.ts b/superset-frontend/src/dashboard/reducers/types.ts index 0f8b06793..46749d4a7 100644 --- a/superset-frontend/src/dashboard/reducers/types.ts +++ b/superset-frontend/src/dashboard/reducers/types.ts @@ -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; }; diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts index c8b8acc20..a7bd2cac6 100644 --- a/superset-frontend/src/featureFlags.ts +++ b/superset-frontend/src/featureFlags.ts @@ -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', diff --git a/superset/config.py b/superset/config.py index 79eca2162..063f19723 100644 --- a/superset/config.py +++ b/superset/config.py @@ -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