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 <[email protected]>
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];