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 = {
filterSets: {
'set-id': {
id: 'DefaultsID',
'1': {
id: 1,
name: 'Set name',
nativeFilters: {},
dataMask: mockDataMaskInfo,

View File

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

View File

@ -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_CONFIG_COMPLETE =
'SET_FILTER_SETS_CONFIG_COMPLETE';
export interface SetFilterSetsConfigComplete {
type: typeof SET_FILTER_SETS_CONFIG_COMPLETE;
filterSetsConfig: FilterSet[];
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_CONFIG_FAIL = 'SET_FILTER_SETS_CONFIG_FAIL';
export interface SetFilterSetsConfigFail {
type: typeof SET_FILTER_SETS_CONFIG_FAIL;
filterSetsConfig: FilterSet[];
export const SET_FILTER_SETS_FAIL = 'SET_FILTER_SETS_FAIL';
export interface SetFilterSetsFail {
type: typeof SET_FILTER_SETS_FAIL;
}
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 = (
@ -161,66 +201,138 @@ export interface SetBootstrapData {
data: BootstrapData;
}
export const setFilterSetsConfiguration = (
filterSetsConfig: FilterSet[],
) => async (dispatch: Dispatch, getState: () => any) => {
dispatch({
type: SET_FILTER_SETS_CONFIG_BEGIN,
filterSetsConfig,
});
const { id, metadata } = getState().dashboardInfo;
// TODO extract this out when makeApi supports url parameters
const updateDashboard = makeApi<
Partial<DashboardInfo>,
{ result: DashboardInfo }
export const getFilterSets = () => async (
dispatch: Dispatch,
getState: () => RootState,
) => {
const dashboardId = getState().dashboardInfo.id;
const fetchFilterSets = makeApi<
null,
{
count: number;
ids: number[];
result: FilterSetFullData[];
}
>({
method: 'PUT',
endpoint: `/api/v1/dashboard/${id}`,
method: 'GET',
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: SET_FILTER_SETS_BEGIN,
});
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 SAVE_FILTER_SETS = 'SAVE_FILTER_SETS';
export interface SaveFilterSets {
type: typeof SAVE_FILTER_SETS;
name: string;
dataMask: Pick<DataMaskStateWithId, DataMaskType.NativeFilters>;
filtersSetId: string;
}
export const createFilterSet = (filterSet: Omit<FilterSet, 'id'>) => async (
dispatch: Function,
getState: () => RootState,
) => {
const dashboardId = getState().dashboardInfo.id;
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(
name: string,
filtersSetId: string,
dataMask: Pick<DataMaskStateWithId, DataMaskType.NativeFilters>,
): SaveFilterSets {
return {
type: SAVE_FILTER_SETS,
name,
filtersSetId,
dataMask,
dispatch({
type: CREATE_FILTER_SET_BEGIN,
});
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & { 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 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 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;

View File

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

View File

@ -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<EditSectionProps> = ({
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();
};

View File

@ -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: {

View File

@ -86,9 +86,13 @@ const FiltersHeader: FC<FiltersHeaderProps> = ({ 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 (

View File

@ -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<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>,
@ -82,7 +85,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
onEditFilterSet,
disabled,
onFilterSelectionChange,
isFilterSetChanged,
tab,
}) => {
const dispatch = useDispatch();
const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
@ -93,11 +96,15 @@ const FilterSets: React.FC<FilterSetsProps> = ({
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<FilterSetsProps> = ({
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<FilterSetsProps> = ({
);
};
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<FilterSetsProps> = ({
{},
),
};
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<FilterSetsProps> = ({
};
const handleCreateFilterSet = () => {
const newFilterSet: FilterSet = {
const newFilterSet: Omit<FilterSet, 'id'> = {
name: filterSetName.trim(),
id: generateFiltersSetId(),
nativeFilters: filters,
dataMask: Object.keys(filters).reduce(
(prev, nextFilterId) => ({
@ -225,9 +219,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
{},
),
};
dispatch(
setFilterSetsConfiguration([newFilterSet].concat(filterSetFilterValues)),
);
dispatch(createFilterSet(newFilterSet));
setEditMode(false);
setFilterSetName(DEFAULT_FILTER_SET_NAME);
};
@ -255,9 +247,11 @@ const FilterSets: React.FC<FilterSetsProps> = ({
{filterSetFilterValues.map(filterSet => (
<FilterSetUnitWrapper
{...getFilterBarTestId('filter-set-wrapper')}
data-anchor={getFilterBarTestId('filter-set-wrapper', true)}
data-selected={filterSet.id === selectedFiltersSetId}
onClick={(e: MouseEvent<HTMLElement>) =>
takeFilterSet(filterSet.id, e.target as HTMLElement)
onClick={
(e =>
takeFilterSet(filterSet.id, e as MouseEvent)) as HandlerFunction
}
key={filterSet.id}
>

View File

@ -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: {},

View File

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

View File

@ -150,7 +150,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
}) => {
const history = useHistory();
const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
const [editFilterSetId, setEditFilterSetId] = useState<string | null>(null);
const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null);
const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskStateWithId>(
dataMaskApplied,
);
@ -161,13 +161,11 @@ const FilterBar: React.FC<FiltersBarProps> = ({
const filters = useFilters();
const previousFilters = usePrevious(filters);
const filterValues = Object.values<Filter>(filters);
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
const handleFilterSelectionChange = (
filter: Pick<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>,
) => {
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<FiltersBarProps> = ({
onEditFilterSet={setEditFilterSetId}
disabled={!isApplyDisabled}
dataMaskSelected={dataMaskSelected}
isFilterSetChanged={isFilterSetChanged}
tab={tab}
onFilterSelectionChange={handleFilterSelectionChange}
/>
</Tabs.TabPane>

View File

@ -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(() => {

View File

@ -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,
});

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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<string, any>,
@ -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);
}

View File

@ -24,17 +24,20 @@ type TestWithIdType<T> = T extends string ? string : { 'data-test': string };
export const testWithId = <T extends string | JsonObject = JsonObject>(
prefix?: string,
idOnly = false,
) => (id?: string): TestWithIdType<T> => {
) => (id?: string, localIdOnly = false): TestWithIdType<T> => {
const resultIdOnly = localIdOnly || idOnly;
if (!id && prefix) {
return (idOnly ? prefix : { 'data-test': prefix }) as TestWithIdType<T>;
return (resultIdOnly
? prefix
: { 'data-test': prefix }) as TestWithIdType<T>;
}
if (id && !prefix) {
return (idOnly ? id : { 'data-test': id }) as TestWithIdType<T>;
return (resultIdOnly ? id : { 'data-test': id }) as TestWithIdType<T>;
}
if (!id && !prefix) {
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}`;
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,
"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,