feat(Filter-sets): connect to api (#17055)

* fix:fix get permission function

* feat: filtersets new

* feat: connect filter sets to api

* lint: fix lint

* doc: add comment
This commit is contained in:
simcha90 2021-10-17 09:56:54 +03:00 committed by GitHub
parent 40b88f04f6
commit 37944e18d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 319 additions and 189 deletions

View File

@ -32,8 +32,8 @@ export const mockDataMaskInfo: DataMaskStateWithId = {
export const nativeFiltersInfo: NativeFiltersState = { export const nativeFiltersInfo: NativeFiltersState = {
filterSets: { filterSets: {
'set-id': { '1': {
id: 'DefaultsID', id: 1,
name: 'Set name', name: 'Set name',
nativeFilters: {}, nativeFilters: {},
dataMask: mockDataMaskInfo, dataMask: mockDataMaskInfo,

View File

@ -280,7 +280,6 @@ export const hydrateDashboard = (dashboardData, chartData) => (
const nativeFilters = getInitialNativeFilterState({ const nativeFilters = getInitialNativeFilterState({
filterConfig: metadata?.native_filter_configuration || [], filterConfig: metadata?.native_filter_configuration || [],
filterSetsConfig: metadata?.filter_sets_configuration || [],
}); });
if (!metadata) { if (!metadata) {

View File

@ -20,7 +20,6 @@
import { makeApi } from '@superset-ui/core'; import { makeApi } from '@superset-ui/core';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types'; import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types';
import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types';
import { import {
SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
setDataMaskForFilterConfigComplete, setDataMaskForFilterConfigComplete,
@ -28,17 +27,19 @@ import {
import { HYDRATE_DASHBOARD } from './hydrate'; import { HYDRATE_DASHBOARD } from './hydrate';
import { dashboardInfoChanged } from './dashboardInfo'; import { dashboardInfoChanged } from './dashboardInfo';
import { import {
DashboardInfo,
Filters, Filters,
FilterSet, FilterSet,
FilterSetFullData,
FilterSets, FilterSets,
} from '../reducers/types'; } from '../reducers/types';
import { DashboardInfo, RootState } from '../types';
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN'; export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
export interface SetFilterConfigBegin { export interface SetFilterConfigBegin {
type: typeof SET_FILTER_CONFIG_BEGIN; type: typeof SET_FILTER_CONFIG_BEGIN;
filterConfig: FilterConfiguration; filterConfig: FilterConfiguration;
} }
export const SET_FILTER_CONFIG_COMPLETE = 'SET_FILTER_CONFIG_COMPLETE'; export const SET_FILTER_CONFIG_COMPLETE = 'SET_FILTER_CONFIG_COMPLETE';
export interface SetFilterConfigComplete { export interface SetFilterConfigComplete {
type: typeof SET_FILTER_CONFIG_COMPLETE; type: typeof SET_FILTER_CONFIG_COMPLETE;
@ -54,21 +55,60 @@ export interface SetInScopeStatusOfFilters {
type: typeof SET_IN_SCOPE_STATUS_OF_FILTERS; type: typeof SET_IN_SCOPE_STATUS_OF_FILTERS;
filterConfig: FilterConfiguration; filterConfig: FilterConfiguration;
} }
export const SET_FILTER_SETS_CONFIG_BEGIN = 'SET_FILTER_SETS_CONFIG_BEGIN'; export const SET_FILTER_SETS_BEGIN = 'SET_FILTER_SETS_BEGIN';
export interface SetFilterSetsConfigBegin { export interface SetFilterSetsBegin {
type: typeof SET_FILTER_SETS_CONFIG_BEGIN; type: typeof SET_FILTER_SETS_BEGIN;
filterSetsConfig: FilterSet[];
} }
export const SET_FILTER_SETS_CONFIG_COMPLETE = export const SET_FILTER_SETS_COMPLETE = 'SET_FILTER_SETS_COMPLETE';
'SET_FILTER_SETS_CONFIG_COMPLETE'; export interface SetFilterSetsComplete {
export interface SetFilterSetsConfigComplete { type: typeof SET_FILTER_SETS_COMPLETE;
type: typeof SET_FILTER_SETS_CONFIG_COMPLETE; filterSets: FilterSet[];
filterSetsConfig: FilterSet[];
} }
export const SET_FILTER_SETS_CONFIG_FAIL = 'SET_FILTER_SETS_CONFIG_FAIL'; export const SET_FILTER_SETS_FAIL = 'SET_FILTER_SETS_FAIL';
export interface SetFilterSetsConfigFail { export interface SetFilterSetsFail {
type: typeof SET_FILTER_SETS_CONFIG_FAIL; type: typeof SET_FILTER_SETS_FAIL;
filterSetsConfig: FilterSet[]; }
export const CREATE_FILTER_SET_BEGIN = 'CREATE_FILTER_SET_BEGIN';
export interface CreateFilterSetBegin {
type: typeof CREATE_FILTER_SET_BEGIN;
}
export const CREATE_FILTER_SET_COMPLETE = 'CREATE_FILTER_SET_COMPLETE';
export interface CreateFilterSetComplete {
type: typeof CREATE_FILTER_SET_COMPLETE;
filterSet: FilterSet;
}
export const CREATE_FILTER_SET_FAIL = 'CREATE_FILTER_SET_FAIL';
export interface CreateFilterSetFail {
type: typeof CREATE_FILTER_SET_FAIL;
}
export const DELETE_FILTER_SET_BEGIN = 'DELETE_FILTER_SET_BEGIN';
export interface DeleteFilterSetBegin {
type: typeof DELETE_FILTER_SET_BEGIN;
}
export const DELETE_FILTER_SET_COMPLETE = 'DELETE_FILTER_SET_COMPLETE';
export interface DeleteFilterSetComplete {
type: typeof DELETE_FILTER_SET_COMPLETE;
filterSet: FilterSet;
}
export const DELETE_FILTER_SET_FAIL = 'DELETE_FILTER_SET_FAIL';
export interface DeleteFilterSetFail {
type: typeof DELETE_FILTER_SET_FAIL;
}
export const UPDATE_FILTER_SET_BEGIN = 'UPDATE_FILTER_SET_BEGIN';
export interface UpdateFilterSetBegin {
type: typeof UPDATE_FILTER_SET_BEGIN;
}
export const UPDATE_FILTER_SET_COMPLETE = 'UPDATE_FILTER_SET_COMPLETE';
export interface UpdateFilterSetComplete {
type: typeof UPDATE_FILTER_SET_COMPLETE;
filterSet: FilterSet;
}
export const UPDATE_FILTER_SET_FAIL = 'UPDATE_FILTER_SET_FAIL';
export interface UpdateFilterSetFail {
type: typeof UPDATE_FILTER_SET_FAIL;
} }
export const setFilterConfiguration = ( export const setFilterConfiguration = (
@ -161,66 +201,138 @@ export interface SetBootstrapData {
data: BootstrapData; data: BootstrapData;
} }
export const setFilterSetsConfiguration = ( export const getFilterSets = () => async (
filterSetsConfig: FilterSet[], dispatch: Dispatch,
) => async (dispatch: Dispatch, getState: () => any) => { getState: () => RootState,
dispatch({ ) => {
type: SET_FILTER_SETS_CONFIG_BEGIN, const dashboardId = getState().dashboardInfo.id;
filterSetsConfig, const fetchFilterSets = makeApi<
}); null,
const { id, metadata } = getState().dashboardInfo; {
count: number;
// TODO extract this out when makeApi supports url parameters ids: number[];
const updateDashboard = makeApi< result: FilterSetFullData[];
Partial<DashboardInfo>, }
{ result: DashboardInfo }
>({ >({
method: 'PUT', method: 'GET',
endpoint: `/api/v1/dashboard/${id}`, endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
}); });
try { dispatch({
const response = await updateDashboard({ type: SET_FILTER_SETS_BEGIN,
json_metadata: JSON.stringify({ });
...metadata,
filter_sets_configuration: filterSetsConfig, const response = await fetchFilterSets(null);
}),
}); dispatch({
const newMetadata = JSON.parse(response.result.json_metadata); type: SET_FILTER_SETS_COMPLETE,
dispatch( filterSets: response.ids.map((id, i) => ({
dashboardInfoChanged({ ...response.result[i].params,
metadata: newMetadata, id,
}), name: response.result[i].name,
); })),
dispatch({ });
type: SET_FILTER_SETS_CONFIG_COMPLETE,
filterSetsConfig: newMetadata?.filter_sets_configuration,
});
} catch (err) {
dispatch({ type: SET_FILTER_SETS_CONFIG_FAIL, filterSetsConfig });
}
}; };
export const SAVE_FILTER_SETS = 'SAVE_FILTER_SETS'; export const createFilterSet = (filterSet: Omit<FilterSet, 'id'>) => async (
export interface SaveFilterSets { dispatch: Function,
type: typeof SAVE_FILTER_SETS; getState: () => RootState,
name: string; ) => {
dataMask: Pick<DataMaskStateWithId, DataMaskType.NativeFilters>; const dashboardId = getState().dashboardInfo.id;
filtersSetId: string; const postFilterSets = makeApi<
} Partial<FilterSetFullData & { json_metadata: any }>,
{
count: number;
ids: number[];
result: FilterSetFullData[];
}
>({
method: 'POST',
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
});
export function saveFilterSets( dispatch({
name: string, type: CREATE_FILTER_SET_BEGIN,
filtersSetId: string, });
dataMask: Pick<DataMaskStateWithId, DataMaskType.NativeFilters>,
): SaveFilterSets { const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & { name?: string } = {
return { ...filterSet,
type: SAVE_FILTER_SETS,
name,
filtersSetId,
dataMask,
}; };
}
delete serverFilterSet.name;
await postFilterSets({
name: filterSet.name,
owner_type: 'Dashboard',
owner_id: dashboardId,
json_metadata: JSON.stringify(serverFilterSet),
});
dispatch({
type: CREATE_FILTER_SET_COMPLETE,
});
dispatch(getFilterSets());
};
export const updateFilterSet = (filterSet: FilterSet) => async (
dispatch: Function,
getState: () => RootState,
) => {
const dashboardId = getState().dashboardInfo.id;
const postFilterSets = makeApi<
Partial<FilterSetFullData & { json_metadata: any }>,
{}
>({
method: 'PUT',
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSet.id}`,
});
dispatch({
type: UPDATE_FILTER_SET_BEGIN,
});
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & {
name?: string;
id?: number;
} = {
...filterSet,
};
delete serverFilterSet.id;
delete serverFilterSet.name;
await postFilterSets({
name: filterSet.name,
json_metadata: JSON.stringify(serverFilterSet),
});
dispatch({
type: UPDATE_FILTER_SET_COMPLETE,
});
dispatch(getFilterSets());
};
export const deleteFilterSet = (filterSetId: number) => async (
dispatch: Function,
getState: () => RootState,
) => {
const dashboardId = getState().dashboardInfo.id;
const deleteFilterSets = makeApi<{}, {}>({
method: 'DELETE',
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSetId}`,
});
dispatch({
type: DELETE_FILTER_SET_BEGIN,
});
await deleteFilterSets({});
dispatch({
type: DELETE_FILTER_SET_COMPLETE,
});
dispatch(getFilterSets());
};
export const SET_FOCUSED_NATIVE_FILTER = 'SET_FOCUSED_NATIVE_FILTER'; export const SET_FOCUSED_NATIVE_FILTER = 'SET_FOCUSED_NATIVE_FILTER';
export interface SetFocusedNativeFilter { export interface SetFocusedNativeFilter {
@ -248,11 +360,19 @@ export type AnyFilterAction =
| SetFilterConfigBegin | SetFilterConfigBegin
| SetFilterConfigComplete | SetFilterConfigComplete
| SetFilterConfigFail | SetFilterConfigFail
| SetFilterSetsConfigBegin | SetFilterSetsBegin
| SetFilterSetsConfigComplete | SetFilterSetsComplete
| SetFilterSetsConfigFail | SetFilterSetsFail
| SetInScopeStatusOfFilters | SetInScopeStatusOfFilters
| SaveFilterSets
| SetBootstrapData | SetBootstrapData
| SetFocusedNativeFilter | SetFocusedNativeFilter
| UnsetFocusedNativeFilter; | UnsetFocusedNativeFilter
| CreateFilterSetBegin
| CreateFilterSetComplete
| CreateFilterSetFail
| DeleteFilterSetBegin
| DeleteFilterSetComplete
| DeleteFilterSetFail
| UpdateFilterSetBegin
| UpdateFilterSetComplete
| UpdateFilterSetFail;

View File

@ -24,7 +24,7 @@ import { Provider } from 'react-redux';
import EditSection, { EditSectionProps } from './EditSection'; import EditSection, { EditSectionProps } from './EditSection';
const createProps = () => ({ const createProps = () => ({
filterSetId: 'set-id', filterSetId: 1,
dataMaskSelected: { dataMaskSelected: {
DefaultsID: { DefaultsID: {
filterState: { filterState: {

View File

@ -21,7 +21,7 @@ import { HandlerFunction, styled, t } from '@superset-ui/core';
import { Typography, Tooltip } from 'src/common/components'; import { Typography, Tooltip } from 'src/common/components';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters'; import { updateFilterSet } from 'src/dashboard/actions/nativeFilters';
import { DataMaskState } from 'src/dataMask/types'; import { DataMaskState } from 'src/dataMask/types';
import { WarningOutlined } from '@ant-design/icons'; import { WarningOutlined } from '@ant-design/icons';
import { ActionButtons } from './Footer'; import { ActionButtons } from './Footer';
@ -60,7 +60,7 @@ const ActionButton = styled.div<{ disabled?: boolean }>`
`; `;
export type EditSectionProps = { export type EditSectionProps = {
filterSetId: string; filterSetId: number;
dataMaskSelected: DataMaskState; dataMaskSelected: DataMaskState;
onCancel: HandlerFunction; onCancel: HandlerFunction;
disabled: boolean; disabled: boolean;
@ -89,17 +89,12 @@ const EditSection: FC<EditSectionProps> = ({
const handleSave = () => { const handleSave = () => {
dispatch( dispatch(
setFilterSetsConfiguration( updateFilterSet({
filterSetFilterValues.map(filterSet => { id: filterSetId,
const newFilterSet = { name: filterSetName,
...filterSet, nativeFilters: filters,
name: filterSetName, dataMask: { ...dataMaskApplied },
nativeFilters: filters, }),
dataMask: { ...dataMaskApplied },
};
return filterSetId === filterSet.id ? newFilterSet : filterSet;
}),
),
); );
onCancel(); onCancel();
}; };

View File

@ -21,10 +21,11 @@ import { render, screen } from 'spec/helpers/testing-library';
import { mockStore } from 'spec/fixtures/mockStore'; import { mockStore } from 'spec/fixtures/mockStore';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import FilterSets, { FilterSetsProps } from '.'; import FilterSets, { FilterSetsProps } from '.';
import { TabIds } from '../utils';
const createProps = () => ({ const createProps = () => ({
disabled: false, disabled: false,
isFilterSetChanged: false, tab: TabIds.FilterSets,
dataMaskSelected: { dataMaskSelected: {
DefaultsID: { DefaultsID: {
filterState: { filterState: {

View File

@ -86,9 +86,13 @@ const FiltersHeader: FC<FiltersHeaderProps> = ({ dataMask, filterSet }) => {
const getFilterRow = ({ id, name }: { id: string; name: string }) => { const getFilterRow = ({ id, name }: { id: string; name: string }) => {
const changedFilter = const changedFilter =
filterSet && filterSet &&
!areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id], { !areObjectsEqual(
ignoreUndefined: true, filters[id]?.controlValues,
}); filterSet?.nativeFilters?.[id]?.controlValues,
{
ignoreUndefined: true,
},
);
const removedFilter = !Object.keys(filters).includes(id); const removedFilter = !Object.keys(filters).includes(id);
return ( return (

View File

@ -17,27 +17,30 @@
* under the License. * under the License.
*/ */
import React, { useEffect, useState, MouseEvent } from 'react'; import React, { useEffect, useState } from 'react';
import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core'; import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { DataMaskState, DataMaskWithId } from 'src/dataMask/types'; import { DataMaskState, DataMaskWithId } from 'src/dataMask/types';
import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters'; import {
createFilterSet,
deleteFilterSet,
updateFilterSet,
} from 'src/dashboard/actions/nativeFilters';
import { Filters, FilterSet } from 'src/dashboard/reducers/types'; import { Filters, FilterSet } from 'src/dashboard/reducers/types';
import { areObjectsEqual } from 'src/reduxUtils'; import { areObjectsEqual } from 'src/reduxUtils';
import { findExistingFilterSet, generateFiltersSetId } from './utils'; import { findExistingFilterSet } from './utils';
import { Filter } from '../../types'; import { Filter } from '../../types';
import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state'; import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state';
import Footer from './Footer'; import Footer from './Footer';
import FilterSetUnit from './FilterSetUnit'; import FilterSetUnit from './FilterSetUnit';
import { getFilterBarTestId } from '..'; import { getFilterBarTestId } from '..';
import { TabIds } from '../utils';
const FilterSetsWrapper = styled.div` const FilterSetsWrapper = styled.div`
display: grid; display: grid;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding: ${({ theme }) => theme.gridUnit * 2}px
${({ theme }) => theme.gridUnit * 4}px;
& button.superset-button { & button.superset-button {
margin-left: 0; margin-left: 0;
@ -58,7 +61,7 @@ const FilterSetUnitWrapper = styled.div<{
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-gap: ${theme.gridUnit}px; grid-gap: ${theme.gridUnit}px;
border-bottom: 1px solid ${theme.colors.grayscale.light2}; border-bottom: 1px solid ${theme.colors.grayscale.light2};
padding: ${theme.gridUnit * 2}px 0px}; padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 4}px;
cursor: ${!onClick ? 'auto' : 'pointer'}; cursor: ${!onClick ? 'auto' : 'pointer'};
background: ${selected ? theme.colors.primary.light5 : 'transparent'}; background: ${selected ? theme.colors.primary.light5 : 'transparent'};
`} `}
@ -66,9 +69,9 @@ const FilterSetUnitWrapper = styled.div<{
export type FilterSetsProps = { export type FilterSetsProps = {
disabled: boolean; disabled: boolean;
isFilterSetChanged: boolean; tab: TabIds;
dataMaskSelected: DataMaskState; dataMaskSelected: DataMaskState;
onEditFilterSet: (id: string) => void; onEditFilterSet: (id: number) => void;
onFilterSelectionChange: ( onFilterSelectionChange: (
filter: Pick<Filter, 'id'> & Partial<Filter>, filter: Pick<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>, dataMask: Partial<DataMask>,
@ -82,7 +85,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
onEditFilterSet, onEditFilterSet,
disabled, disabled,
onFilterSelectionChange, onFilterSelectionChange,
isFilterSetChanged, tab,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME); const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
@ -93,11 +96,15 @@ const FilterSets: React.FC<FilterSetsProps> = ({
const filters = useFilters(); const filters = useFilters();
const filterValues = Object.values(filters) as Filter[]; const filterValues = Object.values(filters) as Filter[];
const [selectedFiltersSetId, setSelectedFiltersSetId] = useState< const [selectedFiltersSetId, setSelectedFiltersSetId] = useState<
string | null number | null
>(null); >(null);
useEffect(() => { useEffect(() => {
if (isFilterSetChanged) { if (tab === TabIds.AllFilters) {
return;
}
if (!filterSetFilterValues.length) {
setSelectedFiltersSetId(null);
return; return;
} }
@ -105,34 +112,35 @@ const FilterSets: React.FC<FilterSetsProps> = ({
dataMaskSelected, dataMaskSelected,
filterSetFilterValues, filterSetFilterValues,
}); });
setSelectedFiltersSetId(foundFilterSet?.id ?? null); setSelectedFiltersSetId(foundFilterSet?.id ?? null);
}, [isFilterSetChanged, dataMaskSelected, filterSetFilterValues]); }, [tab, dataMaskSelected, filterSetFilterValues]);
const isFilterMissingOrContainsInvalidMetadata = ( const isFilterMissingOrContainsInvalidMetadata = (
id: string, id: string,
filterSet?: FilterSet, filterSet?: FilterSet,
) => ) =>
!filterValues.find(filter => filter?.id === id) || !filterValues.find(filter => filter?.id === id) ||
!areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id], { !areObjectsEqual(
ignoreUndefined: true, filters[id]?.controlValues,
}); filterSet?.nativeFilters?.[id]?.controlValues,
{
ignoreUndefined: true,
},
);
const takeFilterSet = (id: string, target?: HTMLElement) => { const takeFilterSet = (id: number, event?: MouseEvent) => {
const ignoreSelectorHeader = 'ant-collapse-header'; const localTarget = event?.target as HTMLDivElement;
const ignoreSelectorDropdown = 'ant-dropdown-menu-item'; if (localTarget) {
if ( const parent = localTarget.closest(
target?.classList.contains(ignoreSelectorHeader) || `[data-anchor=${getFilterBarTestId('filter-set-wrapper', true)}]`,
target?.classList.contains(ignoreSelectorDropdown) || );
target?.parentElement?.classList.contains(ignoreSelectorHeader) || if (
target?.parentElement?.parentElement?.classList.contains( parent?.querySelector('.ant-collapse-header')?.contains(localTarget) ||
ignoreSelectorHeader, localTarget?.closest('.ant-dropdown')
) || ) {
target?.parentElement?.parentElement?.parentElement?.classList.contains( return;
ignoreSelectorHeader, }
)
) {
// We don't want select filter set when user expand filters
return;
} }
setSelectedFiltersSetId(id); setSelectedFiltersSetId(id);
if (!id) { if (!id) {
@ -152,7 +160,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
); );
}; };
const handleRebuild = (id: string) => { const handleRebuild = (id: number) => {
const filterSet = filterSets[id]; const filterSet = filterSets[id];
// We need remove invalid filters from filter set // We need remove invalid filters from filter set
const newFilters = Object.values(filterSet?.dataMask ?? {}) const newFilters = Object.values(filterSet?.dataMask ?? {})
@ -179,29 +187,16 @@ const FilterSets: React.FC<FilterSetsProps> = ({
{}, {},
), ),
}; };
dispatch( dispatch(updateFilterSet(updatedFilterSet));
setFilterSetsConfiguration(
filterSetFilterValues.map(filterSetIt => {
const isEquals = filterSetIt.id === updatedFilterSet.id;
return isEquals ? updatedFilterSet : filterSetIt;
}),
),
);
}; };
const handleEdit = (id: string) => { const handleEdit = (id: number) => {
takeFilterSet(id); takeFilterSet(id);
onEditFilterSet(id); onEditFilterSet(id);
}; };
const handleDeleteFilterSet = (filterSetId: string) => { const handleDeleteFilterSet = (filterSetId: number) => {
dispatch( dispatch(deleteFilterSet(filterSetId));
setFilterSetsConfiguration(
filterSetFilterValues.filter(
filtersSet => filtersSet.id !== filterSetId,
),
),
);
if (filterSetId === selectedFiltersSetId) { if (filterSetId === selectedFiltersSetId) {
setSelectedFiltersSetId(null); setSelectedFiltersSetId(null);
} }
@ -213,9 +208,8 @@ const FilterSets: React.FC<FilterSetsProps> = ({
}; };
const handleCreateFilterSet = () => { const handleCreateFilterSet = () => {
const newFilterSet: FilterSet = { const newFilterSet: Omit<FilterSet, 'id'> = {
name: filterSetName.trim(), name: filterSetName.trim(),
id: generateFiltersSetId(),
nativeFilters: filters, nativeFilters: filters,
dataMask: Object.keys(filters).reduce( dataMask: Object.keys(filters).reduce(
(prev, nextFilterId) => ({ (prev, nextFilterId) => ({
@ -225,9 +219,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
{}, {},
), ),
}; };
dispatch( dispatch(createFilterSet(newFilterSet));
setFilterSetsConfiguration([newFilterSet].concat(filterSetFilterValues)),
);
setEditMode(false); setEditMode(false);
setFilterSetName(DEFAULT_FILTER_SET_NAME); setFilterSetName(DEFAULT_FILTER_SET_NAME);
}; };
@ -255,9 +247,11 @@ const FilterSets: React.FC<FilterSetsProps> = ({
{filterSetFilterValues.map(filterSet => ( {filterSetFilterValues.map(filterSet => (
<FilterSetUnitWrapper <FilterSetUnitWrapper
{...getFilterBarTestId('filter-set-wrapper')} {...getFilterBarTestId('filter-set-wrapper')}
data-anchor={getFilterBarTestId('filter-set-wrapper', true)}
data-selected={filterSet.id === selectedFiltersSetId} data-selected={filterSet.id === selectedFiltersSetId}
onClick={(e: MouseEvent<HTMLElement>) => onClick={
takeFilterSet(filterSet.id, e.target as HTMLElement) (e =>
takeFilterSet(filterSet.id, e as MouseEvent)) as HandlerFunction
} }
key={filterSet.id} key={filterSet.id}
> >

View File

@ -28,7 +28,7 @@ test('Should find correct filter', () => {
const dataMaskSelected = createDataMaskSelected(); const dataMaskSelected = createDataMaskSelected();
const filterSetFilterValues: FilterSet[] = [ const filterSetFilterValues: FilterSet[] = [
{ {
id: 'id-01', id: 1,
name: 'name-01', name: 'name-01',
nativeFilters: {}, nativeFilters: {},
dataMask: { dataMask: {
@ -46,7 +46,7 @@ test('Should find correct filter', () => {
filterId: { id: 'filterId', filterState: { value: 'value-1' } }, filterId: { id: 'filterId', filterState: { value: 'value-1' } },
filterId2: { id: 'filterId2', filterState: { value: 'value-2' } }, filterId2: { id: 'filterId2', filterState: { value: 'value-2' } },
}, },
id: 'id-01', id: 1,
name: 'name-01', name: 'name-01',
nativeFilters: {}, nativeFilters: {},
}); });
@ -56,7 +56,7 @@ test('Should return undefined when nativeFilters has less values', () => {
const dataMaskSelected = createDataMaskSelected(); const dataMaskSelected = createDataMaskSelected();
const filterSetFilterValues = [ const filterSetFilterValues = [
{ {
id: 'id-01', id: 1,
name: 'name-01', name: 'name-01',
nativeFilters: {}, nativeFilters: {},
dataMask: { dataMask: {
@ -75,7 +75,7 @@ test('Should return undefined when nativeFilters has different values', () => {
const dataMaskSelected = createDataMaskSelected(); const dataMaskSelected = createDataMaskSelected();
const filterSetFilterValues: FilterSet[] = [ const filterSetFilterValues: FilterSet[] = [
{ {
id: 'id-01', id: 1,
name: 'name-01', name: 'name-01',
nativeFilters: {}, nativeFilters: {},
dataMask: { dataMask: {
@ -95,7 +95,7 @@ test('Should return undefined when dataMask:{}', () => {
const dataMaskSelected = createDataMaskSelected(); const dataMaskSelected = createDataMaskSelected();
const filterSetFilterValues = [ const filterSetFilterValues = [
{ {
id: 'id-01', id: 1,
name: 'name-01', name: 'name-01',
nativeFilters: {}, nativeFilters: {},
dataMask: {}, dataMask: {},
@ -112,7 +112,7 @@ test('Should return undefined when dataMask is empty}', () => {
const dataMaskSelected = createDataMaskSelected(); const dataMaskSelected = createDataMaskSelected();
const filterSetFilterValues: FilterSet[] = [ const filterSetFilterValues: FilterSet[] = [
{ {
id: 'id-01', id: 1,
name: 'name-01', name: 'name-01',
nativeFilters: {}, nativeFilters: {},
dataMask: {}, dataMask: {},

View File

@ -58,6 +58,7 @@ export const findExistingFilterSet = ({
const isEqual = areObjectsEqual( const isEqual = areObjectsEqual(
filterFromSelectedFilters.filterState, filterFromSelectedFilters.filterState,
dataMaskFromFilterSet?.[id]?.filterState, dataMaskFromFilterSet?.[id]?.filterState,
{ ignoreUndefined: true, ignoreNull: true },
); );
const hasSamePropsNumber = const hasSamePropsNumber =
dataMaskSelectedEntries.length === dataMaskSelectedEntries.length ===

View File

@ -150,7 +150,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
}) => { }) => {
const history = useHistory(); const history = useHistory();
const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask(); const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
const [editFilterSetId, setEditFilterSetId] = useState<string | null>(null); const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null);
const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskStateWithId>( const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskStateWithId>(
dataMaskApplied, dataMaskApplied,
); );
@ -161,13 +161,11 @@ const FilterBar: React.FC<FiltersBarProps> = ({
const filters = useFilters(); const filters = useFilters();
const previousFilters = usePrevious(filters); const previousFilters = usePrevious(filters);
const filterValues = Object.values<Filter>(filters); const filterValues = Object.values<Filter>(filters);
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
const handleFilterSelectionChange = ( const handleFilterSelectionChange = (
filter: Pick<Filter, 'id'> & Partial<Filter>, filter: Pick<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>, dataMask: Partial<DataMask>,
) => { ) => {
setIsFilterSetChanged(tab !== TabIds.AllFilters);
setDataMaskSelected(draft => { setDataMaskSelected(draft => {
// force instant updating on initialization for filters with `requiredFirst` is true or instant filters // force instant updating on initialization for filters with `requiredFirst` is true or instant filters
if ( if (
@ -341,7 +339,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
onEditFilterSet={setEditFilterSetId} onEditFilterSet={setEditFilterSetId}
disabled={!isApplyDisabled} disabled={!isApplyDisabled}
dataMaskSelected={dataMaskSelected} dataMaskSelected={dataMaskSelected}
isFilterSetChanged={isFilterSetChanged} tab={tab}
onFilterSelectionChange={handleFilterSelectionChange} onFilterSelectionChange={handleFilterSelectionChange}
/> />
</Tabs.TabPane> </Tabs.TabPane>

View File

@ -16,8 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useEffect, useRef, FC } from 'react'; import React, { FC, useRef, useEffect } from 'react';
import { t } from '@superset-ui/core'; import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useToasts } from 'src/components/MessageToasts/withToasts'; import { useToasts } from 'src/components/MessageToasts/withToasts';
@ -31,6 +31,7 @@ import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
import { setDatasources } from 'src/dashboard/actions/datasources'; import { setDatasources } from 'src/dashboard/actions/datasources';
import injectCustomCss from 'src/dashboard/util/injectCustomCss'; import injectCustomCss from 'src/dashboard/util/injectCustomCss';
import setupPlugins from 'src/setup/setupPlugins'; import setupPlugins from 'src/setup/setupPlugins';
import { getFilterSets } from '../actions/nativeFilters';
setupPlugins(); setupPlugins();
const DashboardContainer = React.lazy( const DashboardContainer = React.lazy(
@ -66,6 +67,9 @@ const DashboardPage: FC = () => {
if (readyToRender && !isDashboardHydrated.current) { if (readyToRender && !isDashboardHydrated.current) {
isDashboardHydrated.current = true; isDashboardHydrated.current = true;
dispatch(hydrateDashboard(dashboard, charts)); dispatch(hydrateDashboard(dashboard, charts));
if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET)) {
dispatch(getFilterSets());
}
} }
useEffect(() => { useEffect(() => {

View File

@ -18,10 +18,9 @@
*/ */
import { import {
AnyFilterAction, AnyFilterAction,
SAVE_FILTER_SETS,
SET_FILTER_CONFIG_COMPLETE, SET_FILTER_CONFIG_COMPLETE,
SET_IN_SCOPE_STATUS_OF_FILTERS, SET_IN_SCOPE_STATUS_OF_FILTERS,
SET_FILTER_SETS_CONFIG_COMPLETE, SET_FILTER_SETS_COMPLETE,
SET_FOCUSED_NATIVE_FILTER, SET_FOCUSED_NATIVE_FILTER,
UNSET_FOCUSED_NATIVE_FILTER, UNSET_FOCUSED_NATIVE_FILTER,
} from 'src/dashboard/actions/nativeFilters'; } from 'src/dashboard/actions/nativeFilters';
@ -72,33 +71,20 @@ export default function nativeFilterReducer(
}, },
action: AnyFilterAction, action: AnyFilterAction,
) { ) {
const { filterSets } = state;
switch (action.type) { switch (action.type) {
case HYDRATE_DASHBOARD: case HYDRATE_DASHBOARD:
return { return {
filters: action.data.nativeFilters.filters, filters: action.data.nativeFilters.filters,
filterSets: action.data.nativeFilters.filterSets, filterSets: action.data.nativeFilters.filterSets,
}; };
case SAVE_FILTER_SETS:
return {
...state,
filterSets: {
...filterSets,
[action.filtersSetId]: {
id: action.filtersSetId,
name: action.name,
dataMask: action.dataMask,
},
},
};
case SET_FILTER_CONFIG_COMPLETE: case SET_FILTER_CONFIG_COMPLETE:
case SET_IN_SCOPE_STATUS_OF_FILTERS: case SET_IN_SCOPE_STATUS_OF_FILTERS:
return getInitialState({ filterConfig: action.filterConfig, state }); return getInitialState({ filterConfig: action.filterConfig, state });
case SET_FILTER_SETS_CONFIG_COMPLETE: case SET_FILTER_SETS_COMPLETE:
return getInitialState({ return getInitialState({
filterSetsConfig: action.filterSetsConfig, filterSetsConfig: action.filterSets,
state, state,
}); });

View File

@ -19,6 +19,7 @@
import componentTypes from 'src/dashboard/util/componentTypes'; import componentTypes from 'src/dashboard/util/componentTypes';
import { DataMaskStateWithId } from 'src/dataMask/types'; import { DataMaskStateWithId } from 'src/dataMask/types';
import { JsonObject } from '@superset-ui/core';
import { Filter, Scope } from '../components/nativeFilters/types'; import { Filter, Scope } from '../components/nativeFilters/types';
export enum Scoping { export enum Scoping {
@ -82,12 +83,25 @@ export type LayoutItem = {
}; };
export type FilterSet = { export type FilterSet = {
id: string; id: number;
name: string; name: string;
nativeFilters: Filters; nativeFilters: Filters;
dataMask: DataMaskStateWithId; dataMask: DataMaskStateWithId;
}; };
export type FilterSetFullData = {
changed_by_fk: string | null;
changed_on: string | null;
created_by_fk: string | null;
created_on: string | null;
dashboard_id: number;
description: string | null;
name: string;
owner_id: number;
owner_type: string;
params: JsonObject;
};
export type FilterSets = { export type FilterSets = {
[filtersSetId: string]: FilterSet; [filtersSetId: string]: FilterSet;
}; };

View File

@ -64,6 +64,7 @@ export type DashboardState = {
isRefreshing: boolean; isRefreshing: boolean;
hasUnsavedChanges: boolean; hasUnsavedChanges: boolean;
}; };
export type DashboardInfo = { export type DashboardInfo = {
id: number; id: number;
common: { common: {
@ -72,7 +73,9 @@ export type DashboardInfo = {
}; };
userId: string; userId: string;
dash_edit_perm: boolean; dash_edit_perm: boolean;
json_metadata: string;
metadata: { metadata: {
native_filter_configuration: JsonObject;
show_native_filters: boolean; show_native_filters: boolean;
chart_configuration: JsonObject; chart_configuration: JsonObject;
}; };

View File

@ -19,7 +19,7 @@
import shortid from 'shortid'; import shortid from 'shortid';
import { compose } from 'redux'; import { compose } from 'redux';
import persistState, { StorageAdapter } from 'redux-localstorage'; import persistState, { StorageAdapter } from 'redux-localstorage';
import { isEqual, omitBy, isUndefined } from 'lodash'; import { isEqual, omitBy, isUndefined, isNull } from 'lodash';
export function addToObject( export function addToObject(
state: Record<string, any>, state: Record<string, any>,
@ -177,13 +177,20 @@ export function areArraysShallowEqual(arr1: unknown[], arr2: unknown[]) {
export function areObjectsEqual( export function areObjectsEqual(
obj1: any, obj1: any,
obj2: any, obj2: any,
opts = { ignoreUndefined: false }, opts: {
ignoreUndefined?: boolean;
ignoreNull?: boolean;
} = { ignoreUndefined: false, ignoreNull: false },
) { ) {
let comp1 = obj1; let comp1 = obj1;
let comp2 = obj2; let comp2 = obj2;
if (opts.ignoreUndefined) { if (opts.ignoreUndefined) {
comp1 = omitBy(obj1, isUndefined); comp1 = omitBy(comp1, isUndefined);
comp2 = omitBy(obj2, isUndefined); comp2 = omitBy(comp2, isUndefined);
}
if (opts.ignoreNull) {
comp1 = omitBy(comp1, isNull);
comp2 = omitBy(comp2, isNull);
} }
return isEqual(comp1, comp2); return isEqual(comp1, comp2);
} }

View File

@ -24,17 +24,20 @@ type TestWithIdType<T> = T extends string ? string : { 'data-test': string };
export const testWithId = <T extends string | JsonObject = JsonObject>( export const testWithId = <T extends string | JsonObject = JsonObject>(
prefix?: string, prefix?: string,
idOnly = false, idOnly = false,
) => (id?: string): TestWithIdType<T> => { ) => (id?: string, localIdOnly = false): TestWithIdType<T> => {
const resultIdOnly = localIdOnly || idOnly;
if (!id && prefix) { if (!id && prefix) {
return (idOnly ? prefix : { 'data-test': prefix }) as TestWithIdType<T>; return (resultIdOnly
? prefix
: { 'data-test': prefix }) as TestWithIdType<T>;
} }
if (id && !prefix) { if (id && !prefix) {
return (idOnly ? id : { 'data-test': id }) as TestWithIdType<T>; return (resultIdOnly ? id : { 'data-test': id }) as TestWithIdType<T>;
} }
if (!id && !prefix) { if (!id && !prefix) {
console.warn('testWithId function has missed "prefix" and "id" params'); console.warn('testWithId function has missed "prefix" and "id" params');
return (idOnly ? '' : { 'data-test': '' }) as TestWithIdType<T>; return (resultIdOnly ? '' : { 'data-test': '' }) as TestWithIdType<T>;
} }
const newId = `${prefix}__${id}`; const newId = `${prefix}__${id}`;
return (idOnly ? newId : { 'data-test': newId }) as TestWithIdType<T>; return (resultIdOnly ? newId : { 'data-test': newId }) as TestWithIdType<T>;
}; };

View File

@ -381,6 +381,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
"ESCAPE_MARKDOWN_HTML": False, "ESCAPE_MARKDOWN_HTML": False,
"DASHBOARD_NATIVE_FILTERS": True, "DASHBOARD_NATIVE_FILTERS": True,
"DASHBOARD_CROSS_FILTERS": False, "DASHBOARD_CROSS_FILTERS": False,
# Feature is under active development and breaking changes are expected
"DASHBOARD_NATIVE_FILTERS_SET": False, "DASHBOARD_NATIVE_FILTERS_SET": False,
"DASHBOARD_FILTERS_EXPERIMENTAL": False, "DASHBOARD_FILTERS_EXPERIMENTAL": False,
"GLOBAL_ASYNC_QUERIES": False, "GLOBAL_ASYNC_QUERIES": False,