diff --git a/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.ts b/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.ts index 1a309fdfc1a33..7feba639fd357 100644 --- a/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.ts +++ b/superset-frontend/spec/javascripts/dashboard/fixtures/mockNativeFilters.ts @@ -32,8 +32,8 @@ export const mockDataMaskInfo: DataMaskStateWithId = { export const nativeFiltersInfo: NativeFiltersState = { filterSets: { - 'set-id': { - id: 'DefaultsID', + '1': { + id: 1, name: 'Set name', nativeFilters: {}, dataMask: mockDataMaskInfo, diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 9c524e2d05be4..2868c74814c21 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -280,7 +280,6 @@ export const hydrateDashboard = (dashboardData, chartData) => ( const nativeFilters = getInitialNativeFilterState({ filterConfig: metadata?.native_filter_configuration || [], - filterSetsConfig: metadata?.filter_sets_configuration || [], }); if (!metadata) { diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 150b8857ae6e5..aee5ca14165ad 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -20,7 +20,6 @@ import { makeApi } from '@superset-ui/core'; import { Dispatch } from 'redux'; import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types'; -import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types'; import { SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, setDataMaskForFilterConfigComplete, @@ -28,17 +27,19 @@ import { import { HYDRATE_DASHBOARD } from './hydrate'; import { dashboardInfoChanged } from './dashboardInfo'; import { - DashboardInfo, Filters, FilterSet, + FilterSetFullData, FilterSets, } from '../reducers/types'; +import { DashboardInfo, RootState } from '../types'; export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN'; export interface SetFilterConfigBegin { type: typeof SET_FILTER_CONFIG_BEGIN; filterConfig: FilterConfiguration; } + export const SET_FILTER_CONFIG_COMPLETE = 'SET_FILTER_CONFIG_COMPLETE'; export interface SetFilterConfigComplete { type: typeof SET_FILTER_CONFIG_COMPLETE; @@ -54,21 +55,60 @@ export interface SetInScopeStatusOfFilters { type: typeof SET_IN_SCOPE_STATUS_OF_FILTERS; filterConfig: FilterConfiguration; } -export const SET_FILTER_SETS_CONFIG_BEGIN = 'SET_FILTER_SETS_CONFIG_BEGIN'; -export interface SetFilterSetsConfigBegin { - type: typeof SET_FILTER_SETS_CONFIG_BEGIN; - filterSetsConfig: FilterSet[]; +export const SET_FILTER_SETS_BEGIN = 'SET_FILTER_SETS_BEGIN'; +export interface SetFilterSetsBegin { + type: typeof SET_FILTER_SETS_BEGIN; +} +export const SET_FILTER_SETS_COMPLETE = 'SET_FILTER_SETS_COMPLETE'; +export interface SetFilterSetsComplete { + type: typeof SET_FILTER_SETS_COMPLETE; + filterSets: FilterSet[]; +} +export const SET_FILTER_SETS_FAIL = 'SET_FILTER_SETS_FAIL'; +export interface SetFilterSetsFail { + type: typeof SET_FILTER_SETS_FAIL; } -export const SET_FILTER_SETS_CONFIG_COMPLETE = - 'SET_FILTER_SETS_CONFIG_COMPLETE'; -export interface SetFilterSetsConfigComplete { - type: typeof SET_FILTER_SETS_CONFIG_COMPLETE; - 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 SET_FILTER_SETS_CONFIG_FAIL = 'SET_FILTER_SETS_CONFIG_FAIL'; -export interface SetFilterSetsConfigFail { - type: typeof SET_FILTER_SETS_CONFIG_FAIL; - filterSetsConfig: 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 = ( @@ -161,66 +201,138 @@ export interface SetBootstrapData { data: BootstrapData; } -export const setFilterSetsConfiguration = ( - filterSetsConfig: FilterSet[], -) => async (dispatch: Dispatch, getState: () => any) => { +export const getFilterSets = () => async ( + dispatch: Dispatch, + getState: () => RootState, +) => { + const dashboardId = getState().dashboardInfo.id; + const fetchFilterSets = makeApi< + null, + { + count: number; + ids: number[]; + result: FilterSetFullData[]; + } + >({ + method: 'GET', + endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`, + }); + dispatch({ - type: SET_FILTER_SETS_CONFIG_BEGIN, - filterSetsConfig, + type: SET_FILTER_SETS_BEGIN, }); - const { id, metadata } = getState().dashboardInfo; - // TODO extract this out when makeApi supports url parameters - const updateDashboard = makeApi< - Partial, - { result: DashboardInfo } + const response = await fetchFilterSets(null); + + dispatch({ + type: SET_FILTER_SETS_COMPLETE, + filterSets: response.ids.map((id, i) => ({ + ...response.result[i].params, + id, + name: response.result[i].name, + })), + }); +}; + +export const createFilterSet = (filterSet: Omit) => async ( + dispatch: Function, + getState: () => RootState, +) => { + const dashboardId = getState().dashboardInfo.id; + const postFilterSets = makeApi< + Partial, + { + count: number; + ids: number[]; + result: FilterSetFullData[]; + } >({ - method: 'PUT', - endpoint: `/api/v1/dashboard/${id}`, + method: 'POST', + endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`, }); - try { - const response = await updateDashboard({ - json_metadata: JSON.stringify({ - ...metadata, - filter_sets_configuration: filterSetsConfig, - }), - }); - const newMetadata = JSON.parse(response.result.json_metadata); - dispatch( - dashboardInfoChanged({ - metadata: newMetadata, - }), - ); - dispatch({ - type: SET_FILTER_SETS_CONFIG_COMPLETE, - filterSetsConfig: newMetadata?.filter_sets_configuration, - }); - } catch (err) { - dispatch({ type: SET_FILTER_SETS_CONFIG_FAIL, filterSetsConfig }); - } + dispatch({ + type: CREATE_FILTER_SET_BEGIN, + }); + + const serverFilterSet: Omit & { name?: string } = { + ...filterSet, + }; + + 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 SAVE_FILTER_SETS = 'SAVE_FILTER_SETS'; -export interface SaveFilterSets { - type: typeof SAVE_FILTER_SETS; - name: string; - dataMask: Pick; - filtersSetId: string; -} +export const updateFilterSet = (filterSet: FilterSet) => async ( + dispatch: Function, + getState: () => RootState, +) => { + const dashboardId = getState().dashboardInfo.id; + const postFilterSets = makeApi< + Partial, + {} + >({ + method: 'PUT', + endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSet.id}`, + }); -export function saveFilterSets( - name: string, - filtersSetId: string, - dataMask: Pick, -): SaveFilterSets { - return { - type: SAVE_FILTER_SETS, - name, - filtersSetId, - dataMask, + dispatch({ + type: UPDATE_FILTER_SET_BEGIN, + }); + + const serverFilterSet: Omit & { + 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 interface SetFocusedNativeFilter { @@ -248,11 +360,19 @@ export type AnyFilterAction = | SetFilterConfigBegin | SetFilterConfigComplete | SetFilterConfigFail - | SetFilterSetsConfigBegin - | SetFilterSetsConfigComplete - | SetFilterSetsConfigFail + | SetFilterSetsBegin + | SetFilterSetsComplete + | SetFilterSetsFail | SetInScopeStatusOfFilters - | SaveFilterSets | SetBootstrapData | SetFocusedNativeFilter - | UnsetFocusedNativeFilter; + | UnsetFocusedNativeFilter + | CreateFilterSetBegin + | CreateFilterSetComplete + | CreateFilterSetFail + | DeleteFilterSetBegin + | DeleteFilterSetComplete + | DeleteFilterSetFail + | UpdateFilterSetBegin + | UpdateFilterSetComplete + | UpdateFilterSetFail; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.test.tsx index 812b0af270f66..829b70cf3a7df 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.test.tsx @@ -24,7 +24,7 @@ import { Provider } from 'react-redux'; import EditSection, { EditSectionProps } from './EditSection'; const createProps = () => ({ - filterSetId: 'set-id', + filterSetId: 1, dataMaskSelected: { DefaultsID: { filterState: { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx index bd9f724af4a6e..5e78091f83c8c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx @@ -21,7 +21,7 @@ import { HandlerFunction, styled, t } from '@superset-ui/core'; import { Typography, Tooltip } from 'src/common/components'; import { useDispatch } from 'react-redux'; 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 { WarningOutlined } from '@ant-design/icons'; import { ActionButtons } from './Footer'; @@ -60,7 +60,7 @@ const ActionButton = styled.div<{ disabled?: boolean }>` `; export type EditSectionProps = { - filterSetId: string; + filterSetId: number; dataMaskSelected: DataMaskState; onCancel: HandlerFunction; disabled: boolean; @@ -89,17 +89,12 @@ const EditSection: FC = ({ const handleSave = () => { dispatch( - setFilterSetsConfiguration( - filterSetFilterValues.map(filterSet => { - const newFilterSet = { - ...filterSet, - name: filterSetName, - nativeFilters: filters, - dataMask: { ...dataMaskApplied }, - }; - return filterSetId === filterSet.id ? newFilterSet : filterSet; - }), - ), + updateFilterSet({ + id: filterSetId, + name: filterSetName, + nativeFilters: filters, + dataMask: { ...dataMaskApplied }, + }), ); onCancel(); }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.test.tsx index ef8f859274290..78ad4599933d3 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.test.tsx @@ -21,10 +21,11 @@ import { render, screen } from 'spec/helpers/testing-library'; import { mockStore } from 'spec/fixtures/mockStore'; import { Provider } from 'react-redux'; import FilterSets, { FilterSetsProps } from '.'; +import { TabIds } from '../utils'; const createProps = () => ({ disabled: false, - isFilterSetChanged: false, + tab: TabIds.FilterSets, dataMaskSelected: { DefaultsID: { filterState: { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx index d63033142f8a4..cd451e9f621c7 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx @@ -86,9 +86,13 @@ const FiltersHeader: FC = ({ dataMask, filterSet }) => { const getFilterRow = ({ id, name }: { id: string; name: string }) => { const changedFilter = filterSet && - !areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id], { - ignoreUndefined: true, - }); + !areObjectsEqual( + filters[id]?.controlValues, + filterSet?.nativeFilters?.[id]?.controlValues, + { + ignoreUndefined: true, + }, + ); const removedFilter = !Object.keys(filters).includes(id); return ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx index 6f6b3d8c8a4ff..09a6304225f24 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx @@ -17,27 +17,30 @@ * 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 { useDispatch } from 'react-redux'; 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 { areObjectsEqual } from 'src/reduxUtils'; -import { findExistingFilterSet, generateFiltersSetId } from './utils'; +import { findExistingFilterSet } from './utils'; import { Filter } from '../../types'; import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state'; import Footer from './Footer'; import FilterSetUnit from './FilterSetUnit'; import { getFilterBarTestId } from '..'; +import { TabIds } from '../utils'; const FilterSetsWrapper = styled.div` display: grid; align-items: center; justify-content: center; grid-template-columns: 1fr; - padding: ${({ theme }) => theme.gridUnit * 2}px - ${({ theme }) => theme.gridUnit * 4}px; & button.superset-button { margin-left: 0; @@ -58,7 +61,7 @@ const FilterSetUnitWrapper = styled.div<{ grid-template-columns: 1fr; grid-gap: ${theme.gridUnit}px; 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'}; background: ${selected ? theme.colors.primary.light5 : 'transparent'}; `} @@ -66,9 +69,9 @@ const FilterSetUnitWrapper = styled.div<{ export type FilterSetsProps = { disabled: boolean; - isFilterSetChanged: boolean; + tab: TabIds; dataMaskSelected: DataMaskState; - onEditFilterSet: (id: string) => void; + onEditFilterSet: (id: number) => void; onFilterSelectionChange: ( filter: Pick & Partial, dataMask: Partial, @@ -82,7 +85,7 @@ const FilterSets: React.FC = ({ onEditFilterSet, disabled, onFilterSelectionChange, - isFilterSetChanged, + tab, }) => { const dispatch = useDispatch(); const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME); @@ -93,11 +96,15 @@ const FilterSets: React.FC = ({ const filters = useFilters(); const filterValues = Object.values(filters) as Filter[]; const [selectedFiltersSetId, setSelectedFiltersSetId] = useState< - string | null + number | null >(null); useEffect(() => { - if (isFilterSetChanged) { + if (tab === TabIds.AllFilters) { + return; + } + if (!filterSetFilterValues.length) { + setSelectedFiltersSetId(null); return; } @@ -105,34 +112,35 @@ const FilterSets: React.FC = ({ dataMaskSelected, filterSetFilterValues, }); + setSelectedFiltersSetId(foundFilterSet?.id ?? null); - }, [isFilterSetChanged, dataMaskSelected, filterSetFilterValues]); + }, [tab, dataMaskSelected, filterSetFilterValues]); const isFilterMissingOrContainsInvalidMetadata = ( id: string, filterSet?: FilterSet, ) => !filterValues.find(filter => filter?.id === id) || - !areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id], { - ignoreUndefined: true, - }); + !areObjectsEqual( + filters[id]?.controlValues, + filterSet?.nativeFilters?.[id]?.controlValues, + { + ignoreUndefined: true, + }, + ); - const takeFilterSet = (id: string, target?: HTMLElement) => { - const ignoreSelectorHeader = 'ant-collapse-header'; - const ignoreSelectorDropdown = 'ant-dropdown-menu-item'; - if ( - target?.classList.contains(ignoreSelectorHeader) || - target?.classList.contains(ignoreSelectorDropdown) || - target?.parentElement?.classList.contains(ignoreSelectorHeader) || - target?.parentElement?.parentElement?.classList.contains( - ignoreSelectorHeader, - ) || - target?.parentElement?.parentElement?.parentElement?.classList.contains( - ignoreSelectorHeader, - ) - ) { - // We don't want select filter set when user expand filters - return; + const takeFilterSet = (id: number, event?: MouseEvent) => { + const localTarget = event?.target as HTMLDivElement; + if (localTarget) { + const parent = localTarget.closest( + `[data-anchor=${getFilterBarTestId('filter-set-wrapper', true)}]`, + ); + if ( + parent?.querySelector('.ant-collapse-header')?.contains(localTarget) || + localTarget?.closest('.ant-dropdown') + ) { + return; + } } setSelectedFiltersSetId(id); if (!id) { @@ -152,7 +160,7 @@ const FilterSets: React.FC = ({ ); }; - const handleRebuild = (id: string) => { + const handleRebuild = (id: number) => { const filterSet = filterSets[id]; // We need remove invalid filters from filter set const newFilters = Object.values(filterSet?.dataMask ?? {}) @@ -179,29 +187,16 @@ const FilterSets: React.FC = ({ {}, ), }; - dispatch( - setFilterSetsConfiguration( - filterSetFilterValues.map(filterSetIt => { - const isEquals = filterSetIt.id === updatedFilterSet.id; - return isEquals ? updatedFilterSet : filterSetIt; - }), - ), - ); + dispatch(updateFilterSet(updatedFilterSet)); }; - const handleEdit = (id: string) => { + const handleEdit = (id: number) => { takeFilterSet(id); onEditFilterSet(id); }; - const handleDeleteFilterSet = (filterSetId: string) => { - dispatch( - setFilterSetsConfiguration( - filterSetFilterValues.filter( - filtersSet => filtersSet.id !== filterSetId, - ), - ), - ); + const handleDeleteFilterSet = (filterSetId: number) => { + dispatch(deleteFilterSet(filterSetId)); if (filterSetId === selectedFiltersSetId) { setSelectedFiltersSetId(null); } @@ -213,9 +208,8 @@ const FilterSets: React.FC = ({ }; const handleCreateFilterSet = () => { - const newFilterSet: FilterSet = { + const newFilterSet: Omit = { name: filterSetName.trim(), - id: generateFiltersSetId(), nativeFilters: filters, dataMask: Object.keys(filters).reduce( (prev, nextFilterId) => ({ @@ -225,9 +219,7 @@ const FilterSets: React.FC = ({ {}, ), }; - dispatch( - setFilterSetsConfiguration([newFilterSet].concat(filterSetFilterValues)), - ); + dispatch(createFilterSet(newFilterSet)); setEditMode(false); setFilterSetName(DEFAULT_FILTER_SET_NAME); }; @@ -255,9 +247,11 @@ const FilterSets: React.FC = ({ {filterSetFilterValues.map(filterSet => ( ) => - takeFilterSet(filterSet.id, e.target as HTMLElement) + onClick={ + (e => + takeFilterSet(filterSet.id, e as MouseEvent)) as HandlerFunction } key={filterSet.id} > diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/findExistingFilterSet.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/findExistingFilterSet.test.ts index ac0401bf6bb94..4c17ac86ad4a8 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/findExistingFilterSet.test.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/findExistingFilterSet.test.ts @@ -28,7 +28,7 @@ test('Should find correct filter', () => { const dataMaskSelected = createDataMaskSelected(); const filterSetFilterValues: FilterSet[] = [ { - id: 'id-01', + id: 1, name: 'name-01', nativeFilters: {}, dataMask: { @@ -46,7 +46,7 @@ test('Should find correct filter', () => { filterId: { id: 'filterId', filterState: { value: 'value-1' } }, filterId2: { id: 'filterId2', filterState: { value: 'value-2' } }, }, - id: 'id-01', + id: 1, name: 'name-01', nativeFilters: {}, }); @@ -56,7 +56,7 @@ test('Should return undefined when nativeFilters has less values', () => { const dataMaskSelected = createDataMaskSelected(); const filterSetFilterValues = [ { - id: 'id-01', + id: 1, name: 'name-01', nativeFilters: {}, dataMask: { @@ -75,7 +75,7 @@ test('Should return undefined when nativeFilters has different values', () => { const dataMaskSelected = createDataMaskSelected(); const filterSetFilterValues: FilterSet[] = [ { - id: 'id-01', + id: 1, name: 'name-01', nativeFilters: {}, dataMask: { @@ -95,7 +95,7 @@ test('Should return undefined when dataMask:{}', () => { const dataMaskSelected = createDataMaskSelected(); const filterSetFilterValues = [ { - id: 'id-01', + id: 1, name: 'name-01', nativeFilters: {}, dataMask: {}, @@ -112,7 +112,7 @@ test('Should return undefined when dataMask is empty}', () => { const dataMaskSelected = createDataMaskSelected(); const filterSetFilterValues: FilterSet[] = [ { - id: 'id-01', + id: 1, name: 'name-01', nativeFilters: {}, dataMask: {}, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/index.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/index.ts index 3da7a8d5aa7dd..368fbbbb2bdff 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/index.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils/index.ts @@ -58,6 +58,7 @@ export const findExistingFilterSet = ({ const isEqual = areObjectsEqual( filterFromSelectedFilters.filterState, dataMaskFromFilterSet?.[id]?.filterState, + { ignoreUndefined: true, ignoreNull: true }, ); const hasSamePropsNumber = dataMaskSelectedEntries.length === diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx index bcb326df463fd..b0499eb4552b9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx @@ -150,7 +150,7 @@ const FilterBar: React.FC = ({ }) => { const history = useHistory(); const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask(); - const [editFilterSetId, setEditFilterSetId] = useState(null); + const [editFilterSetId, setEditFilterSetId] = useState(null); const [dataMaskSelected, setDataMaskSelected] = useImmer( dataMaskApplied, ); @@ -161,13 +161,11 @@ const FilterBar: React.FC = ({ const filters = useFilters(); const previousFilters = usePrevious(filters); const filterValues = Object.values(filters); - const [isFilterSetChanged, setIsFilterSetChanged] = useState(false); const handleFilterSelectionChange = ( filter: Pick & Partial, dataMask: Partial, ) => { - setIsFilterSetChanged(tab !== TabIds.AllFilters); setDataMaskSelected(draft => { // force instant updating on initialization for filters with `requiredFirst` is true or instant filters if ( @@ -341,7 +339,7 @@ const FilterBar: React.FC = ({ onEditFilterSet={setEditFilterSetId} disabled={!isApplyDisabled} dataMaskSelected={dataMaskSelected} - isFilterSetChanged={isFilterSetChanged} + tab={tab} onFilterSelectionChange={handleFilterSelectionChange} /> diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index 4cb1e5b457396..6130aad7874da 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useEffect, useRef, FC } from 'react'; -import { t } from '@superset-ui/core'; +import React, { FC, useRef, useEffect } from 'react'; +import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; 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 injectCustomCss from 'src/dashboard/util/injectCustomCss'; import setupPlugins from 'src/setup/setupPlugins'; +import { getFilterSets } from '../actions/nativeFilters'; setupPlugins(); const DashboardContainer = React.lazy( @@ -66,6 +67,9 @@ const DashboardPage: FC = () => { if (readyToRender && !isDashboardHydrated.current) { isDashboardHydrated.current = true; dispatch(hydrateDashboard(dashboard, charts)); + if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET)) { + dispatch(getFilterSets()); + } } useEffect(() => { diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index 1ed96a64c958d..1521ec10c5121 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -18,10 +18,9 @@ */ import { AnyFilterAction, - SAVE_FILTER_SETS, SET_FILTER_CONFIG_COMPLETE, SET_IN_SCOPE_STATUS_OF_FILTERS, - SET_FILTER_SETS_CONFIG_COMPLETE, + SET_FILTER_SETS_COMPLETE, SET_FOCUSED_NATIVE_FILTER, UNSET_FOCUSED_NATIVE_FILTER, } from 'src/dashboard/actions/nativeFilters'; @@ -72,33 +71,20 @@ export default function nativeFilterReducer( }, action: AnyFilterAction, ) { - const { filterSets } = state; switch (action.type) { case HYDRATE_DASHBOARD: return { filters: action.data.nativeFilters.filters, 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_IN_SCOPE_STATUS_OF_FILTERS: return getInitialState({ filterConfig: action.filterConfig, state }); - case SET_FILTER_SETS_CONFIG_COMPLETE: + case SET_FILTER_SETS_COMPLETE: return getInitialState({ - filterSetsConfig: action.filterSetsConfig, + filterSetsConfig: action.filterSets, state, }); diff --git a/superset-frontend/src/dashboard/reducers/types.ts b/superset-frontend/src/dashboard/reducers/types.ts index b31079880b39d..5cef1a1fa742f 100644 --- a/superset-frontend/src/dashboard/reducers/types.ts +++ b/superset-frontend/src/dashboard/reducers/types.ts @@ -19,6 +19,7 @@ import componentTypes from 'src/dashboard/util/componentTypes'; import { DataMaskStateWithId } from 'src/dataMask/types'; +import { JsonObject } from '@superset-ui/core'; import { Filter, Scope } from '../components/nativeFilters/types'; export enum Scoping { @@ -82,12 +83,25 @@ export type LayoutItem = { }; export type FilterSet = { - id: string; + id: number; name: string; nativeFilters: Filters; 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 = { [filtersSetId: string]: FilterSet; }; diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 51b2b320e7984..6f3467693c02c 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -64,6 +64,7 @@ export type DashboardState = { isRefreshing: boolean; hasUnsavedChanges: boolean; }; + export type DashboardInfo = { id: number; common: { @@ -72,7 +73,9 @@ export type DashboardInfo = { }; userId: string; dash_edit_perm: boolean; + json_metadata: string; metadata: { + native_filter_configuration: JsonObject; show_native_filters: boolean; chart_configuration: JsonObject; }; diff --git a/superset-frontend/src/reduxUtils.ts b/superset-frontend/src/reduxUtils.ts index 605e7690ca1ad..245abc23e540f 100644 --- a/superset-frontend/src/reduxUtils.ts +++ b/superset-frontend/src/reduxUtils.ts @@ -19,7 +19,7 @@ import shortid from 'shortid'; import { compose } from 'redux'; import persistState, { StorageAdapter } from 'redux-localstorage'; -import { isEqual, omitBy, isUndefined } from 'lodash'; +import { isEqual, omitBy, isUndefined, isNull } from 'lodash'; export function addToObject( state: Record, @@ -177,13 +177,20 @@ export function areArraysShallowEqual(arr1: unknown[], arr2: unknown[]) { export function areObjectsEqual( obj1: any, obj2: any, - opts = { ignoreUndefined: false }, + opts: { + ignoreUndefined?: boolean; + ignoreNull?: boolean; + } = { ignoreUndefined: false, ignoreNull: false }, ) { let comp1 = obj1; let comp2 = obj2; if (opts.ignoreUndefined) { - comp1 = omitBy(obj1, isUndefined); - comp2 = omitBy(obj2, isUndefined); + comp1 = omitBy(comp1, isUndefined); + comp2 = omitBy(comp2, isUndefined); + } + if (opts.ignoreNull) { + comp1 = omitBy(comp1, isNull); + comp2 = omitBy(comp2, isNull); } return isEqual(comp1, comp2); } diff --git a/superset-frontend/src/utils/testUtils.ts b/superset-frontend/src/utils/testUtils.ts index de3dee83c5bc6..3c1957e30f653 100644 --- a/superset-frontend/src/utils/testUtils.ts +++ b/superset-frontend/src/utils/testUtils.ts @@ -24,17 +24,20 @@ type TestWithIdType = T extends string ? string : { 'data-test': string }; export const testWithId = ( prefix?: string, idOnly = false, -) => (id?: string): TestWithIdType => { +) => (id?: string, localIdOnly = false): TestWithIdType => { + const resultIdOnly = localIdOnly || idOnly; if (!id && prefix) { - return (idOnly ? prefix : { 'data-test': prefix }) as TestWithIdType; + return (resultIdOnly + ? prefix + : { 'data-test': prefix }) as TestWithIdType; } if (id && !prefix) { - return (idOnly ? id : { 'data-test': id }) as TestWithIdType; + return (resultIdOnly ? id : { 'data-test': id }) as TestWithIdType; } if (!id && !prefix) { console.warn('testWithId function has missed "prefix" and "id" params'); - return (idOnly ? '' : { 'data-test': '' }) as TestWithIdType; + return (resultIdOnly ? '' : { 'data-test': '' }) as TestWithIdType; } const newId = `${prefix}__${id}`; - return (idOnly ? newId : { 'data-test': newId }) as TestWithIdType; + return (resultIdOnly ? newId : { 'data-test': newId }) as TestWithIdType; }; diff --git a/superset/config.py b/superset/config.py index 838bbd4cccf80..343d8dfe8c88c 100644 --- a/superset/config.py +++ b/superset/config.py @@ -381,6 +381,7 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]: "ESCAPE_MARKDOWN_HTML": False, "DASHBOARD_NATIVE_FILTERS": True, "DASHBOARD_CROSS_FILTERS": False, + # Feature is under active development and breaking changes are expected "DASHBOARD_NATIVE_FILTERS_SET": False, "DASHBOARD_FILTERS_EXPERIMENTAL": False, "GLOBAL_ASYNC_QUERIES": False,