This is an automated email from the ASF dual-hosted git repository. graceguo pushed a commit to branch scoped-filter-redux-v2 in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit af17619e18893ba8e2860c59877056e0eb105c53 Author: Grace <[email protected]> AuthorDate: Wed Nov 6 13:35:32 2019 -0800 merge filter scope settings into dashboard redux state --- .../src/dashboard/actions/dashboardFilters.js | 16 +++ .../src/dashboard/actions/dashboardLayout.js | 4 + .../assets/src/dashboard/components/Dashboard.jsx | 72 +++++------- .../components/FilterIndicatorsContainer.jsx | 30 ++--- .../assets/src/dashboard/components/Header.jsx | 5 + .../dashboard/components/HeaderActionsDropdown.jsx | 3 + .../assets/src/dashboard/components/SaveModal.jsx | 3 + .../src/dashboard/components/SliceHeader.jsx | 2 - .../dashboard/components/SliceHeaderControls.jsx | 5 +- .../components/filterscope/FilterScopeSelector.jsx | 62 +++++----- .../dashboard/components/gridComponents/Chart.jsx | 16 ++- superset/assets/src/dashboard/containers/Chart.jsx | 11 +- .../assets/src/dashboard/containers/Dashboard.jsx | 8 +- .../src/dashboard/containers/DashboardHeader.jsx | 10 +- .../src/dashboard/containers/FilterIndicators.jsx | 6 +- .../src/dashboard/containers/FilterScope.jsx | 11 +- .../src/dashboard/reducers/dashboardFilters.js | 43 ++++++- .../src/dashboard/reducers/getInitialState.js | 51 ++++++-- .../src/dashboard/util/activeDashboardFilters.js | 130 ++++++++++++++++----- .../util/charts/getEffectiveExtraFilters.js | 43 ++----- .../util/charts/getFormDataWithExtraFilters.js | 10 +- .../src/dashboard/util/getCurrentScopeChartIds.js | 62 ---------- .../assets/src/dashboard/util/getDashboardUrl.js | 20 +++- ...ashboardUrl.js => getFilterValuesByFilterId.js} | 23 +++- superset/assets/src/dashboard/util/propShapes.jsx | 26 ++--- ...hboardUrl.js => serializeActiveFilterValues.js} | 21 +++- ...getDashboardUrl.js => serializeFilterScopes.js} | 19 ++- superset/views/core.py | 14 ++- 28 files changed, 420 insertions(+), 306 deletions(-) diff --git a/superset/assets/src/dashboard/actions/dashboardFilters.js b/superset/assets/src/dashboard/actions/dashboardFilters.js index c2b8b9c..b8f92b8 100644 --- a/superset/assets/src/dashboard/actions/dashboardFilters.js +++ b/superset/assets/src/dashboard/actions/dashboardFilters.js @@ -46,11 +46,13 @@ export const CHANGE_FILTER = 'CHANGE_FILTER'; export function changeFilter(chartId, newSelectedValues, merge) { return (dispatch, getState) => { if (isValidFilter(getState, chartId)) { + const components = getState().dashboardLayout.present; return dispatch({ type: CHANGE_FILTER, chartId, newSelectedValues, merge, + components, }); } return getState().dashboardFilters; @@ -66,3 +68,17 @@ export function updateDirectPathToFilter(chartId, path) { return getState().dashboardFilters; }; } + +export const UPDATE_LAYOUT_COMPONENTS = 'UPDATE_LAYOUT_COMPONENTS'; +export function updateLayoutComponents(components) { + return dispatch => { + dispatch({ type: UPDATE_LAYOUT_COMPONENTS, components }); + }; +} + +export const UPDATE_DASHBOARD_FILTERS_SCOPE = 'UPDATE_DASHBOARD_FILTERS_SCOPE'; +export function updateDashboardFiltersScope(scopes) { + return dispatch => { + dispatch({ type: UPDATE_DASHBOARD_FILTERS_SCOPE, scopes }); + }; +} diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js index b276e37..8e517d5 100644 --- a/superset/assets/src/dashboard/actions/dashboardLayout.js +++ b/superset/assets/src/dashboard/actions/dashboardLayout.js @@ -20,6 +20,7 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo'; import { t } from '@superset-ui/translation'; import { addWarningToast } from '../../messageToasts/actions'; +import { updateLayoutComponents } from './dashboardFilters'; import { setUnsavedChanges } from './dashboardState'; import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes'; import { @@ -42,6 +43,9 @@ function setUnsavedChangesAfterAction(action) { dispatch(result); } + const components = getState().dashboardLayout.present; + dispatch(updateLayoutComponents(components)); + if (!getState().dashboardState.hasUnsavedChanges) { dispatch(setUnsavedChanges(true)); } diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx index 334704e..85c670c 100644 --- a/superset/assets/src/dashboard/components/Dashboard.jsx +++ b/superset/assets/src/dashboard/components/Dashboard.jsx @@ -28,9 +28,7 @@ import { slicePropShape, dashboardInfoPropShape, dashboardStatePropShape, - loadStatsPropShape, } from '../util/propShapes'; -import { areObjectsEqual } from '../../reduxUtils'; import { LOG_ACTIONS_MOUNT_DASHBOARD } from '../../logger/LogUtils'; import OmniContainer from '../../components/OmniContainer'; import { safeStringify } from '../../utils/safeStringify'; @@ -50,7 +48,6 @@ const propTypes = { slices: PropTypes.objectOf(slicePropShape).isRequired, filters: PropTypes.object.isRequired, datasources: PropTypes.object.isRequired, - loadStats: loadStatsPropShape.isRequired, layout: PropTypes.object.isRequired, impressionId: PropTypes.string.isRequired, initMessages: PropTypes.array, @@ -122,27 +119,36 @@ class Dashboard extends React.PureComponent { // do not apply filter when dashboard in edit mode if (!editMode && safeStringify(appliedFilters) !== safeStringify(filters)) { // refresh charts if a filter was removed, added, or changed - let changedFilterKey = null; const currFilterKeys = Object.keys(filters); const appliedFilterKeys = Object.keys(appliedFilters); - currFilterKeys.forEach(key => { - if ( - // filter was added or changed - typeof appliedFilters[key] === 'undefined' || - !areObjectsEqual(appliedFilters[key], filters[key]) + const allKeys = new Set(currFilterKeys.concat(appliedFilterKeys)); + [...allKeys].forEach(filterKey => { + const affectedChartIds = []; + if (!currFilterKeys.includes(filterKey)) { + // removed filter? + [].push.apply(affectedChartIds, appliedFilters[filterKey].scope); + } else if (!appliedFilterKeys.includes(filterKey)) { + // added filter? + [].push.apply(affectedChartIds, filters[filterKey].scope); + } else if ( + safeStringify(filters[filterKey].values) !== + safeStringify(appliedFilters[filterKey].values) || + safeStringify(filters[filterKey].scope) !== + safeStringify(appliedFilters[filterKey].scope) ) { - changedFilterKey = key; + // changed filter field value? + const affectedScope = filters[filterKey].scope.concat( + appliedFilters[filterKey].scope, + ); + [].push.apply(affectedChartIds, affectedScope); } + + const idSet = new Set(affectedChartIds); + this.refreshCharts([...idSet]); }); - if ( - !!changedFilterKey || - currFilterKeys.length !== appliedFilterKeys.length // remove 1 or more filters - ) { - this.refreshExcept(changedFilterKey); - this.appliedFilters = filters; - } + this.appliedFilters = filters; } if (hasUnsavedChanges) { @@ -157,35 +163,9 @@ class Dashboard extends React.PureComponent { return Object.values(this.props.charts); } - refreshExcept(filterKey) { - const { filters } = this.props; - const currentFilteredNames = - filterKey && filters[filterKey] ? Object.keys(filters[filterKey]) : []; - const filterImmuneSlices = this.props.dashboardInfo.metadata - .filterImmuneSlices; - const filterImmuneSliceFields = this.props.dashboardInfo.metadata - .filterImmuneSliceFields; - - this.getAllCharts().forEach(chart => { - // filterKey is a string, filter_immune_slices array contains numbers - if ( - String(chart.id) === filterKey || - filterImmuneSlices.includes(chart.id) - ) { - return; - } - - const filterImmuneSliceFieldsNames = - filterImmuneSliceFields[chart.id] || []; - // has filter-able field names - if ( - currentFilteredNames.length === 0 || - currentFilteredNames.some( - name => !filterImmuneSliceFieldsNames.includes(name), - ) - ) { - this.props.actions.triggerQuery(true, chart.id); - } + refreshCharts(ids) { + ids.forEach(id => { + this.props.actions.triggerQuery(true, id); }); } diff --git a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx index f287108..bfc5658 100644 --- a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx +++ b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx @@ -23,6 +23,7 @@ import { isEmpty } from 'lodash'; import FilterIndicator from './FilterIndicator'; import FilterIndicatorGroup from './FilterIndicatorGroup'; import { FILTER_INDICATORS_DISPLAY_LENGTH } from '../util/constants'; +import { getChartIdsInFilterScope } from '../util/activeDashboardFilters'; import { getDashboardFilterKey } from '../util/getDashboardFilterKey'; import { getFilterColorMap } from '../util/dashboardFiltersColorMap'; @@ -33,8 +34,6 @@ const propTypes = { chartStatus: PropTypes.string, // from redux - filterImmuneSlices: PropTypes.arrayOf(PropTypes.number).isRequired, - filterImmuneSliceFields: PropTypes.object.isRequired, setDirectPathToChild: PropTypes.func.isRequired, filterFieldOnFocus: PropTypes.object.isRequired, }; @@ -59,8 +58,6 @@ export default class FilterIndicatorsContainer extends React.PureComponent { const { dashboardFilters, chartId: currentChartId, - filterImmuneSlices, - filterImmuneSliceFields, filterFieldOnFocus, } = this.props; @@ -76,20 +73,22 @@ export default class FilterIndicatorsContainer extends React.PureComponent { chartId, componentId, directPathToFilter, - scope, isDateFilter, isInstantFilter, columns, labels, + scopes, } = dashboardFilter; - // do not apply filter on filter_box itself - // do not apply filter on filterImmuneSlices list - if ( - currentChartId !== chartId && - !filterImmuneSlices.includes(currentChartId) - ) { + if (currentChartId !== chartId) { Object.keys(columns).forEach(name => { + const chartIdsInFilterScope = getChartIdsInFilterScope({ + filterScope: scopes[name], + }); + + if (!chartIdsInFilterScope.includes(currentChartId)) { + return; + } const colorMapKey = getDashboardFilterKey({ chartId, column: name, @@ -101,7 +100,6 @@ export default class FilterIndicatorsContainer extends React.PureComponent { colorCode: dashboardFiltersColorMap[colorMapKey], componentId, directPathToFilter: directPathToLabel, - scope, isDateFilter, isInstantFilter, name, @@ -116,14 +114,6 @@ export default class FilterIndicatorsContainer extends React.PureComponent { name === filterFieldOnFocus.column, }; - // do not apply filter on fields in the filterImmuneSliceFields map - if ( - filterImmuneSliceFields[currentChartId] && - filterImmuneSliceFields[currentChartId].includes(name) - ) { - return; - } - if (isEmpty(indicator.values)) { indicators[1].push(indicator); } else { diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 569dac9..1d56986 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -54,6 +54,7 @@ const propTypes = { charts: PropTypes.objectOf(chartPropShape).isRequired, layout: PropTypes.object.isRequired, filters: PropTypes.object.isRequired, + filterScopes: PropTypes.object.isRequired, expandedSlices: PropTypes.object.isRequired, css: PropTypes.string.isRequired, colorNamespace: PropTypes.string, @@ -218,6 +219,7 @@ class Header extends React.PureComponent { colorNamespace, colorScheme, filters, + filterScopes, dashboardInfo, refreshFrequency, } = this.props; @@ -236,6 +238,7 @@ class Header extends React.PureComponent { label_colors: labelColors, dashboard_title: dashboardTitle, default_filters: safeStringify(filters), + filter_scopes: safeStringify(filterScopes), refresh_frequency: refreshFrequency, }; @@ -264,6 +267,7 @@ class Header extends React.PureComponent { dashboardTitle, layout, filters, + filterScopes, expandedSlices, css, colorNamespace, @@ -416,6 +420,7 @@ class Header extends React.PureComponent { dashboardTitle={dashboardTitle} layout={layout} filters={filters} + filterScopes={filterScopes} expandedSlices={expandedSlices} css={css} colorNamespace={colorNamespace} diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx index 6fc0182..6038b4e 100644 --- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx +++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx @@ -51,6 +51,7 @@ const propTypes = { isLoading: PropTypes.bool.isRequired, layout: PropTypes.object.isRequired, filters: PropTypes.object.isRequired, + filterScopes: PropTypes.object.isRequired, expandedSlices: PropTypes.object.isRequired, onSave: PropTypes.func.isRequired, }; @@ -121,6 +122,7 @@ class HeaderActionsDropdown extends React.PureComponent { hasUnsavedChanges, layout, filters, + filterScopes, expandedSlices, onSave, userCanEdit, @@ -149,6 +151,7 @@ class HeaderActionsDropdown extends React.PureComponent { saveType={SAVE_TYPE_NEWDASHBOARD} layout={layout} filters={filters} + filterScopes={filterScopes} expandedSlices={expandedSlices} refreshFrequency={refreshFrequency} css={css} diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx index d926bc8..5a978b5 100644 --- a/superset/assets/src/dashboard/components/SaveModal.jsx +++ b/superset/assets/src/dashboard/components/SaveModal.jsx @@ -38,6 +38,7 @@ const propTypes = { saveType: PropTypes.oneOf([SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD]), triggerNode: PropTypes.node.isRequired, filters: PropTypes.object.isRequired, + filterScopes: PropTypes.object.isRequired, css: PropTypes.string.isRequired, colorNamespace: PropTypes.string, colorScheme: PropTypes.string, @@ -102,6 +103,7 @@ class SaveModal extends React.PureComponent { colorScheme, expandedSlices, filters, + filterScopes, dashboardId, refreshFrequency, } = this.props; @@ -121,6 +123,7 @@ class SaveModal extends React.PureComponent { dashboard_title: saveType === SAVE_TYPE_NEWDASHBOARD ? newDashName : dashboardTitle, default_filters: safeStringify(filters), + filter_scopes: safeStringify(filterScopes), duplicate_slices: this.state.duplicateSlices, refresh_frequency: refreshFrequency, }; diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx index 100a070..3d36a51 100644 --- a/superset/assets/src/dashboard/components/SliceHeader.jsx +++ b/superset/assets/src/dashboard/components/SliceHeader.jsx @@ -94,7 +94,6 @@ class SliceHeader extends React.PureComponent { annotationQuery, annotationError, componentId, - filters, addDangerToast, } = this.props; @@ -146,7 +145,6 @@ class SliceHeader extends React.PureComponent { supersetCanCSV={supersetCanCSV} sliceCanEdit={sliceCanEdit} componentId={componentId} - filters={filters} addDangerToast={addDangerToast} /> )} diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx index 65565f0..779609d 100644 --- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx +++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx @@ -23,11 +23,11 @@ import { Dropdown, MenuItem } from 'react-bootstrap'; import { t } from '@superset-ui/translation'; import URLShortLinkModal from '../../components/URLShortLinkModal'; import getDashboardUrl from '../util/getDashboardUrl'; +import { getActiveFilters } from '../util/activeDashboardFilters'; const propTypes = { slice: PropTypes.object.isRequired, componentId: PropTypes.string.isRequired, - filters: PropTypes.object.isRequired, addDangerToast: PropTypes.func.isRequired, isCached: PropTypes.bool, isExpanded: PropTypes.bool, @@ -107,7 +107,6 @@ class SliceHeaderControls extends React.PureComponent { isCached, cachedDttm, updatedDttm, - filters, componentId, addDangerToast, } = this.props; @@ -162,7 +161,7 @@ class SliceHeaderControls extends React.PureComponent { <URLShortLinkModal url={getDashboardUrl( window.location.pathname, - filters, + getActiveFilters(), componentId, )} addDangerToast={addDangerToast} diff --git a/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx index a469906..fd70499 100644 --- a/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx +++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx @@ -26,24 +26,26 @@ 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 getFilterScopeFromNodesTree from '../../util/getFilterScopeFromNodesTree'; import getRevertedFilterScope from '../../util/getRevertedFilterScope'; import FilterScopeTree from './FilterScopeTree'; import FilterFieldTree from './FilterFieldTree'; +import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters'; import { getChartIdAndColumnFromFilterKey, getDashboardFilterKey, } from '../../util/getDashboardFilterKey'; import { ALL_FILTERS } from '../../util/constants'; +import { dashboardFilterPropShape } from '../../util/propShapes'; const propTypes = { - dashboardFilters: PropTypes.object.isRequired, + dashboardFilters: dashboardFilterPropShape.isRequired, layout: PropTypes.object.isRequired, - filterImmuneSlices: PropTypes.arrayOf(PropTypes.number).isRequired, - filterImmuneSliceFields: PropTypes.object.isRequired, - setDirectPathToChild: PropTypes.func.isRequired, + + updateDashboardFiltersScope: PropTypes.func.isRequired, + setUnsavedChanges: PropTypes.func.isRequired, onCloseModal: PropTypes.func.isRequired, }; @@ -51,12 +53,7 @@ export default class FilterScopeSelector extends React.PureComponent { constructor(props) { super(props); - const { - dashboardFilters, - filterImmuneSlices, - filterImmuneSliceFields, - layout, - } = props; + const { dashboardFilters, layout } = props; if (Object.keys(dashboardFilters).length > 0) { // display filter fields in tree structure @@ -88,14 +85,12 @@ export default class FilterScopeSelector extends React.PureComponent { 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); + // display filter_box chart as checked, but do not show checkbox + const chartIdsInFilterScope = getChartIdsInFilterScope({ + filterScope: dashboardFilters[filterId].scopes[columnName], + }); + return { ...mapByChartId, [filterKey]: { @@ -103,7 +98,7 @@ export default class FilterScopeSelector extends React.PureComponent { nodes, // filtered nodes in display if searchText is not empty nodesFiltered: [...nodes], - checked, + checked: chartIdsInFilterScope, expanded, }, }; @@ -304,17 +299,28 @@ export default class FilterScopeSelector extends React.PureComponent { onSave() { const { filterScopeMap } = this.state; - console.log( - 'i am current state', - this.allfilterFields.reduce( - (map, key) => ({ + const allFilterFieldScopes = this.allfilterFields.reduce( + (map, filterKey) => { + const { chartId } = getChartIdAndColumnFromFilterKey(filterKey); + const nodes = filterScopeMap[filterKey].nodes; + // remove filter's chart id from its own scope + const checkedChartIds = + filterScopeMap[filterKey].checked.filter(id => id !== chartId) || []; + + return { ...map, - [key]: filterScopeMap[key].checked, - }), - {}, - ), + [filterKey]: getFilterScopeFromNodesTree({ + filterKey, + nodes, + checkedChartIds, + }), + }; + }, + {}, ); + this.props.updateDashboardFiltersScope(allFilterFieldScopes); + this.props.setUnsavedChanges(true); // save do not close modal } @@ -503,7 +509,7 @@ export default class FilterScopeSelector extends React.PureComponent { )} </div> <div className="dashboard-modal-actions-container"> - <Button onClick={this.onClose}>{t('Cancel')}</Button> + <Button onClick={this.onClose}>{t('Close')}</Button> {showSelector && ( <Button bsStyle="primary" onClick={this.onSave}> {t('Save')} diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx index 4d34cc6..d50c24c 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx @@ -30,6 +30,9 @@ import { LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART, LOG_ACTIONS_FORCE_REFRESH_CHART, } from '../../../logger/LogUtils'; +import { safeStringify } from '../../../utils/safeStringify'; +import { isFilterBox } from '../../util/activeDashboardFilters'; +import getFilterValuesByFilterId from '../../util/getFilterValuesByFilterId'; const propTypes = { id: PropTypes.number.isRequired, @@ -46,6 +49,7 @@ const propTypes = { slice: slicePropShape.isRequired, sliceName: PropTypes.string.isRequired, timeout: PropTypes.number.isRequired, + // all active filter fields in dashboard filters: PropTypes.object.isRequired, refreshChart: PropTypes.func.isRequired, logEvent: PropTypes.func.isRequired, @@ -115,7 +119,9 @@ class Chart extends React.Component { for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) { const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i]; - if (nextProps[prop] !== this.props[prop]) { + if ( + safeStringify(nextProps[prop]) !== safeStringify(this.props[prop]) + ) { return true; } } @@ -238,6 +244,12 @@ class Chart extends React.Component { const isCached = queryResponse && queryResponse.is_cached; const cachedDttm = queryResponse && queryResponse.cached_dttm; const isOverflowable = OVERFLOWABLE_VIZ_TYPES.has(slice.viz_type); + const initialValues = isFilterBox(id) + ? getFilterValuesByFilterId({ + activeFilters: filters, + filterId: id, + }) + : {}; return ( <div> @@ -297,7 +309,7 @@ class Chart extends React.Component { chartId={id} chartStatus={chart.chartStatus} datasource={datasource} - initialValues={filters[id]} + initialValues={initialValues} formData={formData} queryResponse={chart.queryResponse} timeout={timeout} diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx index 5f0b107..361fe47 100644 --- a/superset/assets/src/dashboard/containers/Chart.jsx +++ b/superset/assets/src/dashboard/containers/Chart.jsx @@ -29,7 +29,10 @@ import { changeFilter } from '../actions/dashboardFilters'; import { addDangerToast } from '../../messageToasts/actions'; import { refreshChart } from '../../chart/chartAction'; import { logEvent } from '../../logger/actions'; -import { getActiveFilters } from '../util/activeDashboardFilters'; +import { + getActiveFilters, + getAppliedFilterValues, +} from '../util/activeDashboardFilters'; import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters'; import Chart from '../components/gridComponents/Chart'; @@ -48,7 +51,6 @@ function mapStateToProps( const { id } = ownProps; const chart = chartQueries[id] || {}; const { colorScheme, colorNamespace } = dashboardState; - const filters = getActiveFilters(); return { chart, @@ -57,12 +59,11 @@ function mapStateToProps( {}, slice: sliceEntities.slices[id], timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, - filters: filters || EMPTY_FILTERS, + filters: getActiveFilters() || EMPTY_FILTERS, // note: this method caches filters if possible to prevent render cascades formData: getFormDataWithExtraFilters({ chart, - dashboardMetadata: dashboardInfo.metadata, - filters, + filters: getAppliedFilterValues(id), colorScheme, colorNamespace, sliceId: id, diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx index c602cd3..91cc70f 100644 --- a/superset/assets/src/dashboard/containers/Dashboard.jsx +++ b/superset/assets/src/dashboard/containers/Dashboard.jsx @@ -27,7 +27,6 @@ import { } from '../actions/dashboardState'; import { triggerQuery } from '../../chart/chartAction'; import { logEvent } from '../../logger/actions'; -import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent'; import { getActiveFilters } from '../util/activeDashboardFilters'; function mapStateToProps(state) { @@ -49,7 +48,8 @@ function mapStateToProps(state) { dashboardState, charts, datasources, - // filters prop: All the filter_box's state in this dashboard + // filters prop: + // All the active filter_box's values and scope in this dashboard, for each filter field. // When dashboard is first loaded into browser, // its value is from preselect_filters that dashboard owner saved in dashboard's meta data // When user start interacting with dashboard, it will be user picked values from all filter_box @@ -57,10 +57,6 @@ function mapStateToProps(state) { slices: sliceEntities.slices, layout: dashboardLayout.present, impressionId, - loadStats: getLoadStatsPerTopLevelComponent({ - layout: dashboardLayout.present, - chartQueries: charts, - }), }; } diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx index 9d11998..b9bfff2 100644 --- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx +++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx @@ -50,6 +50,8 @@ import { addWarningToast, } from '../../messageToasts/actions'; +import serializeFilterScopes from '../util/serializeFilterScopes'; +import serializeActiveFilterValues from '../util/serializeActiveFilterValues'; import { logEvent } from '../../logger/actions'; import { DASHBOARD_HEADER_ID } from '../util/constants'; import { getActiveFilters } from '../util/activeDashboardFilters'; @@ -58,14 +60,20 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState, dashboardInfo, + dashboardFilters, charts, }) { + // persist selected values for each filter field, grouped by filter id + const filters = serializeActiveFilterValues(getActiveFilters()); + // persist filter scope for each filter field, grouped by filter id + const filterScopes = serializeFilterScopes(dashboardFilters); return { dashboardInfo, undoLength: undoableLayout.past.length, redoLength: undoableLayout.future.length, layout: undoableLayout.present, - filters: getActiveFilters(), + filters, + filterScopes, dashboardTitle: ( (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {} ).text, diff --git a/superset/assets/src/dashboard/containers/FilterIndicators.jsx b/superset/assets/src/dashboard/containers/FilterIndicators.jsx index 9ac8d62..80e3f77 100644 --- a/superset/assets/src/dashboard/containers/FilterIndicators.jsx +++ b/superset/assets/src/dashboard/containers/FilterIndicators.jsx @@ -23,7 +23,7 @@ import FilterIndicatorsContainer from '../components/FilterIndicatorsContainer'; import { setDirectPathToChild } from '../actions/dashboardState'; function mapStateToProps( - { dashboardState, dashboardFilters, dashboardInfo, charts }, + { dashboardState, dashboardFilters, dashboardLayout, charts }, ownProps, ) { const chartId = ownProps.chartId; @@ -33,9 +33,7 @@ function mapStateToProps( dashboardFilters, chartId, chartStatus, - filterImmuneSlices: dashboardInfo.metadata.filterImmuneSlices || [], - filterImmuneSliceFields: - dashboardInfo.metadata.filterImmuneSliceFields || {}, + layout: dashboardLayout.present, filterFieldOnFocus: dashboardState.focusedFilterField.length === 0 ? {} diff --git a/superset/assets/src/dashboard/containers/FilterScope.jsx b/superset/assets/src/dashboard/containers/FilterScope.jsx index f88d665..d816df9 100644 --- a/superset/assets/src/dashboard/containers/FilterScope.jsx +++ b/superset/assets/src/dashboard/containers/FilterScope.jsx @@ -19,15 +19,13 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { setDirectPathToChild } from '../actions/dashboardState'; +import { updateDashboardFiltersScope } from '../actions/dashboardFilters'; +import { setUnsavedChanges } from '../actions/dashboardState'; import FilterScopeSelector from '../components/filterscope/FilterScopeSelector'; -function mapStateToProps({ dashboardLayout, dashboardFilters, dashboardInfo }) { +function mapStateToProps({ dashboardLayout, dashboardFilters }) { return { dashboardFilters, - filterImmuneSlices: dashboardInfo.metadata.filterImmuneSlices || [], - filterImmuneSliceFields: - dashboardInfo.metadata.filterImmuneSliceFields || {}, layout: dashboardLayout.present, }; } @@ -35,7 +33,8 @@ function mapStateToProps({ dashboardLayout, dashboardFilters, dashboardInfo }) { function mapDispatchToProps(dispatch) { return bindActionCreators( { - setDirectPathToChild, + updateDashboardFiltersScope, + setUnsavedChanges, }, dispatch, ); diff --git a/superset/assets/src/dashboard/reducers/dashboardFilters.js b/superset/assets/src/dashboard/reducers/dashboardFilters.js index 224b50b..75cb243 100644 --- a/superset/assets/src/dashboard/reducers/dashboardFilters.js +++ b/superset/assets/src/dashboard/reducers/dashboardFilters.js @@ -22,12 +22,15 @@ import { REMOVE_FILTER, CHANGE_FILTER, UPDATE_DIRECT_PATH_TO_FILTER, + UPDATE_LAYOUT_COMPONENTS, + UPDATE_DASHBOARD_FILTERS_SCOPE, } from '../actions/dashboardFilters'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; +import { DASHBOARD_ROOT_ID } from '../util/constants'; import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata'; import { buildFilterColorMap } from '../util/dashboardFiltersColorMap'; import { buildActiveFilters } from '../util/activeDashboardFilters'; -import { DASHBOARD_ROOT_ID } from '../util/constants'; +import { getChartIdAndColumnFromFilterKey } from '../util/getDashboardFilterKey'; export const DASHBOARD_FILTER_SCOPE_GLOBAL = { scope: [DASHBOARD_ROOT_ID], @@ -113,10 +116,42 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { }, }; - if (action.type === REMOVE_FILTER) { + if (action.type === UPDATE_LAYOUT_COMPONENTS) { + buildActiveFilters({ + dashboardFilters, + components: action.components, + }); + return dashboardFilters; + } else if (action.type === UPDATE_DASHBOARD_FILTERS_SCOPE) { + const allDashboardFiltersScope = action.scopes; + // update filter scope for each filter field + const updatedFilters = Object.entries(allDashboardFiltersScope).reduce( + (map, entry) => { + const [filterKey, { scope, immune }] = entry; + const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey); + const scopes = { + ...map[chartId].scopes, + [column]: { + scope, + immune, + }, + }; + return { + ...map, + [chartId]: { + ...map[chartId], + scopes, + }, + }; + }, + dashboardFilters, + ); + + return updatedFilters; + } else if (action.type === REMOVE_FILTER) { const { chartId } = action; const { [chartId]: deletedFilter, ...updatedFilters } = dashboardFilters; - buildActiveFilters(updatedFilters); + buildActiveFilters({ dashboardFilters: updatedFilters }); buildFilterColorMap(updatedFilters); return updatedFilters; @@ -127,7 +162,7 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { dashboardFilters[action.chartId], ), }; - buildActiveFilters(updatedFilters); + buildActiveFilters({ dashboardFilters: updatedFilters }); buildFilterColorMap(updatedFilters); return updatedFilters; diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js index 185a8b0..1cefe92 100644 --- a/superset/assets/src/dashboard/reducers/getInitialState.js +++ b/superset/assets/src/dashboard/reducers/getInitialState.js @@ -17,12 +17,15 @@ * under the License. */ /* eslint-disable camelcase */ -import { isString } from 'lodash'; +import { isString, isEmpty } from 'lodash'; import shortid from 'shortid'; import { CategoricalColorNamespace } from '@superset-ui/color'; import { chart } from '../../chart/chartReducer'; -import { dashboardFilter } from './dashboardFilters'; +import { + DASHBOARD_FILTER_SCOPE_GLOBAL, + dashboardFilter, +} from './dashboardFilters'; import { initSliceEntities } from './sliceEntities'; import { getParam } from '../../modules/utils'; import { applyDefaultFormData } from '../../explore/store'; @@ -98,6 +101,11 @@ export default function(bootstrapData) { let newSlicesContainer; let newSlicesContainerWidth = 0; + const filterImmuneSliceFields = + dashboard.metadata.filter_immune_slice_fields || {}; + const filterImmuneSlices = dashboard.metadata.filter_immune_slices || []; + const filterScopes = dashboard.metadata.filter_scopes || {}; + const chartQueries = {}; const dashboardFilters = {}; const slices = {}; @@ -173,6 +181,34 @@ export default function(bootstrapData) { }); } + // backward compatible: + // merge scoped filter settings with old global immune settings + const scopesByChartId = Object.keys(columns).reduce((map, column) => { + const scopeSettings = { + ...filterScopes[key], + }; + const { scope, immune } = { + ...DASHBOARD_FILTER_SCOPE_GLOBAL, + ...scopeSettings[column], + }; + const immuneChartIds = new Set(filterImmuneSlices.slice()); + if (!isEmpty(filterImmuneSliceFields)) { + Object.keys(filterImmuneSliceFields).forEach(strChartId => { + if (filterImmuneSliceFields[strChartId].includes(column)) { + immuneChartIds.add(parseInt(strChartId, 10)); + } + }); + } + + return { + ...map, + [column]: { + scope, + immune: [...immuneChartIds].concat(immune), + }, + }; + }, {}); + const componentId = chartIdToLayoutId[key]; const directPathToFilter = (layout[componentId].parents || []).slice(); directPathToFilter.push(componentId); @@ -184,6 +220,7 @@ export default function(bootstrapData) { directPathToFilter, columns, labels, + scopes: scopesByChartId, isInstantFilter: !!slice.form_data.instant_filtering, isDateFilter: Object.keys(columns).includes(TIME_RANGE), }; @@ -198,8 +235,11 @@ export default function(bootstrapData) { layout[layoutId].meta.sliceName = slice.slice_name; } }); - buildActiveFilters(dashboardFilters); - buildFilterColorMap(dashboardFilters); + buildActiveFilters({ + dashboardFilters, + components: layout, + }); + buildFilterColorMap(dashboardFilters, layout); // store the header as a layout component so we can undo/redo changes layout[DASHBOARD_HEADER_ID] = { @@ -233,9 +273,6 @@ export default function(bootstrapData) { id: dashboard.id, slug: dashboard.slug, metadata: { - filterImmuneSliceFields: - dashboard.metadata.filter_immune_slice_fields || {}, - filterImmuneSlices: dashboard.metadata.filter_immune_slices || [], timed_refresh_immune_slices: dashboard.metadata.timed_refresh_immune_slices, }, diff --git a/superset/assets/src/dashboard/util/activeDashboardFilters.js b/superset/assets/src/dashboard/util/activeDashboardFilters.js index 8fa00d0..4a6a5af 100644 --- a/superset/assets/src/dashboard/util/activeDashboardFilters.js +++ b/superset/assets/src/dashboard/util/activeDashboardFilters.js @@ -16,52 +16,120 @@ * specific language governing permissions and limitations * under the License. */ -let activeFilters = {}; +import { isEmpty } from 'lodash'; + +import { + getChartIdAndColumnFromFilterKey, + getDashboardFilterKey, +} from './getDashboardFilterKey'; +import { CHART_TYPE } from '../util/componentTypes'; + let allFilterBoxChartIds = []; +let activeFilters = {}; +let appliedFilterValuesByChart = {}; +let allComponents = {}; +// output: { [id_column]: { values, scope } } 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 +// currently filter_box is a chart, +// when selecting filter scopes, they have to be out pulled out in a few places. +// after we make filter_box a dashboard build-in component, will not need this check anymore. export function isFilterBox(chartId) { return allFilterBoxChartIds.includes(chartId); } -export function getAllFilterBoxChartIds() { - return allFilterBoxChartIds; +// output: { [column]: values } +export function getAppliedFilterValues(chartId) { + if (!(chartId in appliedFilterValuesByChart)) { + appliedFilterValuesByChart[chartId] = Object.entries(activeFilters).reduce( + (map, entry) => { + const [filterKey, { scope: chartIds, values }] = entry; + if (chartIds.includes(chartId)) { + const { column } = getChartIdAndColumnFromFilterKey(filterKey); + return { + ...map, + [column]: values, + }; + } + return map; + }, + {}, + ); + } + return appliedFilterValuesByChart[chartId]; +} + +export function getChartIdsInFilterScope({ filterScope }) { + function traverse(chartIds, component, immuneChartIds) { + if (!component) { + return; + } + + if ( + component.type === CHART_TYPE && + component.meta && + component.meta.chartId && + !immuneChartIds.includes(component.meta.chartId) + ) { + chartIds.push(component.meta.chartId); + } else if (component.children) { + component.children.forEach(child => + traverse(chartIds, allComponents[child], immuneChartIds), + ); + } + } + + const chartIds = []; + const { scope: scopeComponentIds, immune: immuneChartIds } = filterScope; + scopeComponentIds.forEach(componentId => + traverse(chartIds, allComponents[componentId], immuneChartIds), + ); + + return chartIds; } -// 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( +// non-empty filter fields in dashboardFilters, +// activeFilters map contains selected values and filter scope. +// values: array of selected values +// scope: array of chartIds that applicable by the filter field. +export function buildActiveFilters({ dashboardFilters = {}, components = {} }) { + allFilterBoxChartIds = Object.values(dashboardFilters).map( filter => filter.chartId, ); - activeFilters = Object.values(allDashboardFilters).reduce( - (result, filter) => { - const { chartId, columns } = filter; + // clear cache + if (!isEmpty(components)) { + allComponents = components; + } + appliedFilterValuesByChart = {}; + activeFilters = Object.values(dashboardFilters).reduce((result, filter) => { + const { chartId, columns, scopes } = filter; + const nonEmptyFilters = {}; - 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], - }; - } - }); + Object.keys(columns).forEach(column => { + if ( + Array.isArray(columns[column]) + ? columns[column].length + : columns[column] !== undefined + ) { + // remove filter itself + const scope = getChartIdsInFilterScope({ + filterScope: scopes[column], + }).filter(id => chartId !== id); - return result; - }, - {}, - ); + nonEmptyFilters[getDashboardFilterKey({ chartId, column })] = { + values: columns[column], + scope, + }; + } + }); + + return { + ...result, + ...nonEmptyFilters, + }; + }, {}); } diff --git a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js index fa6ad2a..53ead91 100644 --- a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js +++ b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js @@ -16,43 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -export default function getEffectiveExtraFilters({ - dashboardMetadata, - filters, - sliceId, -}) { - const immuneSlices = dashboardMetadata.filterImmuneSlices || []; - - if (sliceId && immuneSlices.includes(sliceId)) { - // The slice is immune to dashboard filters - return []; - } - - // Build a list of fields the slice is immune to filters on +export default function getEffectiveExtraFilters(filters) { const effectiveFilters = []; - let immuneToFields = []; - if ( - sliceId && - dashboardMetadata.filterImmuneSliceFields && - dashboardMetadata.filterImmuneSliceFields[sliceId] - ) { - immuneToFields = dashboardMetadata.filterImmuneSliceFields[sliceId]; - } - - Object.keys(filters).forEach(filteringSliceId => { - if (filteringSliceId === sliceId.toString()) { - // Filters applied by the slice don't apply to itself - return; - } - const filtersFromSlice = filters[filteringSliceId]; - Object.keys(filtersFromSlice).forEach(field => { - if (!immuneToFields.includes(field)) { - effectiveFilters.push({ - col: field, - op: 'in', - val: filtersFromSlice[field], - }); - } + Object.entries(filters).forEach(entry => { + const [column, values] = entry; + effectiveFilters.push({ + col: column, + op: 'in', + val: values, }); }); diff --git a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js index 5869a09..5aa8d0f 100644 --- a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js +++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js @@ -22,13 +22,11 @@ import getEffectiveExtraFilters from './getEffectiveExtraFilters'; // We cache formData objects so that our connected container components don't always trigger // render cascades. we cannot leverage the reselect library because our cache size is >1 -const cachedDashboardMetadataByChart = {}; const cachedFiltersByChart = {}; const cachedFormdataByChart = {}; export default function getFormDataWithExtraFilters({ chart = {}, - dashboardMetadata, filters, colorScheme, colorNamespace, @@ -40,7 +38,6 @@ export default function getFormDataWithExtraFilters({ // if dashboard metadata + filters have not changed, use cache if possible if ( - (cachedDashboardMetadataByChart[sliceId] || {}) === dashboardMetadata && (cachedFiltersByChart[sliceId] || {}) === filters && (colorScheme == null || cachedFormdataByChart[sliceId].color_scheme === colorScheme) && @@ -55,14 +52,9 @@ export default function getFormDataWithExtraFilters({ ...chart.formData, ...(colorScheme && { color_scheme: colorScheme }), label_colors: labelColors, - extra_filters: getEffectiveExtraFilters({ - dashboardMetadata, - filters, - sliceId, - }), + extra_filters: getEffectiveExtraFilters(filters), }; - cachedDashboardMetadataByChart[sliceId] = dashboardMetadata; cachedFiltersByChart[sliceId] = filters; cachedFormdataByChart[sliceId] = formData; diff --git a/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js b/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js deleted file mode 100644 index 60d86b5..0000000 --- a/superset/assets/src/dashboard/util/getCurrentScopeChartIds.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 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/util/getDashboardUrl.js b/superset/assets/src/dashboard/util/getDashboardUrl.js index 243c8cb..4e2c636 100644 --- a/superset/assets/src/dashboard/util/getDashboardUrl.js +++ b/superset/assets/src/dashboard/util/getDashboardUrl.js @@ -18,8 +18,26 @@ */ /* eslint camelcase: 0 */ +import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey'; + +// filters: { [id_column]: values } export default function getDashboardUrl(pathname, filters = {}, hash = '') { - const preselect_filters = encodeURIComponent(JSON.stringify(filters)); + // convert flattened { [id_column]: values } object + // to nested filter object + const obj = Object.entries(filters).reduce((map, entry) => { + const [filterKey, { values }] = entry; + const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey); + + return { + ...map, + [chartId]: { + ...map[chartId], + [column]: values, + }, + }; + }, {}); + + const preselect_filters = encodeURIComponent(JSON.stringify(obj)); const hashSection = hash ? `#${hash}` : ''; return `${pathname}?preselect_filters=${preselect_filters}${hashSection}`; } diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js b/superset/assets/src/dashboard/util/getFilterValuesByFilterId.js similarity index 58% copy from superset/assets/src/dashboard/util/getDashboardUrl.js copy to superset/assets/src/dashboard/util/getFilterValuesByFilterId.js index 243c8cb..75f9c70 100644 --- a/superset/assets/src/dashboard/util/getDashboardUrl.js +++ b/superset/assets/src/dashboard/util/getFilterValuesByFilterId.js @@ -16,10 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -/* eslint camelcase: 0 */ +import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey'; -export default function getDashboardUrl(pathname, filters = {}, hash = '') { - const preselect_filters = encodeURIComponent(JSON.stringify(filters)); - const hashSection = hash ? `#${hash}` : ''; - return `${pathname}?preselect_filters=${preselect_filters}${hashSection}`; +// input: { [id_column1]: values, [id_column2]: values } +// output: { column1: values, column2: values } +export default function getFilterValuesByFilterId({ + activeFilters = {}, + filterId, +}) { + return Object.entries(activeFilters).reduce((map, entry) => { + const [filterKey, { values }] = entry; + const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey); + if (chartId === filterId) { + return { + ...map, + [column]: values, + }; + } + return map; + }, {}); } diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx index b59f41e..96e2e7f 100644 --- a/superset/assets/src/dashboard/util/propShapes.jsx +++ b/superset/assets/src/dashboard/util/propShapes.jsx @@ -24,6 +24,7 @@ import headerStyleOptions from './headerStyleOptions'; export const componentShape = PropTypes.shape({ id: PropTypes.string.isRequired, type: PropTypes.oneOf(Object.values(componentTypes)).isRequired, + parents: PropTypes.arrayOf(PropTypes.string), children: PropTypes.arrayOf(PropTypes.string), meta: PropTypes.shape({ // Dimensions @@ -79,10 +80,20 @@ export const filterIndicatorPropShape = PropTypes.shape({ isInstantFilter: PropTypes.bool.isRequired, label: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - scope: PropTypes.arrayOf(PropTypes.string), values: PropTypes.array.isRequired, }); +export const dashboardFilterPropShape = PropTypes.shape({ + chartId: PropTypes.number.isRequired, + componentId: PropTypes.string.isRequired, + directPathToFilter: PropTypes.arrayOf(PropTypes.string).isRequired, + isDateFilter: PropTypes.bool.isRequired, + isInstantFilter: PropTypes.bool.isRequired, + columns: PropTypes.object, + labels: PropTypes.object, + scopes: PropTypes.object, +}); + export const dashboardStatePropShape = PropTypes.shape({ sliceIds: PropTypes.arrayOf(PropTypes.number).isRequired, expandedSlices: PropTypes.object, @@ -128,16 +139,3 @@ export const filterScopeSelectorTreeNodePropShape = PropTypes.oneOfType([ PropTypes.shape(parentShape), leafType, ]); - -export const loadStatsPropShape = PropTypes.objectOf( - PropTypes.shape({ - didLoad: PropTypes.bool.isRequired, - minQueryStartTime: PropTypes.number.isRequired, - id: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - parent_id: PropTypes.string, - parent_type: PropTypes.string, - index: PropTypes.number.isRequired, - slice_ids: PropTypes.arrayOf(PropTypes.number).isRequired, - }), -); diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js b/superset/assets/src/dashboard/util/serializeActiveFilterValues.js similarity index 57% copy from superset/assets/src/dashboard/util/getDashboardUrl.js copy to superset/assets/src/dashboard/util/serializeActiveFilterValues.js index 243c8cb..6497b10 100644 --- a/superset/assets/src/dashboard/util/getDashboardUrl.js +++ b/superset/assets/src/dashboard/util/serializeActiveFilterValues.js @@ -16,10 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -/* eslint camelcase: 0 */ +import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey'; -export default function getDashboardUrl(pathname, filters = {}, hash = '') { - const preselect_filters = encodeURIComponent(JSON.stringify(filters)); - const hashSection = hash ? `#${hash}` : ''; - return `${pathname}?preselect_filters=${preselect_filters}${hashSection}`; +// input: { [id_column1]: values, [id_column2]: values } +// output: { id: { column1: values, column2: values } } +export default function serializeActiveFilterValues(activeFilters) { + return Object.entries(activeFilters).reduce((map, entry) => { + const [filterKey, { values }] = entry; + const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey); + const entryByChartId = { + ...map[chartId], + [column]: values, + }; + return { + ...map, + [chartId]: entryByChartId, + }; + }, {}); } diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js b/superset/assets/src/dashboard/util/serializeFilterScopes.js similarity index 67% copy from superset/assets/src/dashboard/util/getDashboardUrl.js copy to superset/assets/src/dashboard/util/serializeFilterScopes.js index 243c8cb..303171f 100644 --- a/superset/assets/src/dashboard/util/getDashboardUrl.js +++ b/superset/assets/src/dashboard/util/serializeFilterScopes.js @@ -16,10 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -/* eslint camelcase: 0 */ +export default function serializeFilterScopes(dashboardFilters) { + return Object.values(dashboardFilters).reduce((map, { chartId, scopes }) => { + const scopesById = Object.keys(scopes).reduce( + (scopesByColumn, column) => ({ + ...scopesByColumn, + [column]: scopes[column], + }), + {}, + ); -export default function getDashboardUrl(pathname, filters = {}, hash = '') { - const preselect_filters = encodeURIComponent(JSON.stringify(filters)); - const hashSection = hash ? `#${hash}` : ''; - return `${pathname}?preselect_filters=${preselect_filters}${hashSection}`; + return { + ...map, + [chartId]: scopesById, + }; + }, {}); } diff --git a/superset/views/core.py b/superset/views/core.py index 52f488d..706d053 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1634,12 +1634,18 @@ class Superset(BaseSupersetView): dashboard.css = data.get("css") dashboard.dashboard_title = data["dashboard_title"] - if "filter_immune_slices" not in md: - md["filter_immune_slices"] = [] if "timed_refresh_immune_slices" not in md: md["timed_refresh_immune_slices"] = [] - if "filter_immune_slice_fields" not in md: - md["filter_immune_slice_fields"] = {} + + if data["filter_scopes"]: + md.pop("filter_immune_slices", None) + md.pop("filter_immune_slice_fields", None) + md["filter_scopes"] = json.loads(data.get("filter_scopes", "{}")) + else: + if "filter_immune_slices" not in md: + md["filter_immune_slices"] = [] + if "filter_immune_slice_fields" not in md: + md["filter_immune_slice_fields"] = {} md["expanded_slices"] = data["expanded_slices"] md["refresh_frequency"] = data.get("refresh_frequency", 0) default_filters_data = json.loads(data.get("default_filters", "{}"))
