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

graceguo pushed a commit to branch feature--dashboard-scoped-filter
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to 
refs/heads/feature--dashboard-scoped-filter by this push:
     new e36ef96  [WIP][dashboard scoped filter] part 1: scope selector modal 
(#8557)
e36ef96 is described below

commit e36ef965074d7cf8e92e9559ba42d70bc40178e2
Author: Grace Guo <grace....@airbnb.com>
AuthorDate: Wed Nov 13 13:49:32 2019 -0800

    [WIP][dashboard scoped filter] part 1: scope selector modal (#8557)
    
    * filter scope selector modal
    
    * add single-field-edit in multi-edit mode switch
    
    * fix code review comments (round 1)
    
    * refactory after design review
    
    * fix a few props initial value
---
 superset/assets/package-lock.json                  |  11 +
 superset/assets/package.json                       |   1 +
 .../dashboard/fixtures/mockDashboardFilters.js     |   5 +-
 .../dashboard/fixtures/mockDashboardLayout.js      |   2 +-
 .../dashboard/reducers/dashboardFilters_spec.js    |  18 +-
 .../index.less => components/ChartIcon.jsx}        |  24 +-
 superset/assets/src/components/CheckboxIcons.jsx   |  40 ++
 superset/assets/src/components/ModalTrigger.jsx    |   2 +
 .../dashboard/components/DeleteComponentModal.jsx  |   4 +-
 .../components/FilterIndicatorsContainer.jsx       |  11 +-
 .../assets/src/dashboard/components/Header.jsx     |   9 +-
 .../components/filterscope/FilterFieldItem.jsx}    |  48 +-
 .../components/filterscope/FilterFieldTree.jsx     |  94 ++++
 .../components/filterscope/FilterScopeModal.jsx    |  59 +++
 .../components/filterscope/FilterScopeSelector.jsx | 518 +++++++++++++++++++++
 .../components/filterscope/FilterScopeTree.jsx     |  94 ++++
 .../filterscope/renderFilterFieldTreeNodes.jsx}    |  57 +--
 .../filterscope/renderFilterScopeTreeNodes.jsx     |  74 +++
 .../dashboard/containers/FilterScope.jsx}          |  51 +-
 .../src/dashboard/reducers/dashboardFilters.js     |  22 +-
 .../src/dashboard/reducers/getInitialState.js      |   5 +-
 .../src/dashboard/stylesheets/dashboard.less       |  17 +-
 .../stylesheets/filter-scope-selector.less         | 241 ++++++++++
 .../assets/src/dashboard/stylesheets/index.less    |   1 +
 .../src/dashboard/util/activeDashboardFilters.js   |  17 +
 .../dashboard/util/buildFilterScopeTreeEntry.js    |  65 +++
 superset/assets/src/dashboard/util/constants.js    |   3 +
 .../src/dashboard/util/dashboardFiltersColorMap.js |   8 +-
 .../src/dashboard/util/getCurrentScopeChartIds.js  |  62 +++
 .../index.less => util/getDashboardFilterKey.js}   |  21 +-
 .../dashboard/util/getFilterFieldNodesTree.js}     |  50 +-
 .../src/dashboard/util/getFilterScopeNodesTree.js  | 128 +++++
 .../getFilterScopeParentNodes.js}                  |  33 +-
 .../getKeyForFilterScopeTree.js}                   |  22 +-
 .../index.less => util/getRevertedFilterScope.js}  |  36 +-
 .../util/getSelectedChartIdForFilterScopeTree.js   |  53 +++
 superset/assets/src/dashboard/util/propShapes.jsx  |  29 +-
 .../src/visualizations/FilterBox/FilterBox.css     |   2 +-
 .../src/visualizations/FilterBox/FilterBox.jsx     |   5 +-
 39 files changed, 1742 insertions(+), 200 deletions(-)

diff --git a/superset/assets/package-lock.json 
b/superset/assets/package-lock.json
index 743952c..5e8aac3 100644
--- a/superset/assets/package-lock.json
+++ b/superset/assets/package-lock.json
@@ -20244,6 +20244,17 @@
         }
       }
     },
+    "react-checkbox-tree": {
+      "version": "1.5.1",
+      "resolved": 
"https://registry.npmjs.org/react-checkbox-tree/-/react-checkbox-tree-1.5.1.tgz";,
+      "integrity": 
"sha512-fBLMVpd7/YXavzIBz+3OMS5eo2oZLW9PlTY4M1zrJ3TdZRzgILicSzRj6V5VKKm80y8uQXn60skn98pwn3i3Ig==",
+      "requires": {
+        "classnames": "^2.2.5",
+        "lodash": "^4.17.10",
+        "nanoid": "^2.0.0",
+        "prop-types": "^15.5.8"
+      }
+    },
     "react-color": {
       "version": "2.14.1",
       "resolved": 
"https://registry.npmjs.org/react-color/-/react-color-2.14.1.tgz";,
diff --git a/superset/assets/package.json b/superset/assets/package.json
index c286ee7..42b56b5 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -117,6 +117,7 @@
     "react-bootstrap": "^0.31.5",
     "react-bootstrap-dialog": "^0.10.0",
     "react-bootstrap-slider": "2.1.5",
+    "react-checkbox-tree": "^1.5.1",
     "react-color": "^2.13.8",
     "react-datetime": "^2.14.0",
     "react-dnd": "^2.5.4",
diff --git 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js 
b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
index e72e13e..7f13db7 100644
--- 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
+++ 
b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { filterId } from './mockSliceEntities';
+import { DASHBOARD_FILTER_SCOPE_GLOBAL } from 
'../../../../src/dashboard/reducers/dashboardFilters';
 
 export const emptyFilters = {};
 
@@ -33,7 +34,9 @@ export const dashboardFilters = {
       'ROW-l6PrlhwSjh',
       'CHART-rwDfbGqeEn',
     ],
-    scope: 'ROOT_ID',
+    scopes: {
+      region: DASHBOARD_FILTER_SCOPE_GLOBAL,
+    },
     isDateFilter: false,
     isInstantFilter: true,
     columns: {
diff --git 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js 
b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
index aad4445..3267c5c 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
@@ -204,6 +204,6 @@ export const filterComponent = {
     chartId: filterId,
     width: 3,
     height: 10,
-    chartName: 'Filter',
+    sliceName: 'Filter',
   },
 };
diff --git 
a/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js 
b/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
index 88c6714..e234248 100644
--- 
a/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
+++ 
b/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
@@ -22,7 +22,9 @@ import {
   REMOVE_FILTER,
   CHANGE_FILTER,
 } from '../../../../src/dashboard/actions/dashboardFilters';
-import dashboardFiltersReducer from 
'../../../../src/dashboard/reducers/dashboardFilters';
+import dashboardFiltersReducer, {
+  DASHBOARD_FILTER_SCOPE_GLOBAL,
+} from '../../../../src/dashboard/reducers/dashboardFilters';
 import {
   emptyFilters,
   dashboardFilters,
@@ -33,7 +35,6 @@ import {
   column,
 } from '../fixtures/mockSliceEntities';
 import { filterComponent } from '../fixtures/mockDashboardLayout';
-import { DASHBOARD_ROOT_ID } from '../../../../src/dashboard/util/constants';
 
 describe('dashboardFilters reducer', () => {
   const form_data = sliceEntitiesForDashboard.slices[filterId].form_data;
@@ -54,7 +55,7 @@ describe('dashboardFilters reducer', () => {
         chartId: filterId,
         componentId: component.id,
         directPathToFilter,
-        scope: DASHBOARD_ROOT_ID,
+        filterName: component.meta.sliceName,
         isDateFilter: false,
         isInstantFilter: !!form_data.instant_filtering,
         columns: {
@@ -63,6 +64,9 @@ describe('dashboardFilters reducer', () => {
         labels: {
           [column]: column,
         },
+        scopes: {
+          [column]: DASHBOARD_FILTER_SCOPE_GLOBAL,
+        },
       },
     });
   });
@@ -83,7 +87,6 @@ describe('dashboardFilters reducer', () => {
         chartId: filterId,
         componentId: component.id,
         directPathToFilter,
-        scope: DASHBOARD_ROOT_ID,
         isDateFilter: false,
         isInstantFilter: !!form_data.instant_filtering,
         columns: {
@@ -93,6 +96,9 @@ describe('dashboardFilters reducer', () => {
         labels: {
           [column]: column,
         },
+        scopes: {
+          [column]: DASHBOARD_FILTER_SCOPE_GLOBAL,
+        },
       },
     });
   });
@@ -113,7 +119,6 @@ describe('dashboardFilters reducer', () => {
         chartId: filterId,
         componentId: component.id,
         directPathToFilter,
-        scope: DASHBOARD_ROOT_ID,
         isDateFilter: false,
         isInstantFilter: !!form_data.instant_filtering,
         columns: {
@@ -123,6 +128,9 @@ describe('dashboardFilters reducer', () => {
         labels: {
           [column]: column,
         },
+        scopes: {
+          [column]: DASHBOARD_FILTER_SCOPE_GLOBAL,
+        },
       },
     });
   });
diff --git a/superset/assets/src/dashboard/stylesheets/index.less 
b/superset/assets/src/components/ChartIcon.jsx
similarity index 62%
copy from superset/assets/src/dashboard/stylesheets/index.less
copy to superset/assets/src/components/ChartIcon.jsx
index 01a0e3c..d25e453 100644
--- a/superset/assets/src/dashboard/stylesheets/index.less
+++ b/superset/assets/src/components/ChartIcon.jsx
@@ -16,17 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-@import './variables.less';
+import React from 'react';
 
-@import './builder.less';
-@import './builder-sidepane.less';
-@import './buttons.less';
-@import './dashboard.less';
-@import './dnd.less';
-@import './filter-indicator.less';
-@import './filter-indicator-tooltip.less';
-@import './grid.less';
-@import './hover-menu.less';
-@import './popover-menu.less';
-@import './resizable.less';
-@import './components/index.less';
+const ChartIcon = () => (
+  <svg width="18" height="18" viewBox="0 0 18 18" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+    <rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#EFF9F9" 
stroke="#B3DADC" />
+    <rect x="8" y="4" width="2" height="10" rx="1" fill="#B3DADC" />
+    <rect x="12" y="10" width="2" height="4" rx="1" fill="#B3DADC" />
+    <rect x="4" y="6" width="2" height="8" rx="1" fill="#B3DADC" />
+  </svg>
+);
+
+export default ChartIcon;
diff --git a/superset/assets/src/components/CheckboxIcons.jsx 
b/superset/assets/src/components/CheckboxIcons.jsx
new file mode 100644
index 0000000..f26e752
--- /dev/null
+++ b/superset/assets/src/components/CheckboxIcons.jsx
@@ -0,0 +1,40 @@
+/**
+ * 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 from 'react';
+
+export const CheckboxChecked = () => (
+  <svg width="18" height="18" viewBox="0 0 18 18" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+    <path d="M16 0H2C0.89 0 0 0.9 0 2V16C0 17.1 0.89 18 2 18H16C17.11 18 18 
17.1 18 16V2C18 0.9 17.11 0 16 0Z" fill="#00A699" />
+    <path d="M7 14L2 9L3.41 7.59L7 11.17L14.59 3.58L16 5L7 14Z" fill="white" />
+  </svg>
+);
+
+export const CheckboxHalfChecked = () => (
+  <svg width="18" height="18" viewBox="0 0 18 18" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+    <path d="M16 0H2C0.9 0 0 0.9 0 2V16C0 17.1 0.9 18 2 18H16C17.1 18 18 17.1 
18 16V2C18 0.9 17.1 0 16 0Z" fill="#999999" />
+    <path d="M14 10H4V8H14V10Z" fill="white" />
+  </svg>
+);
+
+export const CheckboxUnchecked = () => (
+  <svg width="18" height="18" viewBox="0 0 18 18" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+    <path d="M16 0H2C0.9 0 0 0.9 0 2V16C0 17.1 0.9 18 2 18H16C17.1 18 18 17.1 
18 16V2C18 0.9 17.1 0 16 0Z" fill="#CCCCCC" />
+    <path d="M16 2V16H2V2H16V2Z" fill="white" />
+  </svg>
+);
diff --git a/superset/assets/src/components/ModalTrigger.jsx 
b/superset/assets/src/components/ModalTrigger.jsx
index 750be52..8f363f5 100644
--- a/superset/assets/src/components/ModalTrigger.jsx
+++ b/superset/assets/src/components/ModalTrigger.jsx
@@ -24,6 +24,7 @@ import cx from 'classnames';
 import Button from './Button';
 
 const propTypes = {
+  dialogClassName: PropTypes.string,
   animation: PropTypes.bool,
   triggerNode: PropTypes.node.isRequired,
   modalTitle: PropTypes.node,
@@ -72,6 +73,7 @@ export default class ModalTrigger extends React.Component {
   renderModal() {
     return (
       <Modal
+        dialogClassName={this.props.dialogClassName}
         animation={this.props.animation}
         show={this.state.showModal}
         onHide={this.close}
diff --git a/superset/assets/src/dashboard/components/DeleteComponentModal.jsx 
b/superset/assets/src/dashboard/components/DeleteComponentModal.jsx
index 366e78a..1d50017 100644
--- a/superset/assets/src/dashboard/components/DeleteComponentModal.jsx
+++ b/superset/assets/src/dashboard/components/DeleteComponentModal.jsx
@@ -57,14 +57,14 @@ export default class DeleteComponentModal extends 
React.PureComponent {
         ref={this.setModalRef}
         triggerNode={this.props.triggerNode}
         modalBody={
-          <div className="delete-component-modal">
+          <div className="dashboard-modal delete">
             <h1>{t('Delete dashboard tab?')}</h1>
             <div>
               Deleting a tab will remove all content within it. You may still
               reverse this action with the <b>undo</b> button (cmd + z) until
               you save your changes.
             </div>
-            <div className="delete-modal-actions-container">
+            <div className="dashboard-modal-actions-container">
               <Button onClick={this.close}>{t('Cancel')}</Button>
               <Button bsStyle="primary" onClick={this.deleteTab}>
                 {t('Delete')}
diff --git 
a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx 
b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
index f84e32d..f287108 100644
--- a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
+++ b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
@@ -23,10 +23,8 @@ import { isEmpty } from 'lodash';
 import FilterIndicator from './FilterIndicator';
 import FilterIndicatorGroup from './FilterIndicatorGroup';
 import { FILTER_INDICATORS_DISPLAY_LENGTH } from '../util/constants';
-import {
-  getFilterColorKey,
-  getFilterColorMap,
-} from '../util/dashboardFiltersColorMap';
+import { getDashboardFilterKey } from '../util/getDashboardFilterKey';
+import { getFilterColorMap } from '../util/dashboardFiltersColorMap';
 
 const propTypes = {
   // from props
@@ -92,7 +90,10 @@ export default class FilterIndicatorsContainer extends 
React.PureComponent {
           !filterImmuneSlices.includes(currentChartId)
         ) {
           Object.keys(columns).forEach(name => {
-            const colorMapKey = getFilterColorKey(chartId, name);
+            const colorMapKey = getDashboardFilterKey({
+              chartId,
+              column: name,
+            });
             const directPathToLabel = directPathToFilter.slice();
             directPathToLabel.push(`LABEL-${name}`);
             const indicator = {
diff --git a/superset/assets/src/dashboard/components/Header.jsx 
b/superset/assets/src/dashboard/components/Header.jsx
index b807273..569dac9 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -26,6 +26,7 @@ import HeaderActionsDropdown from './HeaderActionsDropdown';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
+import FilterScopeModal from './filterscope/FilterScopeModal';
 import PublishedStatus from './PublishedStatus';
 import UndoRedoKeylisteners from './UndoRedoKeylisteners';
 
@@ -347,7 +348,7 @@ class Header extends React.PureComponent {
                   bsSize="small"
                   onClick={this.onInsertComponentsButtonClick}
                 >
-                  {t('Insert components')}
+                  {t('Components')}
                 </Button>
               )}
 
@@ -361,6 +362,12 @@ class Header extends React.PureComponent {
                 </Button>
               )}
 
+              {editMode && (
+                <FilterScopeModal
+                  triggerNode={<Button bsSize="small">{t('Filters')}</Button>}
+                />
+              )}
+
               {editMode && hasUnsavedChanges && (
                 <Button
                   bsSize="small"
diff --git 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js 
b/superset/assets/src/dashboard/components/filterscope/FilterFieldItem.jsx
similarity index 55%
copy from 
superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
copy to superset/assets/src/dashboard/components/filterscope/FilterFieldItem.jsx
index e72e13e..fb3689d 100644
--- 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
+++ b/superset/assets/src/dashboard/components/filterscope/FilterFieldItem.jsx
@@ -16,31 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { filterId } from './mockSliceEntities';
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
 
-export const emptyFilters = {};
+import FilterBadgeIcon from '../../../components/FilterBadgeIcon';
 
-export const dashboardFilters = {
-  [filterId]: {
-    chartId: filterId,
-    componentId: 'CHART-rwDfbGqeEn',
-    directPathToFilter: [
-      'ROOT_ID',
-      'TABS-VPEX_c476g',
-      'TAB-PMJyKM1yB',
-      'TABS-YdylzDMTMQ',
-      'TAB-O9AaU9FT0',
-      'ROW-l6PrlhwSjh',
-      'CHART-rwDfbGqeEn',
-    ],
-    scope: 'ROOT_ID',
-    isDateFilter: false,
-    isInstantFilter: true,
-    columns: {
-      region: ['a', 'b'],
-    },
-    labels: {
-      region: 'region',
-    },
-  },
+const propTypes = {
+  label: PropTypes.string.isRequired,
+  colorCode: PropTypes.string.isRequired,
+  isSelected: PropTypes.bool.isRequired,
 };
+
+export default function FilterFieldItem({ label, colorCode, isSelected }) {
+  return (
+    <a
+      className={cx('filter-field-item filter-container', {
+        'is-selected': isSelected,
+      })}
+    >
+      <FilterBadgeIcon colorCode={colorCode} />
+      <label htmlFor={label}>{label}</label>
+    </a>
+  );
+}
+
+FilterFieldItem.propTypes = propTypes;
diff --git 
a/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx 
b/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx
new file mode 100644
index 0000000..7b778ed
--- /dev/null
+++ b/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx
@@ -0,0 +1,94 @@
+/**
+ * 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 from 'react';
+import PropTypes from 'prop-types';
+import CheckboxTree from 'react-checkbox-tree';
+import { t } from '@superset-ui/translation';
+
+import 'react-checkbox-tree/lib/react-checkbox-tree.css';
+import {
+  CheckboxChecked,
+  CheckboxUnchecked,
+  CheckboxHalfChecked,
+} from '../../../components/CheckboxIcons';
+import renderFilterFieldTreeNodes from './renderFilterFieldTreeNodes';
+import { filterScopeSelectorTreeNodePropShape } from '../../util/propShapes';
+
+const propTypes = {
+  activeKey: PropTypes.oneOfType([null, PropTypes.string]),
+  nodes: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired,
+  checked: PropTypes.arrayOf(
+    PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+  ).isRequired,
+  expanded: PropTypes.arrayOf(
+    PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+  ).isRequired,
+  onCheck: PropTypes.func.isRequired,
+  onExpand: PropTypes.func.isRequired,
+  onClick: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  activeKey: null,
+};
+
+const FILTER_FIELD_CHECKBOX_TREE_ICONS = {
+  check: <CheckboxChecked />,
+  uncheck: <CheckboxUnchecked />,
+  halfCheck: <CheckboxHalfChecked />,
+  expandClose: <span className="rct-icon rct-icon-expand-close" />,
+  expandOpen: <span className="rct-icon rct-icon-expand-open" />,
+  expandAll: (
+    <span className="rct-icon rct-icon-expand-all">{t('Expand all')}</span>
+  ),
+  collapseAll: (
+    <span className="rct-icon rct-icon-collapse-all">{t('Collapse all')}</span>
+  ),
+  parentClose: <span className="rct-icon rct-icon-parent-close" />,
+  parentOpen: <span className="rct-icon rct-icon-parent-open" />,
+  leaf: <span className="rct-icon rct-icon-leaf" />,
+};
+
+export default function FilterFieldTree({
+  activeKey,
+  nodes = [],
+  checked = [],
+  expanded = [],
+  onClick,
+  onCheck,
+  onExpand,
+}) {
+  return (
+    <CheckboxTree
+      showExpandAll
+      showNodeIcon={false}
+      expandOnClick
+      nodes={renderFilterFieldTreeNodes({ nodes, activeKey })}
+      checked={checked}
+      expanded={expanded}
+      onClick={onClick}
+      onCheck={onCheck}
+      onExpand={onExpand}
+      icons={FILTER_FIELD_CHECKBOX_TREE_ICONS}
+    />
+  );
+}
+
+FilterFieldTree.propTypes = propTypes;
+FilterFieldTree.defaultProps = defaultProps;
diff --git 
a/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx 
b/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx
new file mode 100644
index 0000000..b2a8be8
--- /dev/null
+++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx
@@ -0,0 +1,59 @@
+/**
+ * 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 from 'react';
+import PropTypes from 'prop-types';
+
+import ModalTrigger from '../../../components/ModalTrigger';
+import FilterScope from '../../containers/FilterScope';
+
+const propTypes = {
+  triggerNode: PropTypes.node.isRequired,
+};
+
+export default class FilterScopeModal extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.modal = React.createRef();
+    this.handleCloseModal = this.handleCloseModal.bind(this);
+  }
+
+  handleCloseModal() {
+    if (this.modal) {
+      this.modal.current.close();
+    }
+  }
+
+  render() {
+    return (
+      <ModalTrigger
+        dialogClassName="filter-scope-modal"
+        ref={this.modal}
+        triggerNode={this.props.triggerNode}
+        modalBody={
+          <div className="dashboard-modal filter-scope">
+            <FilterScope onCloseModal={this.handleCloseModal} />
+          </div>
+        }
+      />
+    );
+  }
+}
+
+FilterScopeModal.propTypes = propTypes;
diff --git 
a/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx 
b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx
new file mode 100644
index 0000000..ae42a57
--- /dev/null
+++ 
b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx
@@ -0,0 +1,518 @@
+/**
+ * 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 from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { Button } from 'react-bootstrap';
+import { t } from '@superset-ui/translation';
+
+import buildFilterScopeTreeEntry from '../../util/buildFilterScopeTreeEntry';
+import getFilterScopeNodesTree from '../../util/getFilterScopeNodesTree';
+import getFilterFieldNodesTree from '../../util/getFilterFieldNodesTree';
+import getFilterScopeParentNodes from '../../util/getFilterScopeParentNodes';
+import getCurrentScopeChartIds from '../../util/getCurrentScopeChartIds';
+import getKeyForFilterScopeTree from '../../util/getKeyForFilterScopeTree';
+import getSelectedChartIdForFilterScopeTree from 
'../../util/getSelectedChartIdForFilterScopeTree';
+import getRevertedFilterScope from '../../util/getRevertedFilterScope';
+import FilterScopeTree from './FilterScopeTree';
+import FilterFieldTree from './FilterFieldTree';
+import {
+  getChartIdAndColumnFromFilterKey,
+  getDashboardFilterKey,
+} from '../../util/getDashboardFilterKey';
+import { ALL_FILTERS_ROOT } from '../../util/constants';
+
+const propTypes = {
+  dashboardFilters: PropTypes.object.isRequired,
+  layout: PropTypes.object.isRequired,
+  filterImmuneSlices: PropTypes.arrayOf(PropTypes.number).isRequired,
+  filterImmuneSliceFields: PropTypes.object.isRequired,
+  setDirectPathToChild: PropTypes.func.isRequired,
+  onCloseModal: PropTypes.func.isRequired,
+};
+
+export default class FilterScopeSelector extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    const {
+      dashboardFilters,
+      filterImmuneSlices,
+      filterImmuneSliceFields,
+      layout,
+    } = props;
+
+    if (Object.keys(dashboardFilters).length > 0) {
+      // display filter fields in tree structure
+      const filterFieldNodes = getFilterFieldNodesTree({
+        dashboardFilters,
+      });
+      // filterFieldNodes root node is dashboard_root component,
+      // so that we can offer a select/deselect all link
+      const filtersNodes = filterFieldNodes[0].children;
+      this.allfilterFields = [];
+      filtersNodes.forEach(({ children }) => {
+        children.forEach(child => {
+          this.allfilterFields.push(child.value);
+        });
+      });
+      this.defaultFilterKey = filtersNodes[0].children[0].value;
+
+      // build FilterScopeTree object for each filterKey
+      const filterScopeMap = Object.values(dashboardFilters).reduce(
+        (map, { chartId: filterId, columns }) => {
+          const filterScopeByChartId = Object.keys(columns).reduce(
+            (mapByChartId, columnName) => {
+              const filterKey = getDashboardFilterKey({
+                chartId: filterId,
+                column: columnName,
+              });
+              const nodes = getFilterScopeNodesTree({
+                components: layout,
+                filterFields: [filterKey],
+                selectedChartId: filterId,
+              });
+              const checked = getCurrentScopeChartIds({
+                scopeComponentIds: ['ROOT_ID'], // 
dashboardFilters[chartId].scopes[columnName],
+                filterField: columnName,
+                filterImmuneSlices,
+                filterImmuneSliceFields,
+                components: layout,
+              });
+              const expanded = getFilterScopeParentNodes(nodes, 1);
+              return {
+                ...mapByChartId,
+                [filterKey]: {
+                  // unfiltered nodes
+                  nodes,
+                  // filtered nodes in display if searchText is not empty
+                  nodesFiltered: [...nodes],
+                  checked,
+                  expanded,
+                },
+              };
+            },
+            {},
+          );
+
+          return {
+            ...map,
+            ...filterScopeByChartId,
+          };
+        },
+        {},
+      );
+
+      // initial state: active defaultFilerKey
+      const { chartId } = getChartIdAndColumnFromFilterKey(
+        this.defaultFilterKey,
+      );
+      const checkedFilterFields = [];
+      const activeFilterField = this.defaultFilterKey;
+      // expand defaultFilterKey in filter field tree
+      const expandedFilterIds = [ALL_FILTERS_ROOT].concat(chartId);
+
+      const filterScopeTreeEntry = buildFilterScopeTreeEntry({
+        checkedFilterFields,
+        activeFilterField,
+        filterScopeMap,
+        layout,
+      });
+      this.state = {
+        showSelector: true,
+        activeFilterField,
+        searchText: '',
+        filterScopeMap: {
+          ...filterScopeMap,
+          ...filterScopeTreeEntry,
+        },
+        filterFieldNodes,
+        checkedFilterFields,
+        expandedFilterIds,
+      };
+    } else {
+      this.state = {
+        showSelector: false,
+      };
+    }
+
+    this.filterNodes = this.filterNodes.bind(this);
+    this.onChangeFilterField = this.onChangeFilterField.bind(this);
+    this.onCheckFilterScope = this.onCheckFilterScope.bind(this);
+    this.onExpandFilterScope = this.onExpandFilterScope.bind(this);
+    this.onSearchInputChange = this.onSearchInputChange.bind(this);
+    this.onCheckFilterField = this.onCheckFilterField.bind(this);
+    this.onExpandFilterField = this.onExpandFilterField.bind(this);
+    this.onClose = this.onClose.bind(this);
+    this.onSave = this.onSave.bind(this);
+  }
+
+  onCheckFilterScope(checked = []) {
+    const {
+      activeFilterField,
+      filterScopeMap,
+      checkedFilterFields,
+    } = this.state;
+
+    const key = getKeyForFilterScopeTree({
+      activeFilterField,
+      checkedFilterFields,
+    });
+    const editingList = activeFilterField
+      ? [activeFilterField]
+      : checkedFilterFields;
+    const updatedEntry = {
+      ...filterScopeMap[key],
+      checked,
+    };
+
+    const updatedFilterScopeMap = getRevertedFilterScope({
+      checked,
+      filterFields: editingList,
+      filterScopeMap,
+    });
+
+    this.setState(() => ({
+      filterScopeMap: {
+        ...filterScopeMap,
+        ...updatedFilterScopeMap,
+        [key]: updatedEntry,
+      },
+    }));
+  }
+
+  onExpandFilterScope(expanded = []) {
+    const {
+      activeFilterField,
+      checkedFilterFields,
+      filterScopeMap,
+    } = this.state;
+    const key = getKeyForFilterScopeTree({
+      activeFilterField,
+      checkedFilterFields,
+    });
+    const updatedEntry = {
+      ...filterScopeMap[key],
+      expanded,
+    };
+    this.setState(() => ({
+      filterScopeMap: {
+        ...filterScopeMap,
+        [key]: updatedEntry,
+      },
+    }));
+  }
+
+  onCheckFilterField(checkedFilterFields = []) {
+    const { layout } = this.props;
+    const { filterScopeMap } = this.state;
+    const filterScopeTreeEntry = buildFilterScopeTreeEntry({
+      checkedFilterFields,
+      activeFilterField: null,
+      filterScopeMap,
+      layout,
+    });
+
+    this.setState(() => ({
+      activeFilterField: null,
+      checkedFilterFields,
+      filterScopeMap: {
+        ...filterScopeMap,
+        ...filterScopeTreeEntry,
+      },
+    }));
+  }
+
+  onExpandFilterField(expandedFilterIds = []) {
+    this.setState(() => ({
+      expandedFilterIds,
+    }));
+  }
+
+  onChangeFilterField(filterField = {}) {
+    const { layout } = this.props;
+    const nextActiveFilterField = filterField.value;
+    const {
+      activeFilterField: currentActiveFilterField,
+      checkedFilterFields,
+      filterScopeMap,
+    } = this.state;
+
+    // we allow single edit and multiple edit in the same view.
+    // if user click on the single filter field,
+    // will show filter scope for the single field.
+    // if user click on the same filter filed again,
+    // will toggle off the single filter field,
+    // and allow multi-edit all checked filter fields.
+    if (nextActiveFilterField === currentActiveFilterField) {
+      const filterScopeTreeEntry = buildFilterScopeTreeEntry({
+        checkedFilterFields,
+        activeFilterField: null,
+        filterScopeMap,
+        layout,
+      });
+
+      this.setState({
+        activeFilterField: null,
+        filterScopeMap: {
+          ...filterScopeMap,
+          ...filterScopeTreeEntry,
+        },
+      });
+    } else if (this.allfilterFields.includes(nextActiveFilterField)) {
+      const filterScopeTreeEntry = buildFilterScopeTreeEntry({
+        checkedFilterFields,
+        activeFilterField: nextActiveFilterField,
+        filterScopeMap,
+        layout,
+      });
+
+      this.setState({
+        activeFilterField: nextActiveFilterField,
+        filterScopeMap: {
+          ...filterScopeMap,
+          ...filterScopeTreeEntry,
+        },
+      });
+    }
+  }
+
+  onSearchInputChange(e) {
+    this.setState({ searchText: e.target.value }, this.filterTree);
+  }
+
+  onClose() {
+    this.props.onCloseModal();
+  }
+
+  onSave() {
+    const { filterScopeMap } = this.state;
+
+    console.log(
+      'i am current state',
+      this.allfilterFields.reduce(
+        (map, key) => ({
+          ...map,
+          [key]: filterScopeMap[key].checked,
+        }),
+        {},
+      ),
+    );
+
+    // save does not close modal
+  }
+
+  filterTree() {
+    // Reset nodes back to unfiltered state
+    if (!this.state.searchText) {
+      this.setState(prevState => {
+        const {
+          activeFilterField,
+          checkedFilterFields,
+          filterScopeMap,
+        } = prevState;
+        const key = getKeyForFilterScopeTree({
+          activeFilterField,
+          checkedFilterFields,
+        });
+
+        const updatedEntry = {
+          ...filterScopeMap[key],
+          nodesFiltered: filterScopeMap[key].nodes,
+        };
+        return {
+          filterScopeMap: {
+            ...filterScopeMap,
+            [key]: updatedEntry,
+          },
+        };
+      });
+    } else {
+      const updater = prevState => {
+        const {
+          activeFilterField,
+          checkedFilterFields,
+          filterScopeMap,
+        } = prevState;
+        const key = getKeyForFilterScopeTree({
+          activeFilterField,
+          checkedFilterFields,
+        });
+
+        const nodesFiltered = filterScopeMap[key].nodes.reduce(
+          this.filterNodes,
+          [],
+        );
+        const expanded = getFilterScopeParentNodes([...nodesFiltered]);
+        const updatedEntry = {
+          ...filterScopeMap[key],
+          nodesFiltered,
+          expanded,
+        };
+
+        return {
+          filterScopeMap: {
+            ...filterScopeMap,
+            [key]: updatedEntry,
+          },
+        };
+      };
+
+      this.setState(updater);
+    }
+  }
+
+  filterNodes(filtered = [], node = {}) {
+    const { searchText } = this.state;
+    const children = (node.children || []).reduce(this.filterNodes, []);
+
+    if (
+      // Node's label matches the search string
+      node.label.toLocaleLowerCase().indexOf(searchText.toLocaleLowerCase()) >
+        -1 ||
+      // Or a children has a matching node
+      children.length
+    ) {
+      filtered.push({ ...node, children });
+    }
+
+    return filtered;
+  }
+
+  renderFilterFieldList() {
+    const {
+      activeFilterField,
+      filterFieldNodes,
+      checkedFilterFields,
+      expandedFilterIds,
+    } = this.state;
+    return (
+      <FilterFieldTree
+        activeKey={activeFilterField}
+        nodes={filterFieldNodes}
+        checked={checkedFilterFields}
+        expanded={expandedFilterIds}
+        onClick={this.onChangeFilterField}
+        onCheck={this.onCheckFilterField}
+        onExpand={this.onExpandFilterField}
+      />
+    );
+  }
+
+  renderFilterScopeTree() {
+    const {
+      filterScopeMap,
+      activeFilterField,
+      checkedFilterFields,
+      searchText,
+    } = this.state;
+
+    const key = getKeyForFilterScopeTree({
+      activeFilterField,
+      checkedFilterFields,
+    });
+
+    const selectedChartId = getSelectedChartIdForFilterScopeTree({
+      activeFilterField,
+      checkedFilterFields,
+    });
+    return (
+      <React.Fragment>
+        <input
+          className="filter-text scope-search multi-edit-mode"
+          placeholder={t('Search...')}
+          type="text"
+          value={searchText}
+          onChange={this.onSearchInputChange}
+        />
+        <FilterScopeTree
+          nodes={filterScopeMap[key].nodesFiltered}
+          checked={filterScopeMap[key].checked}
+          expanded={filterScopeMap[key].expanded}
+          onCheck={this.onCheckFilterScope}
+          onExpand={this.onExpandFilterScope}
+          // pass selectedFilterId prop to FilterScopeTree component,
+          // to hide checkbox for selected filter field itself
+          selectedChartId={selectedChartId}
+        />
+      </React.Fragment>
+    );
+  }
+
+  renderEditingFiltersName() {
+    const { dashboardFilters } = this.props;
+    const { activeFilterField, checkedFilterFields } = this.state;
+    const currentFilterLabels = []
+      .concat(activeFilterField || checkedFilterFields)
+      .map(key => {
+        const { chartId, column } = getChartIdAndColumnFromFilterKey(key);
+        return dashboardFilters[chartId].labels[column] || column;
+      });
+
+    return (
+      <div className="selected-fields multi-edit-mode">
+        {currentFilterLabels.length === 0 && t('No filter is selected.')}
+        {currentFilterLabels.length === 1 && t('Editing 1 filter:')}
+        {currentFilterLabels.length > 1 &&
+          t('Batch editing %d filters:', currentFilterLabels.length)}
+        <span className="selected-scopes">
+          {currentFilterLabels.join(', ')}
+        </span>
+      </div>
+    );
+  }
+
+  render() {
+    const { showSelector } = this.state;
+
+    return (
+      <React.Fragment>
+        <div className="filter-scope-container">
+          <div className="filter-scope-header">
+            <h4>{t('Configure filter scopes')}</h4>
+            {this.renderEditingFiltersName()}
+          </div>
+
+          {!showSelector ? (
+            <div>{t('There are no filters in this dashboard.')}</div>
+          ) : (
+            <div className="filters-scope-selector">
+              <div className={cx('filter-field-pane multi-edit-mode')}>
+                {this.renderFilterFieldList()}
+              </div>
+              <div className="filter-scope-pane multi-edit-mode">
+                {this.renderFilterScopeTree()}
+              </div>
+            </div>
+          )}
+        </div>
+        <div className="dashboard-modal-actions-container">
+          <Button onClick={this.onClose}>{t('Cancel')}</Button>
+          {showSelector && (
+            <Button bsStyle="primary" onClick={this.onSave}>
+              {t('Save')}
+            </Button>
+          )}
+        </div>
+      </React.Fragment>
+    );
+  }
+}
+
+FilterScopeSelector.propTypes = propTypes;
diff --git 
a/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx 
b/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
new file mode 100644
index 0000000..e433f15
--- /dev/null
+++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx
@@ -0,0 +1,94 @@
+/**
+ * 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 from 'react';
+import PropTypes from 'prop-types';
+import CheckboxTree from 'react-checkbox-tree';
+import 'react-checkbox-tree/lib/react-checkbox-tree.css';
+import { t } from '@superset-ui/translation';
+
+import {
+  CheckboxChecked,
+  CheckboxUnchecked,
+  CheckboxHalfChecked,
+} from '../../../components/CheckboxIcons';
+import renderFilterScopeTreeNodes from './renderFilterScopeTreeNodes';
+import { filterScopeSelectorTreeNodePropShape } from '../../util/propShapes';
+
+const propTypes = {
+  nodes: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired,
+  checked: PropTypes.arrayOf(
+    PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+  ).isRequired,
+  expanded: PropTypes.arrayOf(
+    PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+  ).isRequired,
+  onCheck: PropTypes.func.isRequired,
+  onExpand: PropTypes.func.isRequired,
+  selectedChartId: PropTypes.oneOfType([null, PropTypes.number]),
+};
+
+const defaultProps = {
+  selectedChartId: null,
+};
+
+const NOOP = () => {};
+
+const FILTER_SCOPE_CHECKBOX_TREE_ICONS = {
+  check: <CheckboxChecked />,
+  uncheck: <CheckboxUnchecked />,
+  halfCheck: <CheckboxHalfChecked />,
+  expandClose: <span className="rct-icon rct-icon-expand-close" />,
+  expandOpen: <span className="rct-icon rct-icon-expand-open" />,
+  expandAll: (
+    <span className="rct-icon rct-icon-expand-all">{t('Expand all')}</span>
+  ),
+  collapseAll: (
+    <span className="rct-icon rct-icon-collapse-all">{t('Collapse all')}</span>
+  ),
+  parentClose: <span className="rct-icon rct-icon-parent-close" />,
+  parentOpen: <span className="rct-icon rct-icon-parent-open" />,
+  leaf: <span className="rct-icon rct-icon-leaf" />,
+};
+
+export default function FilterScopeTree({
+  nodes = [],
+  checked = [],
+  expanded = [],
+  onCheck,
+  onExpand,
+  selectedChartId,
+}) {
+  return (
+    <CheckboxTree
+      showExpandAll
+      expandOnClick
+      showNodeIcon={false}
+      nodes={renderFilterScopeTreeNodes({ nodes, selectedChartId })}
+      checked={checked}
+      expanded={expanded}
+      onCheck={onCheck}
+      onExpand={onExpand}
+      onClick={NOOP}
+      icons={FILTER_SCOPE_CHECKBOX_TREE_ICONS}
+    />
+  );
+}
+
+FilterScopeTree.propTypes = propTypes;
+FilterScopeTree.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/util/activeDashboardFilters.js 
b/superset/assets/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx
similarity index 50%
copy from superset/assets/src/dashboard/util/activeDashboardFilters.js
copy to 
superset/assets/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx
index 8c70577..5ffd49e 100644
--- a/superset/assets/src/dashboard/util/activeDashboardFilters.js
+++ 
b/superset/assets/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx
@@ -16,35 +16,40 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-let activeFilters = {};
+import React from 'react';
 
-export function getActiveFilters() {
-  return activeFilters;
-}
+import FilterFieldItem from './FilterFieldItem';
+import { getFilterColorMap } from '../../util/dashboardFiltersColorMap';
 
-// non-empty filters from dashboardFilters,
-// this function does not take into account: filter immune or filter scope 
settings
-export function buildActiveFilters(allDashboardFilters = {}) {
-  activeFilters = Object.values(allDashboardFilters).reduce(
-    (result, filter) => {
-      const { chartId, columns } = filter;
+export default function renderFilterFieldTreeNodes({ nodes, activeKey }) {
+  if (!nodes) {
+    return [];
+  }
 
-      Object.keys(columns).forEach(key => {
-        if (
-          Array.isArray(columns[key])
-            ? columns[key].length
-            : columns[key] !== undefined
-        ) {
-          /* eslint-disable no-param-reassign */
-          result[chartId] = {
-            ...result[chartId],
-            [key]: columns[key],
-          };
-        }
-      });
+  const root = nodes[0];
+  const allFilterNodes = root.children;
+  const children = allFilterNodes.map(node => ({
+    ...node,
+    children: node.children.map(child => {
+      const { label, value } = child;
+      const colorCode = getFilterColorMap()[value];
+      return {
+        ...child,
+        label: (
+          <FilterFieldItem
+            isSelected={value === activeKey}
+            label={label}
+            colorCode={colorCode}
+          />
+        ),
+      };
+    }),
+  }));
 
-      return result;
+  return [
+    {
+      ...root,
+      children,
     },
-    {},
-  );
+  ];
 }
diff --git 
a/superset/assets/src/dashboard/components/filterscope/renderFilterScopeTreeNodes.jsx
 
b/superset/assets/src/dashboard/components/filterscope/renderFilterScopeTreeNodes.jsx
new file mode 100644
index 0000000..f4a85cf
--- /dev/null
+++ 
b/superset/assets/src/dashboard/components/filterscope/renderFilterScopeTreeNodes.jsx
@@ -0,0 +1,74 @@
+/**
+ * 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 from 'react';
+import cx from 'classnames';
+
+import ChartIcon from '../../../components/ChartIcon';
+import { CHART_TYPE } from '../../util/componentTypes';
+
+function traverse({ currentNode = {}, selectedChartId }) {
+  if (!currentNode) {
+    return null;
+  }
+
+  const { label, value, type, children } = currentNode;
+  if (children && children.length) {
+    const updatedChildren = children.map(child =>
+      traverse({ currentNode: child, selectedChartId }),
+    );
+    return {
+      ...currentNode,
+      label: (
+        <a
+          className={cx(`filter-scope-type ${type.toLowerCase()}`, {
+            'selected-filter': selectedChartId === value,
+          })}
+        >
+          {type === CHART_TYPE && (
+            <span className="type-indicator">
+              <ChartIcon />
+            </span>
+          )}
+          {label}
+        </a>
+      ),
+      children: updatedChildren,
+    };
+  }
+  return {
+    ...currentNode,
+    label: (
+      <a
+        className={cx(`filter-scope-type ${type.toLowerCase()}`, {
+          'selected-filter': selectedChartId === value,
+        })}
+      >
+        {label}
+      </a>
+    ),
+  };
+}
+
+export default function renderFilterScopeTreeNodes({ nodes, selectedChartId }) 
{
+  if (!nodes) {
+    return [];
+  }
+
+  return nodes.map(node => traverse({ currentNode: node, selectedChartId }));
+}
diff --git 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js 
b/superset/assets/src/dashboard/containers/FilterScope.jsx
similarity index 51%
copy from 
superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
copy to superset/assets/src/dashboard/containers/FilterScope.jsx
index e72e13e..f88d665 100644
--- 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
+++ b/superset/assets/src/dashboard/containers/FilterScope.jsx
@@ -16,31 +16,32 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { filterId } from './mockSliceEntities';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
 
-export const emptyFilters = {};
+import { setDirectPathToChild } from '../actions/dashboardState';
+import FilterScopeSelector from 
'../components/filterscope/FilterScopeSelector';
 
-export const dashboardFilters = {
-  [filterId]: {
-    chartId: filterId,
-    componentId: 'CHART-rwDfbGqeEn',
-    directPathToFilter: [
-      'ROOT_ID',
-      'TABS-VPEX_c476g',
-      'TAB-PMJyKM1yB',
-      'TABS-YdylzDMTMQ',
-      'TAB-O9AaU9FT0',
-      'ROW-l6PrlhwSjh',
-      'CHART-rwDfbGqeEn',
-    ],
-    scope: 'ROOT_ID',
-    isDateFilter: false,
-    isInstantFilter: true,
-    columns: {
-      region: ['a', 'b'],
-    },
-    labels: {
-      region: 'region',
+function mapStateToProps({ dashboardLayout, dashboardFilters, dashboardInfo }) 
{
+  return {
+    dashboardFilters,
+    filterImmuneSlices: dashboardInfo.metadata.filterImmuneSlices || [],
+    filterImmuneSliceFields:
+      dashboardInfo.metadata.filterImmuneSliceFields || {},
+    layout: dashboardLayout.present,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators(
+    {
+      setDirectPathToChild,
     },
-  },
-};
+    dispatch,
+  );
+}
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps,
+)(FilterScopeSelector);
diff --git a/superset/assets/src/dashboard/reducers/dashboardFilters.js 
b/superset/assets/src/dashboard/reducers/dashboardFilters.js
index 7cd7c89..4c1f14d 100644
--- a/superset/assets/src/dashboard/reducers/dashboardFilters.js
+++ b/superset/assets/src/dashboard/reducers/dashboardFilters.js
@@ -17,7 +17,6 @@
  * under the License.
  */
 /* eslint-disable camelcase */
-import { DASHBOARD_ROOT_ID } from '../util/constants';
 import {
   ADD_FILTER,
   REMOVE_FILTER,
@@ -28,15 +27,23 @@ import { TIME_RANGE } from 
'../../visualizations/FilterBox/FilterBox';
 import getFilterConfigsFromFormdata from 
'../util/getFilterConfigsFromFormdata';
 import { buildFilterColorMap } from '../util/dashboardFiltersColorMap';
 import { buildActiveFilters } from '../util/activeDashboardFilters';
+import { DASHBOARD_ROOT_ID } from '../util/constants';
+
+export const DASHBOARD_FILTER_SCOPE_GLOBAL = {
+  scope: [DASHBOARD_ROOT_ID],
+  immune: [],
+};
 
 export const dashboardFilter = {
   chartId: 0,
-  componentId: '',
+  componentId: null,
+  filterName: null,
   directPathToFilter: [],
-  scope: DASHBOARD_ROOT_ID,
   isDateFilter: false,
   isInstantFilter: true,
   columns: {},
+  labels: {},
+  scopes: {},
 };
 
 export default function dashboardFiltersReducer(dashboardFilters = {}, action) 
{
@@ -44,6 +51,13 @@ export default function 
dashboardFiltersReducer(dashboardFilters = {}, action) {
     [ADD_FILTER]() {
       const { chartId, component, form_data } = action;
       const { columns, labels } = getFilterConfigsFromFormdata(form_data);
+      const scopes = Object.keys(columns).reduce(
+        (map, column) => ({
+          ...map,
+          [column]: DASHBOARD_FILTER_SCOPE_GLOBAL,
+        }),
+        {},
+      );
       const directPathToFilter = component
         ? (component.parents || []).slice().concat(component.id)
         : [];
@@ -52,9 +66,11 @@ export default function 
dashboardFiltersReducer(dashboardFilters = {}, action) {
         ...dashboardFilter,
         chartId,
         componentId: component.id,
+        filterName: component.meta.sliceName,
         directPathToFilter,
         columns,
         labels,
+        scopes,
         isInstantFilter: !!form_data.instant_filtering,
         isDateFilter: Object.keys(columns).includes(TIME_RANGE),
       };
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js 
b/superset/assets/src/dashboard/reducers/getInitialState.js
index 85a62e3..185a8b0 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -180,6 +180,7 @@ export default function(bootstrapData) {
           ...dashboardFilter,
           chartId: key,
           componentId,
+          filterName: slice.slice_name,
           directPathToFilter,
           columns,
           labels,
@@ -187,8 +188,6 @@ export default function(bootstrapData) {
           isDateFilter: Object.keys(columns).includes(TIME_RANGE),
         };
       }
-      buildActiveFilters(dashboardFilters);
-      buildFilterColorMap(dashboardFilters);
     }
 
     // sync layout names with current slice names in case a slice was edited
@@ -199,6 +198,8 @@ export default function(bootstrapData) {
       layout[layoutId].meta.sliceName = slice.slice_name;
     }
   });
+  buildActiveFilters(dashboardFilters);
+  buildFilterColorMap(dashboardFilters);
 
   // store the header as a layout component so we can undo/redo changes
   layout[DASHBOARD_HEADER_ID] = {
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less 
b/superset/assets/src/dashboard/stylesheets/dashboard.less
index c37d5e5..cdf11f7 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -165,19 +165,26 @@ body {
     padding: 24px 24px 29px 24px;
   }
 
-  .delete-modal-actions-container {
+  .modal-dialog.filter-scope-modal {
+    width: 80%;
+  }
+
+  .dashboard-modal-actions-container {
     margin-top: 24px;
+    text-align: right;
 
     .btn {
       margin-right: 16px;
       &:last-child {
         margin-right: 0;
       }
+    }
+  }
 
-      &.btn-primary {
-        background: @pink !important;
-        border-color: @pink !important;
-      }
+  .dashboard-modal.delete {
+    .btn.btn-primary {
+      background: @pink;
+      border-color: @pink;
     }
   }
 }
diff --git 
a/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less 
b/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less
new file mode 100644
index 0000000..dd19a66
--- /dev/null
+++ b/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less
@@ -0,0 +1,241 @@
+/**
+ * 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 "../../../stylesheets/less/cosmo/variables.less";
+
+.filter-scope-container {
+  font-size: 14px;
+
+  .nav.nav-tabs {
+    border: none;
+  }
+}
+
+.filter-scope-header {
+  h4 {
+    margin-top: 0;
+  }
+
+  .selected-fields {
+    margin: 12px 0 16px;
+    visibility: hidden;
+
+    &.multi-edit-mode {
+      visibility: visible;
+    }
+
+    .selected-scopes {
+      padding-left: 5px;
+    }
+  }
+}
+
+.filters-scope-selector {
+  margin: 10px -24px 20px;
+  display: flex;
+  flex-direction: row;
+  position: relative;
+  border: 1px solid #ccc;
+  border-left: none;
+  border-right: none;
+
+  a, a:active, a:hover {
+    color: @almost-black;
+    text-decoration: none;
+  }
+
+  .react-checkbox-tree .rct-icon.rct-icon-expand-all,
+  .react-checkbox-tree .rct-icon.rct-icon-collapse-all {
+    font-size: 13px;
+    font-family: @font-family-sans-serif;
+    color: @brand-primary;
+
+    &::before {
+      content: '';
+    }
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    &:focus {
+      outline: none;
+    }
+  }
+
+  .filter-field-pane {
+    position: relative;
+    width: 40%;
+    padding: 16px 16px 16px 24px;
+    border-right: 1px solid #ccc;
+
+    .filter-container {
+      label {
+        font-weight: normal;
+        margin: 0 0 0 16px;
+      }
+    }
+
+    .filter-field-item {
+      height: 35px;
+      display: flex;
+      align-items: center;
+      padding: 0 24px;
+      margin-left: -24px;
+
+      &.is-selected {
+        border: 1px solid #aaa;
+        border-radius: 4px;
+        background-color: #eee;
+        margin-left: -25px;
+      }
+    }
+
+    .react-checkbox-tree {
+      .rct-text {
+        height: 40px;
+      }
+    }
+  }
+
+  .filter-scope-pane {
+    position: relative;
+    flex: 1;
+    padding: 16px 24px 16px 16px;
+  }
+
+  .react-checkbox-tree {
+    flex-direction: column;
+    color: @almost-black;
+    font-size: 14px;
+
+    .filter-scope-type {
+      padding: 8px 0;
+      display: block;
+
+      .type-indicator {
+        position: relative;
+        top: 3px;
+        margin-right: 8px;
+      }
+
+      &.chart {
+        font-weight: normal;
+      }
+
+      &.selected-filter {
+        padding-left: 28px;
+        position: relative;
+        color: #aaa;
+
+        &::before {
+          content: " ";
+          position: absolute;
+          left: 0;
+          top: 50%;
+          width: 18px;
+          height: 18px;
+          border-radius: 2px;
+          margin-top: -9px;
+          box-shadow: inset 0 0 0 2px #ccc;
+          background: #f2f2f2;
+        }
+      }
+
+      &.root {
+        font-weight: 700;
+      }
+
+      &.tab {
+        font-weight: 700;
+      }
+    }
+
+    .rct-checkbox {
+      svg {
+        position: relative;
+        top: 3px;
+        width: 18px;
+      }
+    }
+
+    .rct-node-leaf {
+      .rct-bare-label {
+        &::before {
+          padding-left: 5px;
+        }
+      }
+    }
+
+    .rct-options {
+      text-align: left;
+      margin-left: 0;
+      margin-bottom: 8px;
+    }
+
+    .rct-text {
+      margin: 0;
+      display: flex;
+    }
+
+    .rct-title {
+      display: block;
+      font-weight: bold;
+    }
+
+    // disable style from react-checkbox-trees.css
+    .rct-node-clickable:hover,
+    .rct-node-clickable:focus,
+    label:hover,
+    label:active {
+      background: none !important;
+    }
+  }
+
+  .multi-edit-mode {
+    &.filter-scope-pane {
+      .rct-node.rct-node-leaf .filter-scope-type.filter_box {
+        display: none;
+      }
+    }
+
+    .filter-field-item {
+      padding: 0 16px 0 50px;
+      margin-left: -50px;
+
+      &.is-selected {
+        margin-left: -51px;
+      }
+    }
+  }
+
+  .scope-search {
+    position: absolute;
+    right: 16px;
+    top: 16px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+    padding: 4px 8px 4px 8px;
+    font-size: 13px;
+    outline: none;
+
+    &:focus {
+      border: 1px solid @brand-primary;
+    }
+  }
+}
diff --git a/superset/assets/src/dashboard/stylesheets/index.less 
b/superset/assets/src/dashboard/stylesheets/index.less
index 01a0e3c..8ebce25 100644
--- a/superset/assets/src/dashboard/stylesheets/index.less
+++ b/superset/assets/src/dashboard/stylesheets/index.less
@@ -23,6 +23,7 @@
 @import './buttons.less';
 @import './dashboard.less';
 @import './dnd.less';
+@import './filter-scope-selector.less';
 @import './filter-indicator.less';
 @import './filter-indicator-tooltip.less';
 @import './grid.less';
diff --git a/superset/assets/src/dashboard/util/activeDashboardFilters.js 
b/superset/assets/src/dashboard/util/activeDashboardFilters.js
index 8c70577..8fa00d0 100644
--- a/superset/assets/src/dashboard/util/activeDashboardFilters.js
+++ b/superset/assets/src/dashboard/util/activeDashboardFilters.js
@@ -17,14 +17,31 @@
  * under the License.
  */
 let activeFilters = {};
+let allFilterBoxChartIds = [];
 
 export function getActiveFilters() {
   return activeFilters;
 }
 
+// currently filterbox is a chart,
+// when define filter scopes, they have to be out pulled out in a few places.
+// after we make filterbox a dashboard build-in component,
+// will not need this check anymore
+export function isFilterBox(chartId) {
+  return allFilterBoxChartIds.includes(chartId);
+}
+
+export function getAllFilterBoxChartIds() {
+  return allFilterBoxChartIds;
+}
+
 // non-empty filters from dashboardFilters,
 // this function does not take into account: filter immune or filter scope 
settings
 export function buildActiveFilters(allDashboardFilters = {}) {
+  allFilterBoxChartIds = Object.values(allDashboardFilters).map(
+    filter => filter.chartId,
+  );
+
   activeFilters = Object.values(allDashboardFilters).reduce(
     (result, filter) => {
       const { chartId, columns } = filter;
diff --git a/superset/assets/src/dashboard/util/buildFilterScopeTreeEntry.js 
b/superset/assets/src/dashboard/util/buildFilterScopeTreeEntry.js
new file mode 100644
index 0000000..e91ac51
--- /dev/null
+++ b/superset/assets/src/dashboard/util/buildFilterScopeTreeEntry.js
@@ -0,0 +1,65 @@
+/**
+ * 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 getFilterScopeNodesTree from './getFilterScopeNodesTree';
+import getFilterScopeParentNodes from './getFilterScopeParentNodes';
+import getKeyForFilterScopeTree from './getKeyForFilterScopeTree';
+import getSelectedChartIdForFilterScopeTree from 
'./getSelectedChartIdForFilterScopeTree';
+
+export default function buildFilterScopeTreeEntry({
+  checkedFilterFields = [],
+  activeFilterField,
+  filterScopeMap = {},
+  layout = {},
+}) {
+  const key = getKeyForFilterScopeTree({
+    checkedFilterFields,
+    activeFilterField,
+  });
+  const editingList = activeFilterField
+    ? [activeFilterField]
+    : checkedFilterFields;
+  const selectedChartId = getSelectedChartIdForFilterScopeTree({
+    checkedFilterFields,
+    activeFilterField,
+  });
+  const nodes = getFilterScopeNodesTree({
+    components: layout,
+    filterFields: editingList,
+    selectedChartId,
+  });
+  const checkedChartIdSet = new Set();
+  editingList.forEach(filterField => {
+    (filterScopeMap[filterField].checked || []).forEach(chartId => {
+      checkedChartIdSet.add(`${chartId}:${filterField}`);
+    });
+  });
+  const checked = [...checkedChartIdSet];
+  const expanded = filterScopeMap[key]
+    ? filterScopeMap[key].expanded
+    : getFilterScopeParentNodes(nodes, 1);
+
+  return {
+    [key]: {
+      nodes,
+      nodesFiltered: [...nodes],
+      checked,
+      expanded,
+    },
+  };
+}
diff --git a/superset/assets/src/dashboard/util/constants.js 
b/superset/assets/src/dashboard/util/constants.js
index e2cbd32..5eae2a8 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -76,3 +76,6 @@ export const FILTER_INDICATORS_DISPLAY_LENGTH = 3;
 // in-component element types: can be added into
 // directPathToChild, used for in dashboard navigation and focus
 export const IN_COMPONENT_ELEMENT_TYPES = ['LABEL'];
+
+// filter scope selector filter fields pane root id
+export const ALL_FILTERS_ROOT = 'ALL_FILTERS_ROOT';
diff --git a/superset/assets/src/dashboard/util/dashboardFiltersColorMap.js 
b/superset/assets/src/dashboard/util/dashboardFiltersColorMap.js
index bb8f762..129acf5 100644
--- a/superset/assets/src/dashboard/util/dashboardFiltersColorMap.js
+++ b/superset/assets/src/dashboard/util/dashboardFiltersColorMap.js
@@ -16,15 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { getDashboardFilterKey } from './getDashboardFilterKey';
+
 // should be consistent with @badge-colors .less variable
 const FILTER_COLORS_COUNT = 20;
 
 let filterColorMap = {};
 
-export function getFilterColorKey(chartId, column) {
-  return `${chartId}_${column}`;
-}
-
 export function getFilterColorMap() {
   return filterColorMap;
 }
@@ -38,7 +36,7 @@ export function buildFilterColorMap(allDashboardFilters = {}) 
{
       Object.keys(columns)
         .sort()
         .forEach(column => {
-          const key = getFilterColorKey(chartId, column);
+          const key = getDashboardFilterKey({ chartId, column });
           const colorCode = `badge-${filterColorIndex % FILTER_COLORS_COUNT}`;
           /* eslint-disable no-param-reassign */
           colorMap[key] = colorCode;
diff --git a/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js 
b/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js
new file mode 100644
index 0000000..60d86b5
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js
@@ -0,0 +1,62 @@
+/**
+ * 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 { CHART_TYPE } from '../util/componentTypes';
+
+export default function getCurrentScopeChartIds({
+  scopeComponentIds,
+  filterField,
+  filterImmuneSlices,
+  filterImmuneSliceFields,
+  components,
+}) {
+  let chartIds = [];
+
+  function traverse(component) {
+    if (!component) {
+      return;
+    }
+
+    if (
+      component.type === CHART_TYPE &&
+      component.meta &&
+      component.meta.chartId
+    ) {
+      chartIds.push(component.meta.chartId);
+    } else if (component.children) {
+      component.children.forEach(child => traverse(components[child]));
+    }
+  }
+
+  scopeComponentIds.forEach(componentId => traverse(components[componentId]));
+
+  if (filterImmuneSlices && filterImmuneSlices.length) {
+    chartIds = chartIds.filter(id => !filterImmuneSlices.includes(id));
+  }
+
+  if (filterImmuneSliceFields) {
+    chartIds = chartIds.filter(
+      id =>
+        !(id.toString() in filterImmuneSliceFields) ||
+        !filterImmuneSliceFields[id].includes(filterField),
+    );
+  }
+
+  return chartIds;
+}
diff --git a/superset/assets/src/dashboard/stylesheets/index.less 
b/superset/assets/src/dashboard/util/getDashboardFilterKey.js
similarity index 67%
copy from superset/assets/src/dashboard/stylesheets/index.less
copy to superset/assets/src/dashboard/util/getDashboardFilterKey.js
index 01a0e3c..e6307c3 100644
--- a/superset/assets/src/dashboard/stylesheets/index.less
+++ b/superset/assets/src/dashboard/util/getDashboardFilterKey.js
@@ -16,17 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-@import './variables.less';
+export function getDashboardFilterKey({ chartId, column }) {
+  return `${chartId}_${column}`;
+}
 
-@import './builder.less';
-@import './builder-sidepane.less';
-@import './buttons.less';
-@import './dashboard.less';
-@import './dnd.less';
-@import './filter-indicator.less';
-@import './filter-indicator-tooltip.less';
-@import './grid.less';
-@import './hover-menu.less';
-@import './popover-menu.less';
-@import './resizable.less';
-@import './components/index.less';
+export function getChartIdAndColumnFromFilterKey(key) {
+  const [chartId, ...parts] = key.split('_');
+  const column = parts.slice().join('_');
+  return { chartId: parseInt(chartId, 10), column };
+}
diff --git 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js 
b/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js
similarity index 50%
copy from 
superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
copy to superset/assets/src/dashboard/util/getFilterFieldNodesTree.js
index e72e13e..b55d28f 100644
--- 
a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
+++ b/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js
@@ -16,31 +16,31 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { filterId } from './mockSliceEntities';
+import { t } from '@superset-ui/translation';
 
-export const emptyFilters = {};
+import { getDashboardFilterKey } from './getDashboardFilterKey';
+import { ALL_FILTERS_ROOT } from './constants';
 
-export const dashboardFilters = {
-  [filterId]: {
-    chartId: filterId,
-    componentId: 'CHART-rwDfbGqeEn',
-    directPathToFilter: [
-      'ROOT_ID',
-      'TABS-VPEX_c476g',
-      'TAB-PMJyKM1yB',
-      'TABS-YdylzDMTMQ',
-      'TAB-O9AaU9FT0',
-      'ROW-l6PrlhwSjh',
-      'CHART-rwDfbGqeEn',
-    ],
-    scope: 'ROOT_ID',
-    isDateFilter: false,
-    isInstantFilter: true,
-    columns: {
-      region: ['a', 'b'],
-    },
-    labels: {
-      region: 'region',
+export default function getFilterFieldNodesTree({ dashboardFilters = {} }) {
+  const allFilters = Object.values(dashboardFilters).map(dashboardFilter => {
+    const { chartId, filterName, columns, labels } = dashboardFilter;
+    const children = Object.keys(columns).map(column => ({
+      value: getDashboardFilterKey({ chartId, column }),
+      label: labels[column] || column,
+    }));
+    return {
+      value: chartId,
+      label: filterName,
+      children,
+      showCheckbox: true,
+    };
+  });
+
+  return [
+    {
+      value: ALL_FILTERS_ROOT,
+      label: t('Select/deselect all filters'),
+      children: allFilters,
     },
-  },
-};
+  ];
+}
diff --git a/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js 
b/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js
new file mode 100644
index 0000000..470ac08
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js
@@ -0,0 +1,128 @@
+/**
+ * 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 { isEmpty } from 'lodash';
+import { t } from '@superset-ui/translation';
+
+import { DASHBOARD_ROOT_ID } from './constants';
+import {
+  CHART_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  TAB_TYPE,
+} from '../util/componentTypes';
+
+const FILTER_SCOPE_CONTAINER_TYPES = [TAB_TYPE, DASHBOARD_ROOT_TYPE];
+
+function traverse({
+  currentNode = {},
+  components = {},
+  filterFields = [],
+  selectedChartId,
+}) {
+  if (!currentNode) {
+    return null;
+  }
+
+  const type = currentNode.type;
+  if (
+    CHART_TYPE === type &&
+    currentNode &&
+    currentNode.meta &&
+    currentNode.meta.chartId
+  ) {
+    const chartNode = {
+      value: currentNode.meta.chartId,
+      label:
+        currentNode.meta.sliceName || `${type} ${currentNode.meta.chartId}`,
+      type,
+      showCheckbox: selectedChartId !== currentNode.meta.chartId,
+    };
+
+    return {
+      ...chartNode,
+      children: filterFields.map(filterField => ({
+        value: `${currentNode.meta.chartId}:${filterField}`,
+        label: `${chartNode.label}`,
+        type: 'filter_box',
+        showCheckbox: false,
+      })),
+    };
+  }
+
+  let children = [];
+  if (currentNode.children && currentNode.children.length) {
+    currentNode.children.forEach(child => {
+      const childNodeTree = traverse({
+        currentNode: components[child],
+        components,
+        filterFields,
+        selectedChartId,
+      });
+
+      const childType = components[child].type;
+      if (FILTER_SCOPE_CONTAINER_TYPES.includes(childType)) {
+        children.push(childNodeTree);
+      } else {
+        children = children.concat(childNodeTree);
+      }
+    });
+  }
+
+  if (FILTER_SCOPE_CONTAINER_TYPES.includes(type)) {
+    let label = null;
+    if (type === DASHBOARD_ROOT_TYPE) {
+      label = t('Select/deselect all charts');
+    } else {
+      label =
+        currentNode.meta && currentNode.meta.text
+          ? currentNode.meta.text
+          : `${type} ${currentNode.id}`;
+    }
+
+    return {
+      value: currentNode.id,
+      label,
+      type,
+      children,
+    };
+  }
+
+  return children;
+}
+
+export default function getFilterScopeNodesTree({
+  components = {},
+  filterFields = [],
+  selectedChartId,
+}) {
+  if (isEmpty(components)) {
+    return [];
+  }
+
+  const root = traverse({
+    currentNode: components[DASHBOARD_ROOT_ID],
+    components,
+    filterFields,
+    selectedChartId,
+  });
+  return [
+    {
+      ...root,
+    },
+  ];
+}
diff --git a/superset/assets/src/dashboard/stylesheets/index.less 
b/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js
similarity index 61%
copy from superset/assets/src/dashboard/stylesheets/index.less
copy to superset/assets/src/dashboard/util/getFilterScopeParentNodes.js
index 01a0e3c..8330c64 100644
--- a/superset/assets/src/dashboard/stylesheets/index.less
+++ b/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js
@@ -16,17 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-@import './variables.less';
+export default function getFilterScopeParentNodes(nodes = [], depthLimit = -1) 
{
+  const parentNodes = [];
+  const traverse = (currentNode, depth) => {
+    if (!currentNode) {
+      return;
+    }
 
-@import './builder.less';
-@import './builder-sidepane.less';
-@import './buttons.less';
-@import './dashboard.less';
-@import './dnd.less';
-@import './filter-indicator.less';
-@import './filter-indicator-tooltip.less';
-@import './grid.less';
-@import './hover-menu.less';
-@import './popover-menu.less';
-@import './resizable.less';
-@import './components/index.less';
+    if (currentNode.children && (depthLimit === -1 || depth < depthLimit)) {
+      parentNodes.push(currentNode.value);
+      currentNode.children.forEach(child => traverse(child, depth + 1));
+    }
+  };
+
+  if (nodes.length > 0) {
+    nodes.forEach(node => {
+      traverse(node, 0);
+    });
+  }
+
+  return parentNodes;
+}
diff --git a/superset/assets/src/dashboard/stylesheets/index.less 
b/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
similarity index 67%
copy from superset/assets/src/dashboard/stylesheets/index.less
copy to superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
index 01a0e3c..e85dd51 100644
--- a/superset/assets/src/dashboard/stylesheets/index.less
+++ b/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js
@@ -16,17 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-@import './variables.less';
+import { safeStringify } from '../../utils/safeStringify';
 
-@import './builder.less';
-@import './builder-sidepane.less';
-@import './buttons.less';
-@import './dashboard.less';
-@import './dnd.less';
-@import './filter-indicator.less';
-@import './filter-indicator-tooltip.less';
-@import './grid.less';
-@import './hover-menu.less';
-@import './popover-menu.less';
-@import './resizable.less';
-@import './components/index.less';
+export default function getKeyForFilterScopeTree({
+  activeFilterField,
+  checkedFilterFields,
+}) {
+  return activeFilterField
+    ? safeStringify([activeFilterField])
+    : safeStringify(checkedFilterFields);
+}
diff --git a/superset/assets/src/dashboard/stylesheets/index.less 
b/superset/assets/src/dashboard/util/getRevertedFilterScope.js
similarity index 57%
copy from superset/assets/src/dashboard/stylesheets/index.less
copy to superset/assets/src/dashboard/util/getRevertedFilterScope.js
index 01a0e3c..92e4a29 100644
--- a/superset/assets/src/dashboard/stylesheets/index.less
+++ b/superset/assets/src/dashboard/util/getRevertedFilterScope.js
@@ -16,17 +16,27 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-@import './variables.less';
+export default function getRevertedFilterScope({
+  checked = [],
+  filterFields = [],
+  filterScopeMap = {},
+}) {
+  const checkedChartIdsByFilterField = checked.reduce((map, value) => {
+    const [chartId, filterField] = value.split(':');
+    return {
+      ...map,
+      [filterField]: (map[filterField] || []).concat(parseInt(chartId, 10)),
+    };
+  }, {});
 
-@import './builder.less';
-@import './builder-sidepane.less';
-@import './buttons.less';
-@import './dashboard.less';
-@import './dnd.less';
-@import './filter-indicator.less';
-@import './filter-indicator-tooltip.less';
-@import './grid.less';
-@import './hover-menu.less';
-@import './popover-menu.less';
-@import './resizable.less';
-@import './components/index.less';
+  return filterFields.reduce(
+    (map, filterField) => ({
+      ...map,
+      [filterField]: {
+        ...filterScopeMap[filterField],
+        checked: checkedChartIdsByFilterField[filterField],
+      },
+    }),
+    {},
+  );
+}
diff --git 
a/superset/assets/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js 
b/superset/assets/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js
new file mode 100644
index 0000000..cde72e3
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js
@@ -0,0 +1,53 @@
+/**
+ * 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 { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey';
+
+export default function getSelectedChartIdForFilterScopeTree({
+  activeFilterField,
+  checkedFilterFields,
+}) {
+  // we don't apply filter on filter_box itself, so we will disable
+  // checkbox in filter scope selector.
+  // this function returns chart id based on current filter scope selector 
local state:
+  // 1. if in single-edit mode, return the chart id for selected filter field.
+  // 2. if in multi-edit mode, if all filter fields are from same chart id,
+  // return the single chart id.
+  // otherwise, there is no chart to disable.
+  if (activeFilterField) {
+    return getChartIdAndColumnFromFilterKey(activeFilterField).chartId;
+  }
+
+  if (checkedFilterFields.length) {
+    const { chartId } = getChartIdAndColumnFromFilterKey(
+      checkedFilterFields[0],
+    );
+
+    if (
+      checkedFilterFields.some(
+        filterKey =>
+          getChartIdAndColumnFromFilterKey(filterKey).chartId !== chartId,
+      )
+    ) {
+      return null;
+    }
+    return chartId;
+  }
+
+  return null;
+}
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx 
b/superset/assets/src/dashboard/util/propShapes.jsx
index d4fb6dd..b59f41e 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -35,6 +35,9 @@ export const componentShape = PropTypes.shape({
 
     // Row
     background: PropTypes.oneOf(backgroundStyleOptions.map(opt => opt.value)),
+
+    // Chart
+    chartId: PropTypes.number,
   }),
 });
 
@@ -76,7 +79,7 @@ export const filterIndicatorPropShape = PropTypes.shape({
   isInstantFilter: PropTypes.bool.isRequired,
   label: PropTypes.string.isRequired,
   name: PropTypes.string.isRequired,
-  scope: PropTypes.string.isRequired,
+  scope: PropTypes.arrayOf(PropTypes.string),
   values: PropTypes.array.isRequired,
 });
 
@@ -102,6 +105,30 @@ export const dashboardInfoPropShape = PropTypes.shape({
   userId: PropTypes.string.isRequired,
 });
 
+/* eslint-disable-next-line  no-undef */
+const lazyFunction = f => () => f().apply(this, arguments);
+
+const leafType = PropTypes.shape({
+  value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+  label: PropTypes.string.isRequired,
+});
+
+const parentShape = {
+  value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+  label: PropTypes.string.isRequired,
+  children: PropTypes.arrayOf(
+    PropTypes.oneOfType([
+      PropTypes.shape(lazyFunction(() => parentShape)),
+      leafType,
+    ]),
+  ),
+};
+
+export const filterScopeSelectorTreeNodePropShape = PropTypes.oneOfType([
+  PropTypes.shape(parentShape),
+  leafType,
+]);
+
 export const loadStatsPropShape = PropTypes.objectOf(
   PropTypes.shape({
     didLoad: PropTypes.bool.isRequired,
diff --git a/superset/assets/src/visualizations/FilterBox/FilterBox.css 
b/superset/assets/src/visualizations/FilterBox/FilterBox.css
index f546c9c..0b678e0 100644
--- a/superset/assets/src/visualizations/FilterBox/FilterBox.css
+++ b/superset/assets/src/visualizations/FilterBox/FilterBox.css
@@ -60,7 +60,7 @@ ul.select2-results div.filter_box{
 .filter-container label {
     display: flex;
     font-weight: bold;
-    margin-bottom: 8px;
+    margin: 0 0 8px 8px;
 }
 .filter-container .filter-badge-container {
     width: 30px;
diff --git a/superset/assets/src/visualizations/FilterBox/FilterBox.jsx 
b/superset/assets/src/visualizations/FilterBox/FilterBox.jsx
index a2b9cc8..b259e7b 100644
--- a/superset/assets/src/visualizations/FilterBox/FilterBox.jsx
+++ b/superset/assets/src/visualizations/FilterBox/FilterBox.jsx
@@ -29,7 +29,8 @@ import Control from '../../explore/components/Control';
 import controls from '../../explore/controls';
 import OnPasteSelect from '../../components/OnPasteSelect';
 import VirtualizedRendererWrap from '../../components/VirtualizedRendererWrap';
-import { getFilterColorKey, getFilterColorMap } from 
'../../dashboard/util/dashboardFiltersColorMap';
+import { getDashboardFilterKey } from 
'../../dashboard/util/getDashboardFilterKey';
+import { getFilterColorMap } from 
'../../dashboard/util/dashboardFiltersColorMap';
 import FilterBadgeIcon from '../../components/FilterBadgeIcon';
 
 import './FilterBox.css';
@@ -303,7 +304,7 @@ class FilterBox extends React.Component {
   }
 
   renderFilterBadge(chartId, column) {
-    const colorKey = getFilterColorKey(chartId, column);
+    const colorKey = getDashboardFilterKey({ chartId, column });
     const filterColorMap = getFilterColorMap();
     const colorCode = filterColorMap[colorKey];
 

Reply via email to