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:
simcha90 2021-03-10 11:10:47 +02:00 committed by GitHub
parent 226dd4b907
commit 1d1a1cdc20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 287 additions and 128 deletions

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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