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:
parent
450215549f
commit
5cbe2ac760
|
|
@ -19,6 +19,7 @@
|
|||
import { NativeFiltersState } from 'src/dashboard/reducers/types';
|
||||
|
||||
export const nativeFilters: NativeFiltersState = {
|
||||
filterSets: {},
|
||||
filters: {
|
||||
'NATIVE_FILTER-e7Q8zKixx': {
|
||||
id: 'NATIVE_FILTER-e7Q8zKixx',
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ describe('getFormDataWithExtraFilters', () => {
|
|||
},
|
||||
sliceId: chartId,
|
||||
nativeFilters: {
|
||||
filterSets: {},
|
||||
filters: {
|
||||
[filterId]: ({
|
||||
id: filterId,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue