diff --git a/superset-frontend/images/icons/cross-filter-badge.svg b/superset-frontend/images/icons/cross-filter-badge.svg new file mode 100644 index 000000000..89d123c1c --- /dev/null +++ b/superset-frontend/images/icons/cross-filter-badge.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/superset-frontend/src/components/Icon/index.tsx b/superset-frontend/src/components/Icon/index.tsx index 1e3d5cb39..ae29b21e1 100644 --- a/superset-frontend/src/components/Icon/index.tsx +++ b/superset-frontend/src/components/Icon/index.tsx @@ -24,6 +24,7 @@ import { ReactComponent as AlertSolidSmallIcon } from 'images/icons/alert_solid_ import { ReactComponent as BinocularsIcon } from 'images/icons/binoculars.svg'; import { ReactComponent as BoltIcon } from 'images/icons/bolt.svg'; import { ReactComponent as BoltSmallIcon } from 'images/icons/bolt_small.svg'; +import { ReactComponent as CrossFilterBadge } from 'images/icons/cross-filter-badge.svg'; import { ReactComponent as BoltSmallRunIcon } from 'images/icons/bolt_small_run.svg'; import { ReactComponent as CalendarIcon } from 'images/icons/calendar.svg'; import { ReactComponent as CancelIcon } from 'images/icons/cancel.svg'; @@ -165,6 +166,7 @@ export type IconName = | 'caret-right' | 'caret-up' | 'certified' + | 'cross-filter-badge' | 'check' | 'checkbox-half' | 'checkbox-off' @@ -281,6 +283,7 @@ export const iconsRegistry: Record< 'alert-solid-small': AlertSolidSmallIcon, 'bolt-small': BoltSmallIcon, 'bolt-small-run': BoltSmallRunIcon, + 'cross-filter-badge': CrossFilterBadge, 'cancel-solid': CancelSolidIcon, 'cancel-x': CancelXIcon, 'card-view': CardViewIcon, diff --git a/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingForm.tsx b/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingForm.tsx index 7e98ee038..c1a173dc5 100644 --- a/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingForm.tsx +++ b/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingForm.tsx @@ -25,6 +25,7 @@ import { useForceUpdate } from '../nativeFilters/FiltersConfigModal/FiltersConfi import { CrossFilterScopingFormType } from './types'; type CrossFilterScopingFormProps = { + chartId: number; scope: Scope; form: FormInstance; }; @@ -32,6 +33,7 @@ type CrossFilterScopingFormProps = { const CrossFilterScopingForm: FC = ({ form, scope, + chartId, }) => { const forceUpdate = useForceUpdate(); const formScope = form.getFieldValue('scope'); @@ -44,6 +46,7 @@ const CrossFilterScopingForm: FC = ({ }); }} scope={scope} + chartId={chartId} formScope={formScope} forceUpdate={forceUpdate} formScoping={formScoping} diff --git a/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingModal.tsx b/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingModal.tsx index 765ff2b83..71b8f5e99 100644 --- a/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingModal.tsx +++ b/superset-frontend/src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingModal.tsx @@ -49,7 +49,10 @@ const CrossFilterScopingModal: FC = ({ dispatch( setChartConfiguration({ ...chartConfig, - [chartId]: { crossFilters: { scope: form.getFieldValue('scope') } }, + [chartId]: { + id: chartId, + crossFilters: { scope: form.getFieldValue('scope') }, + }, }), ); onClose(); @@ -88,7 +91,7 @@ const CrossFilterScopingModal: FC = ({ } > - + ); diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx index 0cdfffee7..42a4b7d64 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx @@ -19,7 +19,6 @@ import React, { useState } from 'react'; import { t, useTheme, css } from '@superset-ui/core'; import { - SearchOutlined, MinusCircleFilled, CheckCircleFilled, ExclamationCircleFilled, @@ -27,43 +26,13 @@ import { import { Popover } from 'src/common/components/index'; import Collapse from 'src/common/components/Collapse'; import { Global } from '@emotion/core'; -import { - Indent, - Item, - ItemIcon, - Panel, - Reset, - Title, - FilterValue, -} from './Styles'; +import Icon from 'src/components/Icon'; +import { Indent, Panel, Reset, Title } from './Styles'; import { Indicator } from './selectors'; -import { getFilterValueForDisplay } from '../nativeFilters/FilterBar/FilterSets/utils'; - -export interface IndicatorProps { - indicator: Indicator; - onClick: (path: string[]) => void; -} - -const Indicator = ({ - indicator: { column, name, value = [], path }, - onClick, -}: IndicatorProps) => { - const resultValue = getFilterValueForDisplay(value); - return ( - onClick([...path, `LABEL-${column}`])}> - - <ItemIcon> - <SearchOutlined /> - </ItemIcon> - {name} - {resultValue ? ': ' : ''} - - {resultValue} - - ); -}; +import FilterIndicator from './FilterIndicator'; export interface DetailsPanelProps { + appliedCrossFilterIndicators: Indicator[]; appliedIndicators: Indicator[]; incompatibleIndicators: Indicator[]; unsetIndicators: Indicator[]; @@ -72,6 +41,7 @@ export interface DetailsPanelProps { } const DetailsPanelPopover = ({ + appliedCrossFilterIndicators = [], appliedIndicators = [], incompatibleIndicators = [], unsetIndicators = [], @@ -80,21 +50,34 @@ const DetailsPanelPopover = ({ }: DetailsPanelProps) => { const theme = useTheme(); - function defaultActivePanel() { - if (incompatibleIndicators.length) return 'incompatible'; - if (appliedIndicators.length) return 'applied'; - return 'unset'; - } + const getDefaultActivePanel = () => { + const result = []; + if (appliedCrossFilterIndicators.length) { + result.push('appliedCrossFilters'); + } + if (appliedIndicators.length) { + result.push('applied'); + } + if (incompatibleIndicators.length) { + result.push('incompatible'); + } + if (result.length) { + return result; + } + return ['unset']; + }; const [activePanels, setActivePanels] = useState(() => [ - defaultActivePanel(), + ...getDefaultActivePanel(), ]); function handlePopoverStatus(isOpen: boolean) { // every time the popover opens, make sure the most relevant panel is active if (isOpen) { - if (!activePanels.includes(defaultActivePanel())) { - setActivePanels([...activePanels, defaultActivePanel()]); + if ( + !activePanels.find(panel => getDefaultActivePanel().includes(panel)) + ) { + setActivePanels([...activePanels, ...getDefaultActivePanel()]); } } } @@ -168,6 +151,33 @@ const DetailsPanelPopover = ({ activeKey={activePanels} onChange={handleActivePanelChange} > + {appliedCrossFilterIndicators.length ? ( + + + {t( + 'Applied Cross Filters (%d)', + appliedCrossFilterIndicators.length, + )} + + } + > + + {appliedCrossFilterIndicators.map(indicator => ( + + ))} + + + ) : null} {appliedIndicators.length ? ( {appliedIndicators.map(indicator => ( - {incompatibleIndicators.map(indicator => ( - {unsetIndicators.map(indicator => ( - void; +} + +const FilterIndicator: FC = ({ + indicator: { column, name, value, path = [] }, + onClick = () => {}, +}) => { + const resultValue = getFilterValueForDisplay(value); + return ( + onClick([...path, `LABEL-${column}`])}> + + <ItemIcon> + <SearchOutlined /> + </ItemIcon> + {name} + {resultValue ? ': ' : ''} + + {resultValue} + + ); +}; + +export default FilterIndicator; diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx index fd3b86802..74ffd1967 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx @@ -48,6 +48,13 @@ export const Pill = styled.div` background: ${({ theme }) => theme.colors.grayscale.dark1}; } + &.has-cross-filters { + background: ${({ theme }) => theme.colors.primary.base}; + &:hover { + background: ${({ theme }) => theme.colors.primary.dark1}; + } + } + &.has-incompatible-filters { color: ${({ theme }) => theme.colors.grayscale.dark2}; background: ${({ theme }) => theme.colors.alert.base}; @@ -73,15 +80,6 @@ export const Pill = styled.div` } `; -export const WarningPill = styled(Pill)` - background: ${({ theme }) => theme.colors.alert.base}; - color: ${({ theme }) => theme.colors.grayscale.dark1}; -`; - -export const UnsetPill = styled(Pill)` - background: ${({ theme }) => theme.colors.grayscale.light1}; -`; - export interface TitleProps { bold?: boolean; color?: string; @@ -95,6 +93,11 @@ export const Title = styled.span` return 'auto'; }}; color: ${({ color, theme }) => color || theme.colors.grayscale.light5}; + display: flex; + align-items: center; + & > * { + margin-right: ${({ theme }) => theme.gridUnit}px; + } `; export const ItemIcon = styled.i` diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx index 92d0ab088..cad111ef1 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx @@ -24,6 +24,7 @@ import { Pill } from './Styles'; import { Indicator } from './selectors'; export interface FiltersBadgeProps { + appliedCrossFilterIndicators: Indicator[]; appliedIndicators: Indicator[]; unsetIndicators: Indicator[]; incompatibleIndicators: Indicator[]; @@ -31,12 +32,14 @@ export interface FiltersBadgeProps { } const FiltersBadge = ({ + appliedCrossFilterIndicators, appliedIndicators, unsetIndicators, incompatibleIndicators, onHighlightFilterSource, }: FiltersBadgeProps) => { if ( + !appliedCrossFilterIndicators.length && !appliedIndicators.length && !incompatibleIndicators.length && !unsetIndicators.length @@ -45,10 +48,13 @@ const FiltersBadge = ({ } const isInactive = - !appliedIndicators.length && !incompatibleIndicators.length; + !appliedCrossFilterIndicators.length && + !appliedIndicators.length && + !incompatibleIndicators.length; return ( {!isInactive && ( - {appliedIndicators.length} + {appliedIndicators.length + appliedCrossFilterIndicators.length} )} {incompatibleIndicators.length ? ( diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts index 895eb26f0..2e5e7ac4b 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts @@ -18,16 +18,19 @@ */ import { TIME_FILTER_MAP } from 'src/explore/constants'; import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters'; -import { NativeFiltersState } from 'src/dashboard/reducers/types'; -import { DataMaskStateWithId } from 'src/dataMask/types'; +import { + ChartConfiguration, + NativeFiltersState, +} from 'src/dashboard/reducers/types'; +import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types'; import { Layout } from '../../types'; import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils'; -import { FilterValue } from '../nativeFilters/types'; export enum IndicatorStatus { Unset = 'UNSET', Applied = 'APPLIED', Incompatible = 'INCOMPATIBLE', + CrossFilterApplied = 'CROSS_FILTER_APPLIED', } const TIME_GRANULARITY_FIELDS = new Set(Object.values(TIME_FILTER_MAP)); @@ -53,7 +56,7 @@ const selectIndicatorValue = ( columnKey: string, filter: Filter, datasource: Datasource, -): FilterValue => { +): any => { const values = filter.columns[columnKey]; const arrValues = Array.isArray(values) ? values : [values]; @@ -133,9 +136,9 @@ const getRejectedColumns = (chart: any): Set => export type Indicator = { column?: string; name: string; - value: FilterValue; - status: IndicatorStatus; - path: string[]; + value?: any; + status?: IndicatorStatus; + path?: string[]; }; // inspects redux state to find what the filter indicators should be shown for a given chart @@ -179,22 +182,32 @@ export const selectNativeIndicatorsForChart = ( chartId: number, charts: any, dashboardLayout: Layout, + chartConfiguration: ChartConfiguration = {}, ): Indicator[] => { const chart = charts[chartId]; const appliedColumns = getAppliedColumns(chart); const rejectedColumns = getRejectedColumns(chart); - const getStatus = ( - value: FilterValue, - isAffectedByScope: boolean, - column?: string, - ): IndicatorStatus => { + const getStatus = ({ + value, + isAffectedByScope, + column, + type = DataMaskType.NativeFilters, + }: { + value: any; + isAffectedByScope: boolean; + column?: string; + type?: DataMaskType; + }): IndicatorStatus => { // a filter is only considered unset if it's value is null const hasValue = value !== null; if (!isAffectedByScope) { return IndicatorStatus.Unset; } + if (type === DataMaskType.CrossFilters && hasValue) { + return IndicatorStatus.CrossFilterApplied; + } if (!column && hasValue) { // Filter without datasource return IndicatorStatus.Applied; @@ -207,26 +220,59 @@ export const selectNativeIndicatorsForChart = ( return IndicatorStatus.Unset; }; - const indicators = Object.values(nativeFilters.filters).map(nativeFilter => { - const isAffectedByScope = getTreeCheckedItems( - nativeFilter.scope, - dashboardLayout, - ).some( - layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId, - ); - const column = nativeFilter.targets[0]?.column?.name; - const dataMaskNativeFilters = dataMask.nativeFilters?.[nativeFilter.id]; - let value = dataMaskNativeFilters?.currentState?.value ?? []; - if (!Array.isArray(value)) { - value = [value]; - } - return { - column, - name: nativeFilter.name, - path: [nativeFilter.id], - status: getStatus(value, isAffectedByScope, column), - value, - }; - }); - return indicators; + const nativeFilterIndicators = Object.values(nativeFilters.filters).map( + nativeFilter => { + const isAffectedByScope = getTreeCheckedItems( + nativeFilter.scope, + dashboardLayout, + ).some( + layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId, + ); + const column = nativeFilter.targets[0]?.column?.name; + const dataMaskNativeFilters = dataMask.nativeFilters?.[nativeFilter.id]; + let value = dataMaskNativeFilters?.currentState?.value ?? null; + if (!Array.isArray(value) && value !== null) { + value = [value]; + } + return { + column, + name: nativeFilter.name, + path: [nativeFilter.id], + status: getStatus({ value, isAffectedByScope, column }), + value, + }; + }, + ); + + const crossFilterIndicators = Object.values(chartConfiguration).map( + chartConfig => { + const scope = chartConfig?.crossFilters?.scope; + const isAffectedByScope = getTreeCheckedItems( + scope, + dashboardLayout, + ).some( + layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId, + ); + + const dataMaskCrossFilters = dataMask.crossFilters?.[chartConfig.id]; + let value = dataMaskCrossFilters?.currentState?.value ?? null; + if (!Array.isArray(value) && value !== null) { + value = [value]; + } + return { + name: Object.values(dashboardLayout).find( + layoutItem => layoutItem?.meta?.chartId === chartConfig.id, + )?.meta?.sliceName as string, + path: [`${chartConfig.id}`], + status: getStatus({ + value, + isAffectedByScope, + type: DataMaskType.CrossFilters, + }), + value, + }; + }, + ); + + return crossFilterIndicators.concat(nativeFilterIndicators); }; diff --git a/superset-frontend/src/dashboard/components/SliceHeader.jsx b/superset-frontend/src/dashboard/components/SliceHeader.jsx deleted file mode 100644 index 5f9ecd397..000000000 --- a/superset-frontend/src/dashboard/components/SliceHeader.jsx +++ /dev/null @@ -1,179 +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 React from 'react'; -import PropTypes from 'prop-types'; - -import { t } from '@superset-ui/core'; -import { Tooltip } from 'src/common/components/Tooltip'; -import EditableTitle from '../../components/EditableTitle'; -import SliceHeaderControls from './SliceHeaderControls'; -import FiltersBadge from '../containers/FiltersBadge'; - -const propTypes = { - innerRef: PropTypes.func, - slice: PropTypes.object.isRequired, - isExpanded: PropTypes.bool, - isCached: PropTypes.arrayOf(PropTypes.bool), - cachedDttm: PropTypes.arrayOf(PropTypes.string), - updatedDttm: PropTypes.number, - updateSliceName: PropTypes.func, - toggleExpandSlice: PropTypes.func, - forceRefresh: PropTypes.func, - exploreChart: PropTypes.func, - exportCSV: PropTypes.func, - editMode: PropTypes.bool, - annotationQuery: PropTypes.object, - annotationError: PropTypes.object, - sliceName: PropTypes.string, - supersetCanExplore: PropTypes.bool, - supersetCanCSV: PropTypes.bool, - sliceCanEdit: PropTypes.bool, - componentId: PropTypes.string.isRequired, - dashboardId: PropTypes.number.isRequired, - filters: PropTypes.object.isRequired, - addSuccessToast: PropTypes.func.isRequired, - addDangerToast: PropTypes.func.isRequired, - handleToggleFullSize: PropTypes.func.isRequired, - chartStatus: PropTypes.string.isRequired, -}; - -const defaultProps = { - innerRef: null, - forceRefresh: () => ({}), - updateSliceName: () => ({}), - toggleExpandSlice: () => ({}), - exploreChart: () => ({}), - exportCSV: () => ({}), - editMode: false, - annotationQuery: {}, - annotationError: {}, - cachedDttm: null, - updatedDttm: null, - isCached: [], - isExpanded: [], - sliceName: '', - supersetCanExplore: false, - supersetCanCSV: false, - sliceCanEdit: false, -}; - -const annoationsLoading = t('Annotation layers are still loading.'); -const annoationsError = t('One ore more annotation layers failed loading.'); - -class SliceHeader extends React.PureComponent { - render() { - const { - slice, - isExpanded, - isCached, - cachedDttm, - updatedDttm, - toggleExpandSlice, - forceRefresh, - exploreChart, - exportCSV, - innerRef, - sliceName, - supersetCanExplore, - supersetCanCSV, - sliceCanEdit, - editMode, - updateSliceName, - annotationQuery, - annotationError, - componentId, - dashboardId, - addSuccessToast, - addDangerToast, - handleToggleFullSize, - isFullSize, - chartStatus, - } = this.props; - - return ( -
-
- - {!!Object.values(annotationQuery).length && ( - - - - )} - {!!Object.values(annotationError).length && ( - - - - )} -
-
- {!editMode && ( - <> - - - - )} -
-
- ); - } -} - -SliceHeader.propTypes = propTypes; -SliceHeader.defaultProps = defaultProps; - -export default SliceHeader; diff --git a/superset-frontend/src/dashboard/components/SliceHeader.tsx b/superset-frontend/src/dashboard/components/SliceHeader.tsx new file mode 100644 index 000000000..85eaaa05d --- /dev/null +++ b/superset-frontend/src/dashboard/components/SliceHeader.tsx @@ -0,0 +1,184 @@ +/** + * 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, { FC } from 'react'; +import { styled, t } from '@superset-ui/core'; +import { Tooltip } from 'src/common/components/Tooltip'; +import { useSelector } from 'react-redux'; +import EditableTitle from '../../components/EditableTitle'; +import SliceHeaderControls from './SliceHeaderControls'; +import FiltersBadge from '../containers/FiltersBadge'; +import Icon from '../../components/Icon'; +import { RootState } from '../types'; +import { Slice } from '../../types/Chart'; +import FilterIndicator from './FiltersBadge/FilterIndicator'; + +type SliceHeaderProps = { + innerRef?: string; + slice: Slice; + isExpanded?: boolean; + isCached?: boolean[]; + cachedDttm?: string[]; + updatedDttm?: number; + updateSliceName?: (arg0: string) => void; + toggleExpandSlice?: Function; + forceRefresh?: Function; + exploreChart?: Function; + exportCSV?: Function; + editMode?: boolean; + isFullSize?: boolean; + annotationQuery?: object; + annotationError?: object; + sliceName?: string; + supersetCanExplore?: boolean; + supersetCanCSV?: boolean; + sliceCanEdit?: boolean; + componentId: string; + dashboardId: number; + filters: object; + addSuccessToast: Function; + addDangerToast: Function; + handleToggleFullSize: Function; + chartStatus: string; +}; + +const annoationsLoading = t('Annotation layers are still loading.'); +const annoationsError = t('One ore more annotation layers failed loading.'); + +const CrossFilterIcon = styled(Icon)` + fill: ${({ theme }) => theme.colors.grayscale.light5}; + & circle { + fill: ${({ theme }) => theme.colors.primary.base}; + } +`; + +const SliceHeader: FC = ({ + innerRef = null, + forceRefresh = () => ({}), + updateSliceName = () => ({}), + toggleExpandSlice = () => ({}), + exploreChart = () => ({}), + exportCSV = () => ({}), + editMode = false, + annotationQuery = {}, + annotationError = {}, + cachedDttm = null, + updatedDttm = null, + isCached = [], + isExpanded = [], + sliceName = '', + supersetCanExplore = false, + supersetCanCSV = false, + sliceCanEdit = false, + slice, + componentId, + dashboardId, + addSuccessToast, + addDangerToast, + handleToggleFullSize, + isFullSize, + chartStatus, +}) => { + // TODO: change to indicator field after it will be implemented + const crossFilterValue = useSelector( + state => + state.dataMask?.crossFilters?.[slice?.slice_id]?.currentState?.value, + ); + + return ( +
+
+ + {!!Object.values(annotationQuery).length && ( + + + + )} + {!!Object.values(annotationError).length && ( + + + + )} +
+
+ {!editMode && ( + <> + {crossFilterValue && ( + + } + > + + + )} + + + + )} +
+
+ ); +}; + +export default SliceHeader; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx index 09321ca11..c95e8b9f9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx @@ -33,6 +33,7 @@ type FilterScopeProps = { forceUpdate: Function; scope?: Scope; formScoping?: Scoping; + chartId?: number; }; const Wrapper = styled.div` @@ -54,9 +55,10 @@ const FilterScope: FC = ({ forceUpdate, scope, updateFormValues, + chartId, }) => { - const initialScope = scope || getDefaultScopeValue(); - const initialScoping = isScopingAll(initialScope) + const initialScope = scope || getDefaultScopeValue(chartId); + const initialScoping = isScopingAll(initialScope, chartId) ? Scoping.all : Scoping.specific; @@ -70,8 +72,9 @@ const FilterScope: FC = ({ { if (value === Scoping.all) { + const scope = getDefaultScopeValue(chartId); updateFormValues({ - scope: getDefaultScopeValue(), + scope, }); } forceUpdate(); @@ -94,6 +97,7 @@ const FilterScope: FC = ({ initialScope={initialScope} formScope={formScope} forceUpdate={forceUpdate} + chartId={chartId} /> )} void; formScope?: Scope; initialScope: Scope; + chartId?: number; }; const ScopingTree: FC = ({ @@ -36,22 +37,28 @@ const ScopingTree: FC = ({ initialScope, forceUpdate, updateFormValues, + chartId, }) => { const [expandedKeys, setExpandedKeys] = useState([ DASHBOARD_ROOT_ID, ]); - const { treeData, layout } = useFilterScopeTree(); + const { treeData, layout } = useFilterScopeTree(chartId); const [autoExpandParent, setAutoExpandParent] = useState(true); const handleExpand = (expandedKeys: string[]) => { setExpandedKeys(expandedKeys); setAutoExpandParent(false); }; + const handleCheck = (checkedKeys: string[]) => { forceUpdate(); + const scope = findFilterScope(checkedKeys, layout); + if (chartId !== undefined) { + scope.excluded = [...new Set([...scope.excluded, chartId])]; + } updateFormValues({ - scope: findFilterScope(checkedKeys, layout), + scope, }); }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/state.ts index d4a420d90..9cd4490e9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/state.ts @@ -29,7 +29,9 @@ import { TreeItem } from './types'; import { buildTree } from './utils'; // eslint-disable-next-line import/prefer-default-export -export function useFilterScopeTree(): { +export function useFilterScopeTree( + currentChartId?: number, +): { treeData: [TreeItem]; layout: Layout; } { @@ -49,12 +51,12 @@ export function useFilterScopeTree(): { const validNodes = useMemo( () => Object.values(layout).reduce((acc, cur) => { - if (cur?.type === CHART_TYPE) { + if (cur?.type === CHART_TYPE && currentChartId !== cur?.meta?.chartId) { return [...new Set([...acc, ...cur?.parents, cur.id])]; } return acc; }, []), - [layout], + [layout, currentChartId], ); useMemo(() => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts index 9f7d67486..e6936e46b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts @@ -145,10 +145,12 @@ export const findFilterScope = ( }; }; -export const getDefaultScopeValue = () => ({ +export const getDefaultScopeValue = (chartId?: number): Scope => ({ rootPath: [DASHBOARD_ROOT_ID], - excluded: [], + excluded: chartId ? [chartId] : [], }); -export const isScopingAll = (scope: Scope) => - !scope || (scope.rootPath[0] === DASHBOARD_ROOT_ID && !scope.excluded.length); +export const isScopingAll = (scope: Scope, chartId?: number) => + !scope || + (scope.rootPath[0] === DASHBOARD_ROOT_ID && + !scope.excluded.filter(item => item !== chartId).length); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/types.ts index d816b0ae3..f10452344 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/types.ts @@ -37,12 +37,10 @@ export interface Target { // clarityColumns?: Column[]; } -export type FilterValue = string | number | (string | number)[] | null; - export interface Filter { cascadeParentIds: string[]; - defaultValue: FilterValue; - currentValue?: FilterValue; + defaultValue: any; + currentValue?: any; isInstant: boolean; id: string; // randomly generated at filter creation name: string; diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 3077e8ad0..94f88d3a9 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -87,6 +87,7 @@ function mapStateToProps( supersetCanCSV: !!dashboardInfo.superset_can_csv, sliceCanEdit: !!dashboardInfo.slice_can_edit, ownCurrentState: dataMask.ownFilters?.[id]?.currentState, + crossFilterCurrentState: dataMask.crossFilters?.[id]?.currentState, }; } diff --git a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx index c38e320ba..546b96303 100644 --- a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx +++ b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx @@ -47,7 +47,9 @@ const sortByStatus = (indicators: Indicator[]): Indicator[] => { IndicatorStatus.Incompatible, ]; return indicators.sort( - (a, b) => statuses.indexOf(a.status) - statuses.indexOf(b.status), + (a, b) => + statuses.indexOf(a.status as IndicatorStatus) - + statuses.indexOf(b.status as IndicatorStatus), ); }; @@ -56,6 +58,7 @@ const mapStateToProps = ( datasources, dashboardFilters, nativeFilters, + dashboardInfo, charts, dataMask, dashboardLayout: { present }, @@ -75,6 +78,7 @@ const mapStateToProps = ( chartId, charts, present, + dashboardInfo.metadata?.chart_configuration, ); const indicators = uniqWith( @@ -86,6 +90,9 @@ const mapStateToProps = ( ind2.status !== IndicatorStatus.Applied), ); + const appliedCrossFilterIndicators = indicators.filter( + indicator => indicator.status === IndicatorStatus.CrossFilterApplied, + ); const appliedIndicators = indicators.filter( indicator => indicator.status === IndicatorStatus.Applied, ); @@ -99,6 +106,7 @@ const mapStateToProps = ( return { chartId, appliedIndicators, + appliedCrossFilterIndicators, unsetIndicators, incompatibleIndicators, }; diff --git a/superset-frontend/src/dashboard/reducers/types.ts b/superset-frontend/src/dashboard/reducers/types.ts index 21365f638..7e7e88f16 100644 --- a/superset-frontend/src/dashboard/reducers/types.ts +++ b/superset-frontend/src/dashboard/reducers/types.ts @@ -27,7 +27,8 @@ export enum Scoping { } export type ChartConfiguration = { - [chartId: string]: { + [chartId: number]: { + id: number; crossFilters: { scope: Scope; }; diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 86fa4bf00..696eda40b 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -19,6 +19,7 @@ import { ChartProps } from '@superset-ui/core'; import { chart } from 'src/chart/chartReducer'; import componentTypes from 'src/dashboard/util/componentTypes'; +import { DataMaskStateWithId } from '../dataMask/types'; export type ChartReducerInitialState = typeof chart; @@ -44,6 +45,7 @@ export type RootState = { charts: { [key: string]: Chart }; dashboardLayout: { present: { [key: string]: LayoutItem } }; dashboardFilters: {}; + dataMask: DataMaskStateWithId; }; /** State of dashboardLayout in redux */