You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2021/03/10 09:11:47 UTC

[superset] branch master updated: feat(filter-set): Filter set history (#13529)

This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 1d1a1cd  feat(filter-set): Filter set history (#13529)
1d1a1cd is described below

commit 1d1a1cdc208a7df6e45bffaac3e99675a61e0a20
Author: simcha90 <56...@users.noreply.github.com>
AuthorDate: Wed Mar 10 11:10:47 2021 +0200

    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
---
 .../src/dashboard/actions/nativeFilters.ts         |  10 +-
 .../nativeFilters/FilterBar/FilterBar.tsx          |  76 ++++-----
 .../FilterBar/FilterSets/FilterSetUnit.tsx         | 113 +++++++++++++
 .../FilterBar/FilterSets/FilterSets.tsx            | 188 ++++++++++++---------
 .../FilterBar/FilterSets/FiltersHeader.tsx         |  14 +-
 .../nativeFilters/FilterBar/FilterSets/Footer.tsx  |   6 +-
 .../src/dashboard/reducers/nativeFilters.ts        |   4 +-
 superset-frontend/src/dashboard/reducers/types.ts  |   4 +-
 8 files changed, 287 insertions(+), 128 deletions(-)

diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts
index a1571fe..8a18d52 100644
--- a/superset-frontend/src/dashboard/actions/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts
@@ -26,7 +26,7 @@ import {
   SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
 } from 'src/dataMask/actions';
 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 interface SetFilterConfigBegin {
@@ -46,18 +46,18 @@ export interface SetFilterConfigFail {
 export const SET_FILTER_SETS_CONFIG_BEGIN = 'SET_FILTER_SETS_CONFIG_BEGIN';
 export interface SetFilterSetsConfigBegin {
   type: typeof SET_FILTER_SETS_CONFIG_BEGIN;
-  filterSetsConfig: FiltersSet[];
+  filterSetsConfig: FilterSet[];
 }
 export const SET_FILTER_SETS_CONFIG_COMPLETE =
   'SET_FILTER_SETS_CONFIG_COMPLETE';
 export interface SetFilterSetsConfigComplete {
   type: typeof SET_FILTER_SETS_CONFIG_COMPLETE;
-  filterSetsConfig: FiltersSet[];
+  filterSetsConfig: FilterSet[];
 }
 export const SET_FILTER_SETS_CONFIG_FAIL = 'SET_FILTER_SETS_CONFIG_FAIL';
 export interface SetFilterSetsConfigFail {
   type: typeof SET_FILTER_SETS_CONFIG_FAIL;
-  filterSetsConfig: FiltersSet[];
+  filterSetsConfig: FilterSet[];
 }
 
 interface DashboardInfo {
@@ -110,7 +110,7 @@ export const setFilterConfiguration = (
 };
 
 export const setFilterSetsConfiguration = (
-  filterSetsConfig: FiltersSet[],
+  filterSetsConfig: FilterSet[],
 ) => async (dispatch: Dispatch, getState: () => any) => {
   dispatch({
     type: SET_FILTER_SETS_CONFIG_BEGIN,
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx
index f2964a1..b5f05e4 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx
@@ -27,11 +27,7 @@ import Icon from 'src/components/Icon';
 import { Tabs } from 'src/common/components';
 import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
 import { updateDataMask } from 'src/dataMask/actions';
-import {
-  DataMaskUnitWithId,
-  DataMaskUnit,
-  DataMaskState,
-} from 'src/dataMask/types';
+import { DataMaskUnit, DataMaskState } from 'src/dataMask/types';
 import { useImmer } from 'use-immer';
 import { getInitialMask } from 'src/dataMask/reducer';
 import { areObjectsEqual } from 'src/reduxUtils';
@@ -40,13 +36,15 @@ import { Filter } from '../types';
 import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils';
 import CascadePopover from './CascadePopover';
 import FilterSets from './FilterSets/FilterSets';
-import { useFilters, useFilterSets } from './state';
+import { useDataMask, useFilters, useFilterSets } from './state';
 
 const barWidth = `250px`;
 
 const BarWrapper = styled.div`
   width: ${({ theme }) => theme.gridUnit * 8}px;
-
+  & .ant-tabs-top > .ant-tabs-nav {
+    margin: 0;
+  }
   &.open {
     width: ${barWidth}; // arbitrary...
   }
@@ -158,7 +156,7 @@ const ActionButtons = styled.div`
 `;
 
 const FilterControls = styled.div`
-  padding: 0 ${({ theme }) => theme.gridUnit * 4}px;
+  padding: ${({ theme }) => theme.gridUnit * 4}px;
   &:hover {
     cursor: pointer;
   }
@@ -175,19 +173,17 @@ const FilterBar: React.FC<FiltersBarProps> = ({
   toggleFiltersBar,
   directPathToChild,
 }) => {
-  const [filterData, setFilterData] = useImmer<DataMaskUnit>({});
+  const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskUnit>({});
   const [
     lastAppliedFilterData,
     setLastAppliedFilterData,
   ] = useImmer<DataMaskUnit>({});
   const dispatch = useDispatch();
   const filterSets = useFilterSets();
-  const filterSetsArray = Object.values(filterSets);
+  const filterSetFilterValues = Object.values(filterSets);
   const filters = useFilters();
-  const filtersArray = Object.values(filters);
-  const dataMaskState = useSelector<any, DataMaskUnitWithId>(
-    state => state.dataMask.nativeFilters ?? {},
-  );
+  const filterValues = Object.values(filters);
+  const dataMaskApplied = useDataMask();
   const canEdit = useSelector<any, boolean>(
     ({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
   );
@@ -195,59 +191,59 @@ const FilterBar: React.FC<FiltersBarProps> = ({
   const [isInitialized, setIsInitialized] = useState<boolean>(false);
 
   const handleApply = () => {
-    const filterIds = Object.keys(filterData);
+    const filterIds = Object.keys(dataMaskSelected);
     filterIds.forEach(filterId => {
-      if (filterData[filterId]) {
+      if (dataMaskSelected[filterId]) {
         dispatch(
           updateDataMask(filterId, {
-            nativeFilters: filterData[filterId],
+            nativeFilters: dataMaskSelected[filterId],
           }),
         );
       }
     });
-    setLastAppliedFilterData(() => filterData);
+    setLastAppliedFilterData(() => dataMaskSelected);
   };
 
   useEffect(() => {
     if (isInitialized) {
       return;
     }
-    const areFiltersInitialized = filtersArray.every(filterConfig =>
+    const areFiltersInitialized = filterValues.every(filterValue =>
       areObjectsEqual(
-        filterConfig.defaultValue,
-        filterData[filterConfig.id]?.currentState?.value,
+        filterValue.defaultValue,
+        dataMaskSelected[filterValue.id]?.currentState?.value,
       ),
     );
     if (areFiltersInitialized) {
       handleApply();
       setIsInitialized(true);
     }
-  }, [filtersArray, filterData, isInitialized]);
+  }, [filterValues, dataMaskSelected, isInitialized]);
 
   useEffect(() => {
-    if (filtersArray.length === 0 && filtersOpen) {
+    if (filterValues.length === 0 && filtersOpen) {
       toggleFiltersBar(false);
     }
-  }, [filtersArray.length]);
+  }, [filterValues.length]);
 
   const cascadeChildren = useMemo(
-    () => mapParentFiltersToChildren(filtersArray),
-    [filtersArray],
+    () => mapParentFiltersToChildren(filterValues),
+    [filterValues],
   );
 
   const cascadeFilters = useMemo(() => {
-    const filtersWithValue = filtersArray.map(filter => ({
+    const filtersWithValue = filterValues.map(filter => ({
       ...filter,
-      currentValue: filterData[filter.id]?.currentState?.value,
+      currentValue: dataMaskSelected[filter.id]?.currentState?.value,
     }));
     return buildCascadeFiltersTree(filtersWithValue);
-  }, [filtersArray, filterData]);
+  }, [filterValues, dataMaskSelected]);
 
   const handleFilterSelectionChange = (
     filter: Pick<Filter, 'id'> & Partial<Filter>,
     dataMask: Partial<DataMaskState>,
   ) => {
-    setFilterData(draft => {
+    setDataMaskSelected(draft => {
       const children = cascadeChildren[filter.id] || [];
       // force instant updating on initialization or for parent filters
       if (filter.isInstant || children.length > 0) {
@@ -261,17 +257,17 @@ const FilterBar: React.FC<FiltersBarProps> = ({
   };
 
   const handleClearAll = () => {
-    filtersArray.forEach(filter => {
-      setFilterData(draft => {
+    filterValues.forEach(filter => {
+      setDataMaskSelected(draft => {
         draft[filter.id] = getInitialMask(filter.id);
       });
     });
   };
 
-  const isClearAllDisabled = Object.values(dataMaskState).every(
+  const isClearAllDisabled = Object.values(dataMaskApplied).every(
     filter =>
-      filterData[filter.id]?.currentState?.value === null ||
-      (!filterData[filter.id] && filter.currentState?.value === null),
+      dataMaskSelected[filter.id]?.currentState?.value === null ||
+      (!dataMaskSelected[filter.id] && filter.currentState?.value === null),
   );
 
   const getFilterControls = () => (
@@ -293,7 +289,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
   );
 
   const isApplyDisabled =
-    !isInitialized || areObjectsEqual(filterData, lastAppliedFilterData);
+    !isInitialized || areObjectsEqual(dataMaskSelected, lastAppliedFilterData);
 
   return (
     <BarWrapper data-test="filter-bar" className={cx({ open: filtersOpen })}>
@@ -309,7 +305,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
           <span>{t('Filters')}</span>
           {canEdit && (
             <FilterConfigurationLink
-              createNewOnOpen={filtersArray.length === 0}
+              createNewOnOpen={filterValues.length === 0}
             >
               <Icon name="edit" data-test="create-filter" />
             </FilterConfigurationLink>
@@ -344,18 +340,18 @@ const FilterBar: React.FC<FiltersBarProps> = ({
             onChange={() => {}}
           >
             <Tabs.TabPane
-              tab={t(`All Filters (${filtersArray.length})`)}
+              tab={t(`All Filters (${filterValues.length})`)}
               key="allFilters"
             >
               {getFilterControls()}
             </Tabs.TabPane>
             <Tabs.TabPane
-              tab={t(`Filter Sets (${filterSetsArray.length})`)}
+              tab={t(`Filter Sets (${filterSetFilterValues.length})`)}
               key="filterSets"
             >
               <FilterSets
                 disabled={!isApplyDisabled}
-                dataMaskState={dataMaskState}
+                dataMaskSelected={dataMaskSelected}
                 onFilterSelectionChange={handleFilterSelectionChange}
               />
             </Tabs.TabPane>
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSetUnit.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSetUnit.tsx
new file mode 100644
index 0000000..ee9ea25
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSetUnit.tsx
@@ -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;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.tsx
index 8252677..608c2e7 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.tsx
@@ -16,32 +16,25 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Select, Typography } from 'src/common/components';
-import Button from 'src/components/Button';
-import React, { useState } from 'react';
-import { styled, t, tn } from '@superset-ui/core';
+
+import React, { useEffect, useState, MouseEvent } from 'react';
+import { HandlerFunction, styled, t } from '@superset-ui/core';
 import { useDispatch } from 'react-redux';
-import {
-  DataMaskState,
-  DataMaskUnitWithId,
-  MaskWithId,
-} from 'src/dataMask/types';
+import { DataMaskState, DataMaskUnit, MaskWithId } from 'src/dataMask/types';
 import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters';
+import { areObjectsEqual } from 'src/reduxUtils';
+import { FilterSet } from 'src/dashboard/reducers/types';
 import { generateFiltersSetId } from './utils';
 import { Filter } from '../../types';
 import { useFilters, useDataMask, useFilterSets } from '../state';
 import Footer from './Footer';
-import FiltersHeader from './FiltersHeader';
+import FilterSetUnit from './FilterSetUnit';
 
-const FilterSet = styled.div`
+const FilterSetsWrapper = styled.div`
   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};
   & button.superset-button {
     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 = {
   disabled: boolean;
-  dataMaskState: DataMaskUnitWithId;
+  dataMaskSelected: DataMaskUnit;
   onFilterSelectionChange: (
     filter: Pick<Filter, 'id'> & Partial<Filter>,
     dataMask: Partial<DataMaskState>,
@@ -66,27 +77,59 @@ type FilterSetsProps = {
 const DEFAULT_FILTER_SET_NAME = t('New filter set');
 
 const FilterSets: React.FC<FilterSetsProps> = ({
+  dataMaskSelected,
   disabled,
   onFilterSelectionChange,
-  dataMaskState,
 }) => {
   const dispatch = useDispatch();
   const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
   const [editMode, setEditMode] = useState(false);
+  const dataMaskApplied = useDataMask();
   const filterSets = useFilterSets();
-  const filterSetsArray = Object.values(filterSets);
-  const dataMask = useDataMask();
+  const filterSetFilterValues = Object.values(filterSets);
   const filters = Object.values(useFilters());
   const [selectedFiltersSetId, setSelectedFiltersSetId] = useState<
     string | null
   >(null);
 
-  const takeFilterSet = (value: string) => {
-    setSelectedFiltersSetId(value);
-    if (!value) {
+  useEffect(() => {
+    const foundFilterSet = filterSetFilterValues.find(({ dataMask }) => {
+      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;
     }
-    const filtersSet = filterSets[value];
+    setSelectedFiltersSetId(id);
+    if (!id) {
+      return;
+    }
+    const filtersSet = filterSets[id];
     Object.values(filtersSet.dataMask?.nativeFilters ?? []).forEach(
       dataMask => {
         const { extraFormData, currentState, id } = dataMask as MaskWithId;
@@ -101,7 +144,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
   const handleDeleteFilterSets = () => {
     dispatch(
       setFilterSetsConfiguration(
-        filterSetsArray.filter(
+        filterSetFilterValues.filter(
           filtersSet => filtersSet.id !== selectedFiltersSetId,
         ),
       ),
@@ -116,65 +159,58 @@ const FilterSets: React.FC<FilterSetsProps> = ({
   };
 
   const handleCreateFilterSet = () => {
+    const newFilterSet: FilterSet = {
+      name: filterSetName.trim(),
+      id: generateFiltersSetId(),
+      dataMask: {
+        nativeFilters: dataMaskApplied,
+      },
+    };
     dispatch(
-      setFilterSetsConfiguration(
-        filterSetsArray.concat([
-          {
-            name: filterSetName.trim(),
-            id: generateFiltersSetId(),
-            dataMask: {
-              nativeFilters: dataMaskState,
-            },
-          },
-        ]),
-      ),
+      setFilterSetsConfiguration([newFilterSet].concat(filterSetFilterValues)),
     );
     setEditMode(false);
     setFilterSetName(DEFAULT_FILTER_SET_NAME);
   };
 
   return (
-    <FilterSet>
-      <Typography.Text
-        strong
-        editable={{
-          editing: editMode,
-          icon: <span />,
-          onChange: setFilterSetName,
-        }}
-      >
-        {filterSetName}
-      </Typography.Text>
-      <FiltersHeader dataMask={dataMask} filters={filters} />
-      <Footer
-        isApplyDisabled={!filterSetName.trim()}
-        disabled={disabled}
-        onCancel={handleCancel}
-        editMode={editMode}
-        onEdit={() => setEditMode(true)}
-        onCreate={handleCreateFilterSet}
-      />
-      <Select
-        size="small"
-        allowClear
-        value={selectedFiltersSetId as string}
-        placeholder={tn('Available %d sets', filterSetsArray.length)}
-        onChange={takeFilterSet}
-      >
-        {filterSetsArray.map(({ name, id }) => (
-          <Select.Option value={id}>{name}</Select.Option>
-        ))}
-      </Select>
-      <Button
-        buttonStyle="warning"
-        buttonSize="small"
-        disabled={!selectedFiltersSetId}
-        onClick={handleDeleteFilterSets}
-        data-test="filter-save-filters-set-button"
-      >
-        {t('Delete Filters Set')}
-      </Button>
-    </FilterSet>
+    <FilterSetsWrapper>
+      {!selectedFiltersSetId && (
+        <FilterSetUnitWrapper>
+          <FilterSetUnit
+            filters={filters}
+            editMode={editMode}
+            setFilterSetName={setFilterSetName}
+            filterSetName={filterSetName}
+            dataMaskApplied={dataMaskApplied}
+          />
+          <Footer
+            isApplyDisabled={!filterSetName.trim()}
+            disabled={disabled}
+            onCancel={handleCancel}
+            editMode={editMode}
+            onEdit={() => setEditMode(true)}
+            onCreate={handleCreateFilterSet}
+          />
+        </FilterSetUnitWrapper>
+      )}
+      {filterSetFilterValues.map(filterSet => (
+        <FilterSetUnitWrapper
+          selected={filterSet.id === selectedFiltersSetId}
+          onClick={(e: MouseEvent<HTMLElement>) =>
+            takeFilterSet(e.target as HTMLElement, filterSet.id)
+          }
+        >
+          <FilterSetUnit
+            isApplied={filterSet.id === selectedFiltersSetId && !disabled}
+            onDelete={handleDeleteFilterSets}
+            filters={filters}
+            dataMaskApplied={dataMaskApplied}
+            filterSet={filterSet}
+          />
+        </FilterSetUnitWrapper>
+      ))}
+    </FilterSetsWrapper>
   );
 };
 
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx
index d8faa4b..8982a21 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx
@@ -20,6 +20,7 @@ import React, { FC } from 'react';
 import { styled, t } from '@superset-ui/core';
 import { Collapse, Typography } from 'src/common/components';
 import { DataMaskUnitWithId } from 'src/dataMask/types';
+import { CaretDownOutlined } from '@ant-design/icons';
 import { getFilterValueForDisplay } from './utils';
 import { Filter } from '../../types';
 
@@ -43,6 +44,7 @@ const StyledCollapse = styled(Collapse)`
       align-items: center;
       flex-direction: row-reverse;
       justify-content: flex-end;
+      max-width: max-content;
       & .ant-collapse-arrow {
         position: static;
         padding-left: ${({ theme }) => theme.gridUnit}px;
@@ -53,9 +55,14 @@ const StyledCollapse = styled(Collapse)`
 type FiltersHeaderProps = {
   filters: Filter[];
   dataMask: DataMaskUnitWithId;
+  expanded: boolean;
 };
 
-const FiltersHeader: FC<FiltersHeaderProps> = ({ filters, dataMask }) => {
+const FiltersHeader: FC<FiltersHeaderProps> = ({
+  filters,
+  dataMask,
+  expanded,
+}) => {
   const getFiltersHeader = () => (
     <FilterHeader>
       <Typography.Text type="secondary">
@@ -67,7 +74,10 @@ const FiltersHeader: FC<FiltersHeaderProps> = ({ filters, dataMask }) => {
     <StyledCollapse
       ghost
       expandIconPosition="right"
-      defaultActiveKey={['filters']}
+      defaultActiveKey={expanded ? ['filters'] : undefined}
+      expandIcon={({ isActive }: { isActive: boolean }) => (
+        <CaretDownOutlined rotate={isActive ? 0 : 180} />
+      )}
     >
       <Collapse.Panel header={getFiltersHeader()} key="filters">
         {filters.map(({ id, name }) => (
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx
index a19be00..5e53ed8 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx
@@ -38,6 +38,7 @@ const ActionButton = styled.div<{ disabled: boolean }>`
     flex: 1;
   }
 `;
+
 const ActionButtons = styled.div`
   display: grid;
   flex-direction: row;
@@ -70,7 +71,10 @@ const Footer: FC<FooterProps> = ({
         </Button>
         <Tooltip
           placement="bottom"
-          title={(isApplyDisabled || disabled) && APPLY_FILTERS}
+          title={
+            (isApplyDisabled && t('Please filter set name')) ||
+            (disabled && APPLY_FILTERS)
+          }
         >
           <ActionButton disabled={disabled}>
             <Button
diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
index 92d0708..d860cbe 100644
--- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
@@ -22,7 +22,7 @@ import {
   SET_FILTER_CONFIG_COMPLETE,
   SET_FILTER_SETS_CONFIG_COMPLETE,
 } from 'src/dashboard/actions/nativeFilters';
-import { FiltersSet, NativeFiltersState } from './types';
+import { FilterSet, NativeFiltersState } from './types';
 import { FilterConfiguration } from '../components/nativeFilters/types';
 
 export function getInitialState({
@@ -30,7 +30,7 @@ export function getInitialState({
   filterConfig,
   state: prevState,
 }: {
-  filterSetsConfig?: FiltersSet[];
+  filterSetsConfig?: FilterSet[];
   filterConfig?: FilterConfiguration;
   state?: NativeFiltersState;
 }): NativeFiltersState {
diff --git a/superset-frontend/src/dashboard/reducers/types.ts b/superset-frontend/src/dashboard/reducers/types.ts
index 78b3ea7..a3cd633 100644
--- a/superset-frontend/src/dashboard/reducers/types.ts
+++ b/superset-frontend/src/dashboard/reducers/types.ts
@@ -67,14 +67,14 @@ export type LayoutItem = {
   };
 };
 
-export type FiltersSet = {
+export type FilterSet = {
   id: string;
   name: string;
   dataMask: Partial<DataMaskStateWithId>;
 };
 
 export type FilterSets = {
-  [filtersSetId: string]: FiltersSet;
+  [filtersSetId: string]: FilterSet;
 };
 
 export type Filters = {