You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2022/02/22 16:47:42 UTC

[superset] branch master updated: feat(native-filters): Re-arrange controls in FilterBar (#18784)

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

kgabryje 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 9d5c050  feat(native-filters): Re-arrange controls in FilterBar (#18784)
9d5c050 is described below

commit 9d5c0505cf9bf67be499abd4829195adf6ad17d5
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Tue Feb 22 17:45:53 2022 +0100

    feat(native-filters): Re-arrange controls in FilterBar (#18784)
    
    * feat(native-filters): Re-arrange controls in FilterBar
    
    * Typo fix
    
    * Add tests
    
    * Fix tests
    
    * Address PR reviews
    
    * Add 1px bottom padding to icon
    
    * Fix test
    
    * Tiny refactor
    
    * Change buttons background
    
    * Add hover state to Clear all button
    
    * Fix Clear All button logic so it clears all selections
    
    * Remove redundant useEffect
    
    * Set zindex higher than loading icon
    
    * Fix scrolling issue
---
 .../cypress-base/cypress/support/directories.ts    |   2 +-
 superset-frontend/src/components/Button/index.tsx  |   8 +-
 .../DashboardBuilder/DashboardBuilder.tsx          |  19 ++--
 .../ActionButtons.test.tsx}                        |  54 ++-------
 .../FilterBar/ActionButtons/index.tsx              | 125 +++++++++++++++++++++
 .../nativeFilters/FilterBar/FilterBar.test.tsx     |   4 +-
 .../FilterBar/FilterControls/FilterControls.tsx    |   3 +
 .../nativeFilters/FilterBar/FilterSets/index.tsx   |   2 +
 .../nativeFilters/FilterBar/Header/Header.test.tsx |  60 ----------
 .../nativeFilters/FilterBar/Header/index.tsx       | 108 ++++++------------
 .../components/nativeFilters/FilterBar/index.tsx   |  61 ++++++----
 superset-frontend/src/dashboard/constants.ts       |   9 ++
 superset-frontend/src/utils/common.js              |   2 +
 13 files changed, 239 insertions(+), 218 deletions(-)

diff --git a/superset-frontend/cypress-base/cypress/support/directories.ts b/superset-frontend/cypress-base/cypress/support/directories.ts
index 6d5838f..b68ec36 100644
--- a/superset-frontend/cypress-base/cypress/support/directories.ts
+++ b/superset-frontend/cypress-base/cypress/support/directories.ts
@@ -343,7 +343,7 @@ export const nativeFilters = {
     collapse: dataTestLocator('filter-bar__collapse-button'),
     filterName: dataTestLocator('filter-control-name'),
     filterContent: '.ant-select-selection-item',
-    createFilterButton: dataTestLocator('create-filter'),
+    createFilterButton: dataTestLocator('filter-bar__create-filter'),
     timeRangeFilterContent: dataTestLocator('time-range-trigger'),
   },
   createFilterButton: dataTestLocator('filter-bar__create-filter'),
diff --git a/superset-frontend/src/components/Button/index.tsx b/superset-frontend/src/components/Button/index.tsx
index 725ac6d..8851a6f 100644
--- a/superset-frontend/src/components/Button/index.tsx
+++ b/superset-frontend/src/components/Button/index.tsx
@@ -202,14 +202,16 @@ export default function Button(props: ButtonProps) {
         },
         '&[disabled], &[disabled]:hover': {
           color: grayscale.base,
-          backgroundColor: backgroundColorDisabled,
-          borderColor: borderColorDisabled,
+          backgroundColor:
+            buttonStyle === 'link' ? 'transparent' : backgroundColorDisabled,
+          borderColor:
+            buttonStyle === 'link' ? 'transparent' : borderColorDisabled,
         },
         marginLeft: 0,
         '& + .superset-button': {
           marginLeft: theme.gridUnit * 2,
         },
-        '& :first-of-type': {
+        '& > :first-of-type': {
           marginRight: firstChildMargin,
         },
       }}
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 577351a..e354662 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -51,19 +51,20 @@ import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar';
 import Loading from 'src/components/Loading';
 import { EmptyStateBig } from 'src/components/EmptyState';
 import { useUiConfig } from 'src/components/UiConfigContext';
+import {
+  BUILDER_SIDEPANEL_WIDTH,
+  CLOSED_FILTER_BAR_WIDTH,
+  FILTER_BAR_HEADER_HEIGHT,
+  FILTER_BAR_TABS_HEIGHT,
+  HEADER_HEIGHT,
+  MAIN_HEADER_HEIGHT,
+  OPEN_FILTER_BAR_WIDTH,
+  TABS_HEIGHT,
+} from 'src/dashboard/constants';
 import { shouldFocusTabs, getRootLevelTabsComponent } from './utils';
 import DashboardContainer from './DashboardContainer';
 import { useNativeFilters } from './state';
 
-const MAIN_HEADER_HEIGHT = 53;
-const TABS_HEIGHT = 50;
-const HEADER_HEIGHT = 72;
-const CLOSED_FILTER_BAR_WIDTH = 32;
-const OPEN_FILTER_BAR_WIDTH = 260;
-const FILTER_BAR_HEADER_HEIGHT = 80;
-const FILTER_BAR_TABS_HEIGHT = 46;
-const BUILDER_SIDEPANEL_WIDTH = 374;
-
 type DashboardBuilderProps = {};
 
 const StyledDiv = styled.div`
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/ActionButtons.test.tsx
similarity index 55%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx
copy to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/ActionButtons.test.tsx
index 64a2dd1..77aede5 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/ActionButtons.test.tsx
@@ -17,12 +17,11 @@
  * under the License.
  */
 import React from 'react';
-import { render, screen } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
-import Header from './index';
+import { render, screen } from 'spec/helpers/testing-library';
+import { ActionButtons } from './index';
 
 const createProps = () => ({
-  toggleFiltersBar: jest.fn(),
   onApply: jest.fn(),
   onClearAll: jest.fn(),
   dataMaskSelected: {
@@ -43,34 +42,16 @@ const createProps = () => ({
   isApplyDisabled: false,
 });
 
-test('should render', () => {
-  const mockedProps = createProps();
-  const { container } = render(<Header {...mockedProps} />, { useRedux: true });
-  expect(container).toBeInTheDocument();
-});
-
-test('should render the "Filters" heading', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  expect(screen.getByText('Filters')).toBeInTheDocument();
-});
-
-test('should render the "Clear all" option', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  expect(screen.getByText('Clear all')).toBeInTheDocument();
-});
-
 test('should render the "Apply" button', () => {
   const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  expect(screen.getByText('Apply')).toBeInTheDocument();
-  expect(screen.getByText('Apply').parentElement).toBeEnabled();
+  render(<ActionButtons {...mockedProps} />, { useRedux: true });
+  expect(screen.getByText('Apply filters')).toBeInTheDocument();
+  expect(screen.getByText('Apply filters').parentElement).toBeEnabled();
 });
 
 test('should render the "Clear all" button as disabled', () => {
   const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
+  render(<ActionButtons {...mockedProps} />, { useRedux: true });
   const clearBtn = screen.getByText('Clear all');
   expect(clearBtn.parentElement).toBeDisabled();
 });
@@ -81,8 +62,8 @@ test('should render the "Apply" button as disabled', () => {
     ...mockedProps,
     isApplyDisabled: true,
   };
-  render(<Header {...applyDisabledProps} />, { useRedux: true });
-  const applyBtn = screen.getByText('Apply');
+  render(<ActionButtons {...applyDisabledProps} />, { useRedux: true });
+  const applyBtn = screen.getByText('Apply filters');
   expect(applyBtn.parentElement).toBeDisabled();
   userEvent.click(applyBtn);
   expect(mockedProps.onApply).not.toHaveBeenCalled();
@@ -90,24 +71,9 @@ test('should render the "Apply" button as disabled', () => {
 
 test('should apply', () => {
   const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  const applyBtn = screen.getByText('Apply');
+  render(<ActionButtons {...mockedProps} />, { useRedux: true });
+  const applyBtn = screen.getByText('Apply filters');
   expect(mockedProps.onApply).not.toHaveBeenCalled();
   userEvent.click(applyBtn);
   expect(mockedProps.onApply).toHaveBeenCalled();
 });
-
-test('should render the expand button', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  expect(screen.getByRole('button', { name: 'expand' })).toBeInTheDocument();
-});
-
-test('should toggle', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  const expandBtn = screen.getByRole('button', { name: 'expand' });
-  expect(mockedProps.toggleFiltersBar).not.toHaveBeenCalled();
-  userEvent.click(expandBtn);
-  expect(mockedProps.toggleFiltersBar).toHaveBeenCalled();
-});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx
new file mode 100644
index 0000000..55d4783
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx
@@ -0,0 +1,125 @@
+/**
+ * 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 React, { useMemo } from 'react';
+import {
+  css,
+  DataMaskState,
+  DataMaskStateWithId,
+  styled,
+  t,
+} from '@superset-ui/core';
+import Button from 'src/components/Button';
+import { isNullish } from 'src/utils/common';
+import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants';
+import { getFilterBarTestId } from '../index';
+
+interface ActionButtonsProps {
+  onApply: () => void;
+  onClearAll: () => void;
+  dataMaskSelected: DataMaskState;
+  dataMaskApplied: DataMaskStateWithId;
+  isApplyDisabled: boolean;
+}
+
+const ActionButtonsContainer = styled.div`
+  ${({ theme }) => css`
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    position: fixed;
+    z-index: 100;
+
+    // filter bar width minus 1px for border
+    width: ${OPEN_FILTER_BAR_WIDTH - 1}px;
+    bottom: 0;
+
+    padding: ${theme.gridUnit * 4}px;
+    padding-top: ${theme.gridUnit * 6}px;
+
+    background: linear-gradient(transparent, white 25%);
+
+    pointer-events: none;
+
+    & > button {
+      pointer-events: auto;
+    }
+
+    & > .filter-apply-button {
+      margin-bottom: ${theme.gridUnit * 3}px;
+    }
+
+    && > .filter-clear-all-button {
+      color: ${theme.colors.grayscale.base};
+      margin-left: 0;
+      &:hover {
+        color: ${theme.colors.primary.dark1};
+      }
+
+      &[disabled],
+      &[disabled]:hover {
+        color: ${theme.colors.grayscale.light1};
+      }
+    }
+  `};
+`;
+
+export const ActionButtons = ({
+  onApply,
+  onClearAll,
+  dataMaskApplied,
+  dataMaskSelected,
+  isApplyDisabled,
+}: ActionButtonsProps) => {
+  const isClearAllEnabled = useMemo(
+    () =>
+      Object.values(dataMaskApplied).some(
+        filter =>
+          !isNullish(dataMaskSelected[filter.id]?.filterState?.value) ||
+          (!dataMaskSelected[filter.id] &&
+            !isNullish(filter.filterState?.value)),
+      ),
+    [dataMaskApplied, dataMaskSelected],
+  );
+
+  return (
+    <ActionButtonsContainer>
+      <Button
+        disabled={isApplyDisabled}
+        buttonStyle="primary"
+        htmlType="submit"
+        className="filter-apply-button"
+        onClick={onApply}
+        {...getFilterBarTestId('apply-button')}
+      >
+        {t('Apply filters')}
+      </Button>
+      <Button
+        disabled={!isClearAllEnabled}
+        buttonStyle="link"
+        buttonSize="small"
+        className="filter-clear-all-button"
+        onClick={onClearAll}
+        {...getFilterBarTestId('clear-button')}
+      >
+        {t('Clear all')}
+      </Button>
+    </ActionButtonsContainer>
+  );
+};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
index 9be4a35..632f897 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
@@ -241,9 +241,9 @@ describe('FilterBar', () => {
     expect(screen.getByText('Clear all')).toBeInTheDocument();
   });
 
-  it('should render the "Apply" option', () => {
+  it('should render the "Apply filters" option', () => {
     renderWrapper();
-    expect(screen.getByText('Apply')).toBeInTheDocument();
+    expect(screen.getByText('Apply filters')).toBeInTheDocument();
   });
 
   it('should render the collapse icon', () => {
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
index fb83fc9..dd1bfd9 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
@@ -42,6 +42,9 @@ import { buildCascadeFiltersTree } from './utils';
 
 const Wrapper = styled.div`
   padding: ${({ theme }) => theme.gridUnit * 4}px;
+  // 108px padding to make room for buttons with position: absolute
+  padding-bottom: ${({ theme }) => theme.gridUnit * 27}px;
+
   &:hover {
     cursor: pointer;
   }
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx
index 18f5a56..5fdfd48 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx
@@ -48,6 +48,8 @@ const FilterSetsWrapper = styled.div`
   align-items: center;
   justify-content: center;
   grid-template-columns: 1fr;
+  // 108px padding to make room for buttons with position: absolute
+  padding-bottom: ${({ theme }) => theme.gridUnit * 27}px;
 
   & button.superset-button {
     margin-left: 0;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx
index 64a2dd1..caa8efa 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/Header.test.tsx
@@ -23,24 +23,6 @@ import Header from './index';
 
 const createProps = () => ({
   toggleFiltersBar: jest.fn(),
-  onApply: jest.fn(),
-  onClearAll: jest.fn(),
-  dataMaskSelected: {
-    DefaultsID: {
-      filterState: {
-        value: null,
-      },
-    },
-  },
-  dataMaskApplied: {
-    DefaultsID: {
-      id: 'DefaultsID',
-      filterState: {
-        value: null,
-      },
-    },
-  },
-  isApplyDisabled: false,
 });
 
 test('should render', () => {
@@ -55,48 +37,6 @@ test('should render the "Filters" heading', () => {
   expect(screen.getByText('Filters')).toBeInTheDocument();
 });
 
-test('should render the "Clear all" option', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  expect(screen.getByText('Clear all')).toBeInTheDocument();
-});
-
-test('should render the "Apply" button', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  expect(screen.getByText('Apply')).toBeInTheDocument();
-  expect(screen.getByText('Apply').parentElement).toBeEnabled();
-});
-
-test('should render the "Clear all" button as disabled', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  const clearBtn = screen.getByText('Clear all');
-  expect(clearBtn.parentElement).toBeDisabled();
-});
-
-test('should render the "Apply" button as disabled', () => {
-  const mockedProps = createProps();
-  const applyDisabledProps = {
-    ...mockedProps,
-    isApplyDisabled: true,
-  };
-  render(<Header {...applyDisabledProps} />, { useRedux: true });
-  const applyBtn = screen.getByText('Apply');
-  expect(applyBtn.parentElement).toBeDisabled();
-  userEvent.click(applyBtn);
-  expect(mockedProps.onApply).not.toHaveBeenCalled();
-});
-
-test('should apply', () => {
-  const mockedProps = createProps();
-  render(<Header {...mockedProps} />, { useRedux: true });
-  const applyBtn = screen.getByText('Apply');
-  expect(mockedProps.onApply).not.toHaveBeenCalled();
-  userEvent.click(applyBtn);
-  expect(mockedProps.onApply).toHaveBeenCalled();
-});
-
 test('should render the expand button', () => {
   const mockedProps = createProps();
   render(<Header {...mockedProps} />, { useRedux: true });
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx
index 2aac16e..86895b6 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx
@@ -17,22 +17,15 @@
  * under the License.
  */
 /* eslint-disable no-param-reassign */
-import {
-  DataMaskState,
-  DataMaskStateWithId,
-  Filter,
-  styled,
-  t,
-  useTheme,
-} from '@superset-ui/core';
+import { css, Filter, styled, t, useTheme } from '@superset-ui/core';
 import React, { FC } from 'react';
 import Icons from 'src/components/Icons';
 import Button from 'src/components/Button';
 import { useSelector } from 'react-redux';
 import FilterConfigurationLink from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink';
 import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state';
+import { RootState } from 'src/dashboard/types';
 import { getFilterBarTestId } from '..';
-import { RootState } from '../../../../types';
 
 const TitleArea = styled.h4`
   display: flex;
@@ -46,20 +39,6 @@ const TitleArea = styled.h4`
   }
 `;
 
-const ActionButtons = styled.div`
-  display: grid;
-  flex-direction: row;
-  justify-content: center;
-  align-items: center;
-  grid-gap: 10px;
-  grid-template-columns: 1fr 1fr;
-  ${({ theme }) => `padding: 0 ${theme.gridUnit * 2}px`};
-
-  .btn {
-    flex: 1;
-  }
-`;
-
 const HeaderButton = styled(Button)`
   padding: 0;
 `;
@@ -71,21 +50,28 @@ const Wrapper = styled.div`
 
 type HeaderProps = {
   toggleFiltersBar: (arg0: boolean) => void;
-  onApply: () => void;
-  onClearAll: () => void;
-  dataMaskSelected: DataMaskState;
-  dataMaskApplied: DataMaskStateWithId;
-  isApplyDisabled: boolean;
 };
 
-const Header: FC<HeaderProps> = ({
-  onApply,
-  onClearAll,
-  isApplyDisabled,
-  dataMaskSelected,
-  dataMaskApplied,
-  toggleFiltersBar,
-}) => {
+const AddFiltersButtonContainer = styled.div`
+  ${({ theme }) => css`
+    margin-top: ${theme.gridUnit * 2}px;
+
+    & button > [role='img']:first-of-type {
+      margin-right: ${theme.gridUnit}px;
+      line-height: 0;
+    }
+
+    span[role='img'] {
+      padding-bottom: 1px;
+    }
+
+    .ant-btn > .anticon + span {
+      margin-left: 0;
+    }
+  `}
+`;
+
+const Header: FC<HeaderProps> = ({ toggleFiltersBar }) => {
   const theme = useTheme();
   const filters = useFilters();
   const filterValues = Object.values<Filter>(filters);
@@ -96,27 +82,10 @@ const Header: FC<HeaderProps> = ({
     ({ dashboardInfo }) => dashboardInfo.id,
   );
 
-  const isClearAllDisabled = Object.values(dataMaskApplied).every(
-    filter =>
-      dataMaskSelected[filter.id]?.filterState?.value === null ||
-      (!dataMaskSelected[filter.id] && filter.filterState?.value === null),
-  );
-
   return (
     <Wrapper>
       <TitleArea>
         <span>{t('Filters')}</span>
-        {canEdit && (
-          <FilterConfigurationLink
-            dashboardId={dashboardId}
-            createNewOnOpen={filterValues.length === 0}
-          >
-            <Icons.Edit
-              data-test="create-filter"
-              iconColor={theme.colors.grayscale.base}
-            />
-          </FilterConfigurationLink>
-        )}
         <HeaderButton
           {...getFilterBarTestId('collapse-button')}
           buttonStyle="link"
@@ -126,29 +95,16 @@ const Header: FC<HeaderProps> = ({
           <Icons.Expand iconColor={theme.colors.grayscale.base} />
         </HeaderButton>
       </TitleArea>
-      <ActionButtons className="filter-action-buttons">
-        <Button
-          disabled={isClearAllDisabled}
-          buttonStyle="tertiary"
-          buttonSize="small"
-          className="filter-clear-all-button"
-          onClick={onClearAll}
-          {...getFilterBarTestId('clear-button')}
-        >
-          {t('Clear all')}
-        </Button>
-        <Button
-          disabled={isApplyDisabled}
-          buttonStyle="primary"
-          htmlType="submit"
-          buttonSize="small"
-          className="filter-apply-button"
-          onClick={onApply}
-          {...getFilterBarTestId('apply-button')}
-        >
-          {t('Apply')}
-        </Button>
-      </ActionButtons>
+      {canEdit && (
+        <AddFiltersButtonContainer>
+          <FilterConfigurationLink
+            dashboardId={dashboardId}
+            createNewOnOpen={filterValues.length === 0}
+          >
+            <Icons.PlusSmall /> {t('Add/Edit Filters')}
+          </FilterConfigurationLink>
+        </AddFiltersButtonContainer>
+      )}
     </Wrapper>
   );
 };
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
index d324984..e035ee0 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
@@ -61,6 +61,7 @@ import EditSection from './FilterSets/EditSection';
 import Header from './Header';
 import FilterControls from './FilterControls/FilterControls';
 import { RootState } from '../../../types';
+import { ActionButtons } from './ActionButtons';
 
 export const FILTER_BAR_TEST_ID = 'filter-bar';
 export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID);
@@ -168,7 +169,7 @@ const publishDataMask = debounce(
     const { search } = location;
     const previousParams = new URLSearchParams(search);
     const newParams = new URLSearchParams();
-    let dataMaskKey = '';
+    let dataMaskKey: string;
     previousParams.forEach((value, key) => {
       if (key !== URL_PARAMS.nativeFilters.name) {
         newParams.append(key, value);
@@ -254,7 +255,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
         };
       });
     },
-    [dataMaskSelected, dispatch, setDataMaskSelected, tab],
+    [dataMaskSelected, dispatch, setDataMaskSelected],
   );
 
   useEffect(() => {
@@ -284,10 +285,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({
     }
   }, [JSON.stringify(filters), JSON.stringify(previousFilters)]);
 
-  useEffect(() => {
-    setDataMaskSelected(() => dataMaskApplied);
-  }, [JSON.stringify(dataMaskApplied), setDataMaskSelected]);
-
   const dataMaskAppliedText = JSON.stringify(dataMaskApplied);
   useEffect(() => {
     publishDataMask(history, dashboardId, updateKey, dataMaskApplied, tabId);
@@ -309,9 +306,14 @@ const FilterBar: React.FC<FiltersBarProps> = ({
     filterIds.forEach(filterId => {
       if (dataMaskSelected[filterId]) {
         dispatch(clearDataMask(filterId));
+        setDataMaskSelected(draft => {
+          if (draft[filterId].filterState?.value !== undefined) {
+            draft[filterId].filterState!.value = undefined;
+          }
+        });
       }
     });
-  }, [dataMaskSelected, dispatch]);
+  }, [dataMaskSelected, dispatch, setDataMaskSelected]);
 
   const openFiltersBar = useCallback(
     () => toggleFiltersBar(true),
@@ -350,14 +352,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
         <StyledFilterIcon {...getFilterBarTestId('filter-icon')} iconSize="l" />
       </CollapsedBar>
       <Bar className={cx({ open: filtersOpen })} width={width}>
-        <Header
-          toggleFiltersBar={toggleFiltersBar}
-          onApply={handleApply}
-          onClearAll={handleClearAll}
-          isApplyDisabled={isApplyDisabled}
-          dataMaskSelected={dataMaskSelected}
-          dataMaskApplied={dataMaskApplied}
-        />
+        <Header toggleFiltersBar={toggleFiltersBar} />
         {!isInitialized ? (
           <div css={{ height }}>
             <Loading />
@@ -384,11 +379,26 @@ const FilterBar: React.FC<FiltersBarProps> = ({
                   filterSetId={editFilterSetId}
                 />
               )}
-              <FilterControls
-                dataMaskSelected={dataMaskSelected}
-                directPathToChild={directPathToChild}
-                onFilterSelectionChange={handleFilterSelectionChange}
-              />
+              {filterValues.length === 0 ? (
+                <FilterBarEmptyStateContainer>
+                  <EmptyStateSmall
+                    title={t('No filters are currently added')}
+                    image="filter.svg"
+                    description={
+                      canEdit &&
+                      t(
+                        'Click the button above to add a filter to the dashboard',
+                      )
+                    }
+                  />
+                </FilterBarEmptyStateContainer>
+              ) : (
+                <FilterControls
+                  dataMaskSelected={dataMaskSelected}
+                  directPathToChild={directPathToChild}
+                  onFilterSelectionChange={handleFilterSelectionChange}
+                />
+              )}
             </Tabs.TabPane>
             <Tabs.TabPane
               disabled={!!editFilterSetId}
@@ -416,9 +426,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
                   image="filter.svg"
                   description={
                     canEdit &&
-                    t(
-                      'Click the pencil icon above to add a filter to the dashboard',
-                    )
+                    t('Click the button above to add a filter to the dashboard')
                   }
                 />
               </FilterBarEmptyStateContainer>
@@ -431,6 +439,13 @@ const FilterBar: React.FC<FiltersBarProps> = ({
             )}
           </div>
         )}
+        <ActionButtons
+          onApply={handleApply}
+          onClearAll={handleClearAll}
+          dataMaskSelected={dataMaskSelected}
+          dataMaskApplied={dataMaskApplied}
+          isApplyDisabled={isApplyDisabled}
+        />
       </Bar>
     </BarWrapper>
   );
diff --git a/superset-frontend/src/dashboard/constants.ts b/superset-frontend/src/dashboard/constants.ts
index ab3fa29..169a2d0 100644
--- a/superset-frontend/src/dashboard/constants.ts
+++ b/superset-frontend/src/dashboard/constants.ts
@@ -33,3 +33,12 @@ export const PLACEHOLDER_DATASOURCE: Datasource = {
   main_dttm_col: '',
   description: '',
 };
+
+export const MAIN_HEADER_HEIGHT = 53;
+export const TABS_HEIGHT = 50;
+export const HEADER_HEIGHT = 72;
+export const CLOSED_FILTER_BAR_WIDTH = 32;
+export const OPEN_FILTER_BAR_WIDTH = 260;
+export const FILTER_BAR_HEADER_HEIGHT = 80;
+export const FILTER_BAR_TABS_HEIGHT = 46;
+export const BUILDER_SIDEPANEL_WIDTH = 374;
diff --git a/superset-frontend/src/utils/common.js b/superset-frontend/src/utils/common.js
index 26fdfb4..4efdb20 100644
--- a/superset-frontend/src/utils/common.js
+++ b/superset-frontend/src/utils/common.js
@@ -144,3 +144,5 @@ export const detectOS = () => {
 
   return 'Unknown OS';
 };
+
+export const isNullish = value => value === null || value === undefined;