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:
parent
40b88f04f6
commit
37944e18d6
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -280,7 +280,6 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
|||
|
||||
const nativeFilters = getInitialNativeFilterState({
|
||||
filterConfig: metadata?.native_filter_configuration || [],
|
||||
filterSetsConfig: metadata?.filter_sets_configuration || [],
|
||||
});
|
||||
|
||||
if (!metadata) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { Provider } from 'react-redux';
|
|||
import EditSection, { EditSectionProps } from './EditSection';
|
||||
|
||||
const createProps = () => ({
|
||||
filterSetId: 'set-id',
|
||||
filterSetId: 1,
|
||||
dataMaskSelected: {
|
||||
DefaultsID: {
|
||||
filterState: {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export const findExistingFilterSet = ({
|
|||
const isEqual = areObjectsEqual(
|
||||
filterFromSelectedFilters.filterState,
|
||||
dataMaskFromFilterSet?.[id]?.filterState,
|
||||
{ ignoreUndefined: true, ignoreNull: true },
|
||||
);
|
||||
const hasSamePropsNumber =
|
||||
dataMaskSelectedEntries.length ===
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue