mirror of
https://github.com/apache/superset.git
synced 2024-09-18 19:49:37 -04:00
feat(filter-set): Filter set history (#13529)
* refactor(native-filters): move data mask to root reducer * refactor: update rest stuff for dataMask * refactor: add ownCrrentState to explore * fix: fix immer reducer * fix: merge with master * refactor: support explore dataMask * refactor: support explore dataMask * docs: add comment * refactor: remove json stringify * fix: fix failed cases * feat: filter bat buttons start * fix: fix CR notes * fix: fix cascade filters * fix: fix CR notes * refactor: add clear all * fix: fix CR notes * fix: fix CR notes * fix: fix CR notes * feat: buttons in filter bar * lint: update imports * feat: add tabs for filter sets * feat: add buttons to filter set * feat: first phase add filter sets * fix: undo FF * refactor: continue filter sets * fix: fix CR notes * refactor: header * fix: fix CR notes * fix: fix CR notes * refactor: continue filter sets * lint: fix lint * refactor: continue filter sets * fix: fix filter bar opening * refactor: continue filter sets * refactor: continue filter sets * refactor: continue filter sets * feat: filters sets history * feat: filters sets history * fix: filter set name * refactor: fix expand filters case * fix: fix CR notes * refactor: filter sets * fix: fix CR notes * refactor: filter sets
This commit is contained in:
parent
226dd4b907
commit
1d1a1cdc20
@ -26,7 +26,7 @@ import {
|
|||||||
SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
|
SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
|
||||||
} from 'src/dataMask/actions';
|
} from 'src/dataMask/actions';
|
||||||
import { dashboardInfoChanged } from './dashboardInfo';
|
import { dashboardInfoChanged } from './dashboardInfo';
|
||||||
import { FiltersSet } from '../reducers/types';
|
import { FilterSet } from '../reducers/types';
|
||||||
|
|
||||||
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
|
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
|
||||||
export interface SetFilterConfigBegin {
|
export interface SetFilterConfigBegin {
|
||||||
@ -46,18 +46,18 @@ export interface SetFilterConfigFail {
|
|||||||
export const SET_FILTER_SETS_CONFIG_BEGIN = 'SET_FILTER_SETS_CONFIG_BEGIN';
|
export const SET_FILTER_SETS_CONFIG_BEGIN = 'SET_FILTER_SETS_CONFIG_BEGIN';
|
||||||
export interface SetFilterSetsConfigBegin {
|
export interface SetFilterSetsConfigBegin {
|
||||||
type: typeof SET_FILTER_SETS_CONFIG_BEGIN;
|
type: typeof SET_FILTER_SETS_CONFIG_BEGIN;
|
||||||
filterSetsConfig: FiltersSet[];
|
filterSetsConfig: FilterSet[];
|
||||||
}
|
}
|
||||||
export const SET_FILTER_SETS_CONFIG_COMPLETE =
|
export const SET_FILTER_SETS_CONFIG_COMPLETE =
|
||||||
'SET_FILTER_SETS_CONFIG_COMPLETE';
|
'SET_FILTER_SETS_CONFIG_COMPLETE';
|
||||||
export interface SetFilterSetsConfigComplete {
|
export interface SetFilterSetsConfigComplete {
|
||||||
type: typeof SET_FILTER_SETS_CONFIG_COMPLETE;
|
type: typeof SET_FILTER_SETS_CONFIG_COMPLETE;
|
||||||
filterSetsConfig: FiltersSet[];
|
filterSetsConfig: FilterSet[];
|
||||||
}
|
}
|
||||||
export const SET_FILTER_SETS_CONFIG_FAIL = 'SET_FILTER_SETS_CONFIG_FAIL';
|
export const SET_FILTER_SETS_CONFIG_FAIL = 'SET_FILTER_SETS_CONFIG_FAIL';
|
||||||
export interface SetFilterSetsConfigFail {
|
export interface SetFilterSetsConfigFail {
|
||||||
type: typeof SET_FILTER_SETS_CONFIG_FAIL;
|
type: typeof SET_FILTER_SETS_CONFIG_FAIL;
|
||||||
filterSetsConfig: FiltersSet[];
|
filterSetsConfig: FilterSet[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardInfo {
|
interface DashboardInfo {
|
||||||
@ -110,7 +110,7 @@ export const setFilterConfiguration = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setFilterSetsConfiguration = (
|
export const setFilterSetsConfiguration = (
|
||||||
filterSetsConfig: FiltersSet[],
|
filterSetsConfig: FilterSet[],
|
||||||
) => async (dispatch: Dispatch, getState: () => any) => {
|
) => async (dispatch: Dispatch, getState: () => any) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SET_FILTER_SETS_CONFIG_BEGIN,
|
type: SET_FILTER_SETS_CONFIG_BEGIN,
|
||||||
|
@ -27,11 +27,7 @@ import Icon from 'src/components/Icon';
|
|||||||
import { Tabs } from 'src/common/components';
|
import { Tabs } from 'src/common/components';
|
||||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||||
import { updateDataMask } from 'src/dataMask/actions';
|
import { updateDataMask } from 'src/dataMask/actions';
|
||||||
import {
|
import { DataMaskUnit, DataMaskState } from 'src/dataMask/types';
|
||||||
DataMaskUnitWithId,
|
|
||||||
DataMaskUnit,
|
|
||||||
DataMaskState,
|
|
||||||
} from 'src/dataMask/types';
|
|
||||||
import { useImmer } from 'use-immer';
|
import { useImmer } from 'use-immer';
|
||||||
import { getInitialMask } from 'src/dataMask/reducer';
|
import { getInitialMask } from 'src/dataMask/reducer';
|
||||||
import { areObjectsEqual } from 'src/reduxUtils';
|
import { areObjectsEqual } from 'src/reduxUtils';
|
||||||
@ -40,13 +36,15 @@ import { Filter } from '../types';
|
|||||||
import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils';
|
import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils';
|
||||||
import CascadePopover from './CascadePopover';
|
import CascadePopover from './CascadePopover';
|
||||||
import FilterSets from './FilterSets/FilterSets';
|
import FilterSets from './FilterSets/FilterSets';
|
||||||
import { useFilters, useFilterSets } from './state';
|
import { useDataMask, useFilters, useFilterSets } from './state';
|
||||||
|
|
||||||
const barWidth = `250px`;
|
const barWidth = `250px`;
|
||||||
|
|
||||||
const BarWrapper = styled.div`
|
const BarWrapper = styled.div`
|
||||||
width: ${({ theme }) => theme.gridUnit * 8}px;
|
width: ${({ theme }) => theme.gridUnit * 8}px;
|
||||||
|
& .ant-tabs-top > .ant-tabs-nav {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
&.open {
|
&.open {
|
||||||
width: ${barWidth}; // arbitrary...
|
width: ${barWidth}; // arbitrary...
|
||||||
}
|
}
|
||||||
@ -158,7 +156,7 @@ const ActionButtons = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const FilterControls = styled.div`
|
const FilterControls = styled.div`
|
||||||
padding: 0 ${({ theme }) => theme.gridUnit * 4}px;
|
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -175,19 +173,17 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||||||
toggleFiltersBar,
|
toggleFiltersBar,
|
||||||
directPathToChild,
|
directPathToChild,
|
||||||
}) => {
|
}) => {
|
||||||
const [filterData, setFilterData] = useImmer<DataMaskUnit>({});
|
const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskUnit>({});
|
||||||
const [
|
const [
|
||||||
lastAppliedFilterData,
|
lastAppliedFilterData,
|
||||||
setLastAppliedFilterData,
|
setLastAppliedFilterData,
|
||||||
] = useImmer<DataMaskUnit>({});
|
] = useImmer<DataMaskUnit>({});
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const filterSets = useFilterSets();
|
const filterSets = useFilterSets();
|
||||||
const filterSetsArray = Object.values(filterSets);
|
const filterSetFilterValues = Object.values(filterSets);
|
||||||
const filters = useFilters();
|
const filters = useFilters();
|
||||||
const filtersArray = Object.values(filters);
|
const filterValues = Object.values(filters);
|
||||||
const dataMaskState = useSelector<any, DataMaskUnitWithId>(
|
const dataMaskApplied = useDataMask();
|
||||||
state => state.dataMask.nativeFilters ?? {},
|
|
||||||
);
|
|
||||||
const canEdit = useSelector<any, boolean>(
|
const canEdit = useSelector<any, boolean>(
|
||||||
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
|
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
|
||||||
);
|
);
|
||||||
@ -195,59 +191,59 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
const filterIds = Object.keys(filterData);
|
const filterIds = Object.keys(dataMaskSelected);
|
||||||
filterIds.forEach(filterId => {
|
filterIds.forEach(filterId => {
|
||||||
if (filterData[filterId]) {
|
if (dataMaskSelected[filterId]) {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateDataMask(filterId, {
|
updateDataMask(filterId, {
|
||||||
nativeFilters: filterData[filterId],
|
nativeFilters: dataMaskSelected[filterId],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setLastAppliedFilterData(() => filterData);
|
setLastAppliedFilterData(() => dataMaskSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const areFiltersInitialized = filtersArray.every(filterConfig =>
|
const areFiltersInitialized = filterValues.every(filterValue =>
|
||||||
areObjectsEqual(
|
areObjectsEqual(
|
||||||
filterConfig.defaultValue,
|
filterValue.defaultValue,
|
||||||
filterData[filterConfig.id]?.currentState?.value,
|
dataMaskSelected[filterValue.id]?.currentState?.value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (areFiltersInitialized) {
|
if (areFiltersInitialized) {
|
||||||
handleApply();
|
handleApply();
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
}, [filtersArray, filterData, isInitialized]);
|
}, [filterValues, dataMaskSelected, isInitialized]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filtersArray.length === 0 && filtersOpen) {
|
if (filterValues.length === 0 && filtersOpen) {
|
||||||
toggleFiltersBar(false);
|
toggleFiltersBar(false);
|
||||||
}
|
}
|
||||||
}, [filtersArray.length]);
|
}, [filterValues.length]);
|
||||||
|
|
||||||
const cascadeChildren = useMemo(
|
const cascadeChildren = useMemo(
|
||||||
() => mapParentFiltersToChildren(filtersArray),
|
() => mapParentFiltersToChildren(filterValues),
|
||||||
[filtersArray],
|
[filterValues],
|
||||||
);
|
);
|
||||||
|
|
||||||
const cascadeFilters = useMemo(() => {
|
const cascadeFilters = useMemo(() => {
|
||||||
const filtersWithValue = filtersArray.map(filter => ({
|
const filtersWithValue = filterValues.map(filter => ({
|
||||||
...filter,
|
...filter,
|
||||||
currentValue: filterData[filter.id]?.currentState?.value,
|
currentValue: dataMaskSelected[filter.id]?.currentState?.value,
|
||||||
}));
|
}));
|
||||||
return buildCascadeFiltersTree(filtersWithValue);
|
return buildCascadeFiltersTree(filtersWithValue);
|
||||||
}, [filtersArray, filterData]);
|
}, [filterValues, dataMaskSelected]);
|
||||||
|
|
||||||
const handleFilterSelectionChange = (
|
const handleFilterSelectionChange = (
|
||||||
filter: Pick<Filter, 'id'> & Partial<Filter>,
|
filter: Pick<Filter, 'id'> & Partial<Filter>,
|
||||||
dataMask: Partial<DataMaskState>,
|
dataMask: Partial<DataMaskState>,
|
||||||
) => {
|
) => {
|
||||||
setFilterData(draft => {
|
setDataMaskSelected(draft => {
|
||||||
const children = cascadeChildren[filter.id] || [];
|
const children = cascadeChildren[filter.id] || [];
|
||||||
// force instant updating on initialization or for parent filters
|
// force instant updating on initialization or for parent filters
|
||||||
if (filter.isInstant || children.length > 0) {
|
if (filter.isInstant || children.length > 0) {
|
||||||
@ -261,17 +257,17 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClearAll = () => {
|
const handleClearAll = () => {
|
||||||
filtersArray.forEach(filter => {
|
filterValues.forEach(filter => {
|
||||||
setFilterData(draft => {
|
setDataMaskSelected(draft => {
|
||||||
draft[filter.id] = getInitialMask(filter.id);
|
draft[filter.id] = getInitialMask(filter.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isClearAllDisabled = Object.values(dataMaskState).every(
|
const isClearAllDisabled = Object.values(dataMaskApplied).every(
|
||||||
filter =>
|
filter =>
|
||||||
filterData[filter.id]?.currentState?.value === null ||
|
dataMaskSelected[filter.id]?.currentState?.value === null ||
|
||||||
(!filterData[filter.id] && filter.currentState?.value === null),
|
(!dataMaskSelected[filter.id] && filter.currentState?.value === null),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getFilterControls = () => (
|
const getFilterControls = () => (
|
||||||
@ -293,7 +289,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isApplyDisabled =
|
const isApplyDisabled =
|
||||||
!isInitialized || areObjectsEqual(filterData, lastAppliedFilterData);
|
!isInitialized || areObjectsEqual(dataMaskSelected, lastAppliedFilterData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BarWrapper data-test="filter-bar" className={cx({ open: filtersOpen })}>
|
<BarWrapper data-test="filter-bar" className={cx({ open: filtersOpen })}>
|
||||||
@ -309,7 +305,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||||||
<span>{t('Filters')}</span>
|
<span>{t('Filters')}</span>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<FilterConfigurationLink
|
<FilterConfigurationLink
|
||||||
createNewOnOpen={filtersArray.length === 0}
|
createNewOnOpen={filterValues.length === 0}
|
||||||
>
|
>
|
||||||
<Icon name="edit" data-test="create-filter" />
|
<Icon name="edit" data-test="create-filter" />
|
||||||
</FilterConfigurationLink>
|
</FilterConfigurationLink>
|
||||||
@ -344,18 +340,18 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
>
|
>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
tab={t(`All Filters (${filtersArray.length})`)}
|
tab={t(`All Filters (${filterValues.length})`)}
|
||||||
key="allFilters"
|
key="allFilters"
|
||||||
>
|
>
|
||||||
{getFilterControls()}
|
{getFilterControls()}
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
tab={t(`Filter Sets (${filterSetsArray.length})`)}
|
tab={t(`Filter Sets (${filterSetFilterValues.length})`)}
|
||||||
key="filterSets"
|
key="filterSets"
|
||||||
>
|
>
|
||||||
<FilterSets
|
<FilterSets
|
||||||
disabled={!isApplyDisabled}
|
disabled={!isApplyDisabled}
|
||||||
dataMaskState={dataMaskState}
|
dataMaskSelected={dataMaskSelected}
|
||||||
onFilterSelectionChange={handleFilterSelectionChange}
|
onFilterSelectionChange={handleFilterSelectionChange}
|
||||||
/>
|
/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import { Typography, Dropdown, Menu } from 'src/common/components';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { FilterSet } from 'src/dashboard/reducers/types';
|
||||||
|
import { DataMaskUnitWithId } from 'src/dataMask/types';
|
||||||
|
import { CheckOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||||
|
import { HandlerFunction, styled, supersetTheme, t } from '@superset-ui/core';
|
||||||
|
import FiltersHeader from './FiltersHeader';
|
||||||
|
import { Filter } from '../../types';
|
||||||
|
|
||||||
|
const TitleText = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconsBlock = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-start;
|
||||||
|
& > * {
|
||||||
|
${({ theme }) => `padding-left: ${theme.gridUnit * 2}px`};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type FilterSetUnitProps = {
|
||||||
|
filters: Filter[];
|
||||||
|
editMode?: boolean;
|
||||||
|
isApplied?: boolean;
|
||||||
|
filterSet?: FilterSet;
|
||||||
|
filterSetName?: string;
|
||||||
|
dataMaskApplied: DataMaskUnitWithId;
|
||||||
|
setFilterSetName?: (name: string) => void;
|
||||||
|
onDelete?: HandlerFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterSetUnit: FC<FilterSetUnitProps> = ({
|
||||||
|
filters,
|
||||||
|
editMode,
|
||||||
|
setFilterSetName,
|
||||||
|
onDelete,
|
||||||
|
filterSetName,
|
||||||
|
dataMaskApplied,
|
||||||
|
filterSet,
|
||||||
|
isApplied,
|
||||||
|
}) => {
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item onClick={onDelete}>{t('Delete')}</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TitleText>
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
editable={{
|
||||||
|
editing: editMode,
|
||||||
|
icon: <span />,
|
||||||
|
onChange: setFilterSetName,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filterSet?.name ?? filterSetName}
|
||||||
|
</Typography.Text>
|
||||||
|
<IconsBlock>
|
||||||
|
{isApplied && (
|
||||||
|
<CheckOutlined
|
||||||
|
style={{ color: supersetTheme.colors.success.base }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<Dropdown
|
||||||
|
overlay={menu}
|
||||||
|
placement="bottomRight"
|
||||||
|
trigger={['click']}
|
||||||
|
>
|
||||||
|
<EllipsisOutlined
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</IconsBlock>
|
||||||
|
</TitleText>
|
||||||
|
<FiltersHeader
|
||||||
|
expanded={!filterSet}
|
||||||
|
dataMask={filterSet?.dataMask?.nativeFilters ?? dataMaskApplied}
|
||||||
|
filters={filters}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterSetUnit;
|
@ -16,32 +16,25 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { Select, Typography } from 'src/common/components';
|
|
||||||
import Button from 'src/components/Button';
|
import React, { useEffect, useState, MouseEvent } from 'react';
|
||||||
import React, { useState } from 'react';
|
import { HandlerFunction, styled, t } from '@superset-ui/core';
|
||||||
import { styled, t, tn } from '@superset-ui/core';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import {
|
import { DataMaskState, DataMaskUnit, MaskWithId } from 'src/dataMask/types';
|
||||||
DataMaskState,
|
|
||||||
DataMaskUnitWithId,
|
|
||||||
MaskWithId,
|
|
||||||
} from 'src/dataMask/types';
|
|
||||||
import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters';
|
import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters';
|
||||||
|
import { areObjectsEqual } from 'src/reduxUtils';
|
||||||
|
import { FilterSet } from 'src/dashboard/reducers/types';
|
||||||
import { generateFiltersSetId } from './utils';
|
import { generateFiltersSetId } from './utils';
|
||||||
import { Filter } from '../../types';
|
import { Filter } from '../../types';
|
||||||
import { useFilters, useDataMask, useFilterSets } from '../state';
|
import { useFilters, useDataMask, useFilterSets } from '../state';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import FiltersHeader from './FiltersHeader';
|
import FilterSetUnit from './FilterSetUnit';
|
||||||
|
|
||||||
const FilterSet = styled.div`
|
const FilterSetsWrapper = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-gap: ${({ theme }) => theme.gridUnit}px;
|
|
||||||
${({ theme }) =>
|
|
||||||
`padding: 0 ${theme.gridUnit * 4}px ${theme.gridUnit * 4}px`};
|
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
|
||||||
& button.superset-button {
|
& button.superset-button {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
@ -54,9 +47,27 @@ const FilterSet = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const FilterSetUnitWrapper = styled.div<{
|
||||||
|
onClick?: HandlerFunction;
|
||||||
|
selected?: boolean;
|
||||||
|
}>`
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
${({ theme }) =>
|
||||||
|
`padding: 0 ${theme.gridUnit * 4}px ${theme.gridUnit * 4}px`};
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
|
padding: ${({ theme }) => `${theme.gridUnit * 3}px ${theme.gridUnit * 2}px`};
|
||||||
|
cursor: ${({ onClick }) => (!onClick ? 'auto' : 'pointer')};
|
||||||
|
${({ theme, selected }) =>
|
||||||
|
`background: ${selected ? theme.colors.primary.light5 : 'transparent'}`};
|
||||||
|
`;
|
||||||
|
|
||||||
type FilterSetsProps = {
|
type FilterSetsProps = {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
dataMaskState: DataMaskUnitWithId;
|
dataMaskSelected: DataMaskUnit;
|
||||||
onFilterSelectionChange: (
|
onFilterSelectionChange: (
|
||||||
filter: Pick<Filter, 'id'> & Partial<Filter>,
|
filter: Pick<Filter, 'id'> & Partial<Filter>,
|
||||||
dataMask: Partial<DataMaskState>,
|
dataMask: Partial<DataMaskState>,
|
||||||
@ -66,27 +77,59 @@ type FilterSetsProps = {
|
|||||||
const DEFAULT_FILTER_SET_NAME = t('New filter set');
|
const DEFAULT_FILTER_SET_NAME = t('New filter set');
|
||||||
|
|
||||||
const FilterSets: React.FC<FilterSetsProps> = ({
|
const FilterSets: React.FC<FilterSetsProps> = ({
|
||||||
|
dataMaskSelected,
|
||||||
disabled,
|
disabled,
|
||||||
onFilterSelectionChange,
|
onFilterSelectionChange,
|
||||||
dataMaskState,
|
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
|
const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const dataMaskApplied = useDataMask();
|
||||||
const filterSets = useFilterSets();
|
const filterSets = useFilterSets();
|
||||||
const filterSetsArray = Object.values(filterSets);
|
const filterSetFilterValues = Object.values(filterSets);
|
||||||
const dataMask = useDataMask();
|
|
||||||
const filters = Object.values(useFilters());
|
const filters = Object.values(useFilters());
|
||||||
const [selectedFiltersSetId, setSelectedFiltersSetId] = useState<
|
const [selectedFiltersSetId, setSelectedFiltersSetId] = useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const takeFilterSet = (value: string) => {
|
useEffect(() => {
|
||||||
setSelectedFiltersSetId(value);
|
const foundFilterSet = filterSetFilterValues.find(({ dataMask }) => {
|
||||||
if (!value) {
|
if (dataMask?.nativeFilters) {
|
||||||
|
return Object.values(dataMask?.nativeFilters).every(
|
||||||
|
filterFromFilterSet => {
|
||||||
|
let currentValueFromFiltersTab =
|
||||||
|
dataMaskApplied[filterFromFilterSet.id]?.currentState ?? {};
|
||||||
|
if (dataMaskSelected[filterFromFilterSet.id]) {
|
||||||
|
currentValueFromFiltersTab =
|
||||||
|
dataMaskSelected[filterFromFilterSet.id]?.currentState;
|
||||||
|
}
|
||||||
|
return areObjectsEqual(
|
||||||
|
filterFromFilterSet.currentState ?? {},
|
||||||
|
currentValueFromFiltersTab,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
setSelectedFiltersSetId(foundFilterSet?.id ?? null);
|
||||||
|
}, [dataMaskApplied, dataMaskSelected, filterSetFilterValues]);
|
||||||
|
|
||||||
|
const takeFilterSet = (target: HTMLElement, id: string) => {
|
||||||
|
const ignoreSelector = 'ant-collapse-header';
|
||||||
|
if (
|
||||||
|
target.classList.contains(ignoreSelector) ||
|
||||||
|
target.parentElement?.classList.contains(ignoreSelector) ||
|
||||||
|
target.parentElement?.parentElement?.classList.contains(ignoreSelector)
|
||||||
|
) {
|
||||||
|
// We don't want select filter set when user expand filters
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filtersSet = filterSets[value];
|
setSelectedFiltersSetId(id);
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filtersSet = filterSets[id];
|
||||||
Object.values(filtersSet.dataMask?.nativeFilters ?? []).forEach(
|
Object.values(filtersSet.dataMask?.nativeFilters ?? []).forEach(
|
||||||
dataMask => {
|
dataMask => {
|
||||||
const { extraFormData, currentState, id } = dataMask as MaskWithId;
|
const { extraFormData, currentState, id } = dataMask as MaskWithId;
|
||||||
@ -101,7 +144,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
|||||||
const handleDeleteFilterSets = () => {
|
const handleDeleteFilterSets = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setFilterSetsConfiguration(
|
setFilterSetsConfiguration(
|
||||||
filterSetsArray.filter(
|
filterSetFilterValues.filter(
|
||||||
filtersSet => filtersSet.id !== selectedFiltersSetId,
|
filtersSet => filtersSet.id !== selectedFiltersSetId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -116,65 +159,58 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateFilterSet = () => {
|
const handleCreateFilterSet = () => {
|
||||||
|
const newFilterSet: FilterSet = {
|
||||||
|
name: filterSetName.trim(),
|
||||||
|
id: generateFiltersSetId(),
|
||||||
|
dataMask: {
|
||||||
|
nativeFilters: dataMaskApplied,
|
||||||
|
},
|
||||||
|
};
|
||||||
dispatch(
|
dispatch(
|
||||||
setFilterSetsConfiguration(
|
setFilterSetsConfiguration([newFilterSet].concat(filterSetFilterValues)),
|
||||||
filterSetsArray.concat([
|
|
||||||
{
|
|
||||||
name: filterSetName.trim(),
|
|
||||||
id: generateFiltersSetId(),
|
|
||||||
dataMask: {
|
|
||||||
nativeFilters: dataMaskState,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
setFilterSetName(DEFAULT_FILTER_SET_NAME);
|
setFilterSetName(DEFAULT_FILTER_SET_NAME);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterSet>
|
<FilterSetsWrapper>
|
||||||
<Typography.Text
|
{!selectedFiltersSetId && (
|
||||||
strong
|
<FilterSetUnitWrapper>
|
||||||
editable={{
|
<FilterSetUnit
|
||||||
editing: editMode,
|
filters={filters}
|
||||||
icon: <span />,
|
editMode={editMode}
|
||||||
onChange: setFilterSetName,
|
setFilterSetName={setFilterSetName}
|
||||||
}}
|
filterSetName={filterSetName}
|
||||||
>
|
dataMaskApplied={dataMaskApplied}
|
||||||
{filterSetName}
|
/>
|
||||||
</Typography.Text>
|
<Footer
|
||||||
<FiltersHeader dataMask={dataMask} filters={filters} />
|
isApplyDisabled={!filterSetName.trim()}
|
||||||
<Footer
|
disabled={disabled}
|
||||||
isApplyDisabled={!filterSetName.trim()}
|
onCancel={handleCancel}
|
||||||
disabled={disabled}
|
editMode={editMode}
|
||||||
onCancel={handleCancel}
|
onEdit={() => setEditMode(true)}
|
||||||
editMode={editMode}
|
onCreate={handleCreateFilterSet}
|
||||||
onEdit={() => setEditMode(true)}
|
/>
|
||||||
onCreate={handleCreateFilterSet}
|
</FilterSetUnitWrapper>
|
||||||
/>
|
)}
|
||||||
<Select
|
{filterSetFilterValues.map(filterSet => (
|
||||||
size="small"
|
<FilterSetUnitWrapper
|
||||||
allowClear
|
selected={filterSet.id === selectedFiltersSetId}
|
||||||
value={selectedFiltersSetId as string}
|
onClick={(e: MouseEvent<HTMLElement>) =>
|
||||||
placeholder={tn('Available %d sets', filterSetsArray.length)}
|
takeFilterSet(e.target as HTMLElement, filterSet.id)
|
||||||
onChange={takeFilterSet}
|
}
|
||||||
>
|
>
|
||||||
{filterSetsArray.map(({ name, id }) => (
|
<FilterSetUnit
|
||||||
<Select.Option value={id}>{name}</Select.Option>
|
isApplied={filterSet.id === selectedFiltersSetId && !disabled}
|
||||||
))}
|
onDelete={handleDeleteFilterSets}
|
||||||
</Select>
|
filters={filters}
|
||||||
<Button
|
dataMaskApplied={dataMaskApplied}
|
||||||
buttonStyle="warning"
|
filterSet={filterSet}
|
||||||
buttonSize="small"
|
/>
|
||||||
disabled={!selectedFiltersSetId}
|
</FilterSetUnitWrapper>
|
||||||
onClick={handleDeleteFilterSets}
|
))}
|
||||||
data-test="filter-save-filters-set-button"
|
</FilterSetsWrapper>
|
||||||
>
|
|
||||||
{t('Delete Filters Set')}
|
|
||||||
</Button>
|
|
||||||
</FilterSet>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import React, { FC } from 'react';
|
|||||||
import { styled, t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { Collapse, Typography } from 'src/common/components';
|
import { Collapse, Typography } from 'src/common/components';
|
||||||
import { DataMaskUnitWithId } from 'src/dataMask/types';
|
import { DataMaskUnitWithId } from 'src/dataMask/types';
|
||||||
|
import { CaretDownOutlined } from '@ant-design/icons';
|
||||||
import { getFilterValueForDisplay } from './utils';
|
import { getFilterValueForDisplay } from './utils';
|
||||||
import { Filter } from '../../types';
|
import { Filter } from '../../types';
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ const StyledCollapse = styled(Collapse)`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
max-width: max-content;
|
||||||
& .ant-collapse-arrow {
|
& .ant-collapse-arrow {
|
||||||
position: static;
|
position: static;
|
||||||
padding-left: ${({ theme }) => theme.gridUnit}px;
|
padding-left: ${({ theme }) => theme.gridUnit}px;
|
||||||
@ -53,9 +55,14 @@ const StyledCollapse = styled(Collapse)`
|
|||||||
type FiltersHeaderProps = {
|
type FiltersHeaderProps = {
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
dataMask: DataMaskUnitWithId;
|
dataMask: DataMaskUnitWithId;
|
||||||
|
expanded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FiltersHeader: FC<FiltersHeaderProps> = ({ filters, dataMask }) => {
|
const FiltersHeader: FC<FiltersHeaderProps> = ({
|
||||||
|
filters,
|
||||||
|
dataMask,
|
||||||
|
expanded,
|
||||||
|
}) => {
|
||||||
const getFiltersHeader = () => (
|
const getFiltersHeader = () => (
|
||||||
<FilterHeader>
|
<FilterHeader>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
@ -67,7 +74,10 @@ const FiltersHeader: FC<FiltersHeaderProps> = ({ filters, dataMask }) => {
|
|||||||
<StyledCollapse
|
<StyledCollapse
|
||||||
ghost
|
ghost
|
||||||
expandIconPosition="right"
|
expandIconPosition="right"
|
||||||
defaultActiveKey={['filters']}
|
defaultActiveKey={expanded ? ['filters'] : undefined}
|
||||||
|
expandIcon={({ isActive }: { isActive: boolean }) => (
|
||||||
|
<CaretDownOutlined rotate={isActive ? 0 : 180} />
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Collapse.Panel header={getFiltersHeader()} key="filters">
|
<Collapse.Panel header={getFiltersHeader()} key="filters">
|
||||||
{filters.map(({ id, name }) => (
|
{filters.map(({ id, name }) => (
|
||||||
|
@ -38,6 +38,7 @@ const ActionButton = styled.div<{ disabled: boolean }>`
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ActionButtons = styled.div`
|
const ActionButtons = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -70,7 +71,10 @@ const Footer: FC<FooterProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
title={(isApplyDisabled || disabled) && APPLY_FILTERS}
|
title={
|
||||||
|
(isApplyDisabled && t('Please filter set name')) ||
|
||||||
|
(disabled && APPLY_FILTERS)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ActionButton disabled={disabled}>
|
<ActionButton disabled={disabled}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
SET_FILTER_CONFIG_COMPLETE,
|
SET_FILTER_CONFIG_COMPLETE,
|
||||||
SET_FILTER_SETS_CONFIG_COMPLETE,
|
SET_FILTER_SETS_CONFIG_COMPLETE,
|
||||||
} from 'src/dashboard/actions/nativeFilters';
|
} from 'src/dashboard/actions/nativeFilters';
|
||||||
import { FiltersSet, NativeFiltersState } from './types';
|
import { FilterSet, NativeFiltersState } from './types';
|
||||||
import { FilterConfiguration } from '../components/nativeFilters/types';
|
import { FilterConfiguration } from '../components/nativeFilters/types';
|
||||||
|
|
||||||
export function getInitialState({
|
export function getInitialState({
|
||||||
@ -30,7 +30,7 @@ export function getInitialState({
|
|||||||
filterConfig,
|
filterConfig,
|
||||||
state: prevState,
|
state: prevState,
|
||||||
}: {
|
}: {
|
||||||
filterSetsConfig?: FiltersSet[];
|
filterSetsConfig?: FilterSet[];
|
||||||
filterConfig?: FilterConfiguration;
|
filterConfig?: FilterConfiguration;
|
||||||
state?: NativeFiltersState;
|
state?: NativeFiltersState;
|
||||||
}): NativeFiltersState {
|
}): NativeFiltersState {
|
||||||
|
@ -67,14 +67,14 @@ export type LayoutItem = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FiltersSet = {
|
export type FilterSet = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
dataMask: Partial<DataMaskStateWithId>;
|
dataMask: Partial<DataMaskStateWithId>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FilterSets = {
|
export type FilterSets = {
|
||||||
[filtersSetId: string]: FiltersSet;
|
[filtersSetId: string]: FilterSet;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Filters = {
|
export type Filters = {
|
||||||
|
Loading…
Reference in New Issue
Block a user