From 5009444b71ddf06217c49f2479e49c98a091109b Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Thu, 18 Mar 2021 12:21:50 +0200 Subject: [PATCH 1/9] fix: fix removeing native filter --- superset-frontend/src/dataMask/reducer.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index a027a12c35649..0cb6e277923aa 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -28,13 +28,11 @@ import { UpdateDataMask, } from './actions'; -export function getInitialMask(id: string): MaskWithId { - return { - id, - extraFormData: {}, - currentState: {}, - }; -} +export const getInitialMask = (id: string): MaskWithId => ({ + id, + extraFormData: {}, + currentState: {}, +}); const setUnitDataMask = ( unitName: DataMaskType, @@ -49,11 +47,11 @@ const setUnitDataMask = ( } }; -const emptyDataMask = { +const getEmptyDataMask = () => ({ [DataMaskType.NativeFilters]: {}, [DataMaskType.CrossFilters]: {}, [DataMaskType.OwnFilters]: {}, -}; +}); const dataMaskReducer = produce( (draft: DataMaskStateWithId, action: AnyDataMaskAction) => { @@ -66,7 +64,7 @@ const dataMaskReducer = produce( case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE: Object.values(DataMaskType).forEach(unitName => { - draft[unitName] = emptyDataMask[unitName]; + draft[unitName] = getEmptyDataMask()[unitName]; }); (action.filterConfig ?? []).forEach(filter => { draft[DataMaskType.NativeFilters][filter.id] = @@ -78,7 +76,7 @@ const dataMaskReducer = produce( default: } }, - emptyDataMask, + getEmptyDataMask(), ); export default dataMaskReducer; From ece904b8562fb7121625cfc1bf3a2b909055d38f Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Thu, 18 Mar 2021 14:25:00 +0200 Subject: [PATCH 2/9] fix: fix native-cross filters --- .../src/dashboard/actions/nativeFilters.ts | 1 + .../nativeFilters/FilterBar/FilterBar.tsx | 24 +++++++++++++++++++ superset-frontend/src/dataMask/actions.ts | 1 + superset-frontend/src/dataMask/reducer.ts | 22 +++++++---------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 8d183271e9edc..fca59f2bafd70 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -96,6 +96,7 @@ export const setFilterConfiguration = ( }); dispatch({ type: SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, + unitName: DataMaskType.NativeFilters, filterConfig, }); } catch (err) { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index 098bd6fc635f0..2e38147606ae4 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -240,6 +240,30 @@ const FilterBar: React.FC = ({ } }, [filterValues.length]); + useEffect(() => { + // Remove deleted filters from local state + Object.keys(dataMaskSelected).forEach(selectedId => { + if (!filters[selectedId]) { + setDataMaskSelected(draft => { + delete draft[selectedId]; + }); + } + }); + Object.keys(dataMaskApplied).forEach(appliedId => { + if (!filters[appliedId]) { + setLastAppliedFilterData(draft => { + delete draft[appliedId]; + }); + } + }); + }, [ + dataMaskApplied, + dataMaskSelected, + filters, + setDataMaskSelected, + setLastAppliedFilterData, + ]); + const cascadeChildren = useMemo( () => mapParentFiltersToChildren(filterValues), [filterValues], diff --git a/superset-frontend/src/dataMask/actions.ts b/superset-frontend/src/dataMask/actions.ts index 4340661949b74..5432c30d4d11f 100644 --- a/superset-frontend/src/dataMask/actions.ts +++ b/superset-frontend/src/dataMask/actions.ts @@ -33,6 +33,7 @@ export const SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE = export interface SetDataMaskForFilterConfigComplete { type: typeof SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE; filterConfig: FilterConfiguration; + unitName: DataMaskType; } export const SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL = 'SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL'; diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index 0cb6e277923aa..c18e55b1f60c4 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -47,14 +47,9 @@ const setUnitDataMask = ( } }; -const getEmptyDataMask = () => ({ - [DataMaskType.NativeFilters]: {}, - [DataMaskType.CrossFilters]: {}, - [DataMaskType.OwnFilters]: {}, -}); - const dataMaskReducer = produce( (draft: DataMaskStateWithId, action: AnyDataMaskAction) => { + const oldData = { ...draft }; switch (action.type) { case UPDATE_DATA_MASK: Object.values(DataMaskType).forEach(unitName => @@ -63,20 +58,21 @@ const dataMaskReducer = produce( break; case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE: - Object.values(DataMaskType).forEach(unitName => { - draft[unitName] = getEmptyDataMask()[unitName]; - }); + draft[action.unitName] = {}; (action.filterConfig ?? []).forEach(filter => { - draft[DataMaskType.NativeFilters][filter.id] = - draft[DataMaskType.NativeFilters][filter.id] ?? - getInitialMask(filter.id); + draft[action.unitName][filter.id] = + oldData[action.unitName][filter.id] ?? getInitialMask(filter.id); }); break; default: } }, - getEmptyDataMask(), + { + [DataMaskType.NativeFilters]: {}, + [DataMaskType.CrossFilters]: {}, + [DataMaskType.OwnFilters]: {}, + }, ); export default dataMaskReducer; From 1bd52b4032f8a5e3a40f972c267ce426d535a513 Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Thu, 18 Mar 2021 14:34:13 +0200 Subject: [PATCH 3/9] fix: fix native-cross filters --- superset-frontend/src/dataMask/reducer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index c18e55b1f60c4..4817d6cc7cdf9 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -49,7 +49,6 @@ const setUnitDataMask = ( const dataMaskReducer = produce( (draft: DataMaskStateWithId, action: AnyDataMaskAction) => { - const oldData = { ...draft }; switch (action.type) { case UPDATE_DATA_MASK: Object.values(DataMaskType).forEach(unitName => @@ -61,7 +60,7 @@ const dataMaskReducer = produce( draft[action.unitName] = {}; (action.filterConfig ?? []).forEach(filter => { draft[action.unitName][filter.id] = - oldData[action.unitName][filter.id] ?? getInitialMask(filter.id); + draft[action.unitName][filter.id] ?? getInitialMask(filter.id); }); break; From dd6cc4d7e6fd84134de4579e054635cc41661c26 Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Thu, 18 Mar 2021 14:56:13 +0200 Subject: [PATCH 4/9] fix: fix native-cross filters --- superset-frontend/src/dataMask/reducer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index 4817d6cc7cdf9..c18e55b1f60c4 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -49,6 +49,7 @@ const setUnitDataMask = ( const dataMaskReducer = produce( (draft: DataMaskStateWithId, action: AnyDataMaskAction) => { + const oldData = { ...draft }; switch (action.type) { case UPDATE_DATA_MASK: Object.values(DataMaskType).forEach(unitName => @@ -60,7 +61,7 @@ const dataMaskReducer = produce( draft[action.unitName] = {}; (action.filterConfig ?? []).forEach(filter => { draft[action.unitName][filter.id] = - draft[action.unitName][filter.id] ?? getInitialMask(filter.id); + oldData[action.unitName][filter.id] ?? getInitialMask(filter.id); }); break; From 52389effa5e53f0280288bd46c8c5298962b179a Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Thu, 18 Mar 2021 16:37:24 +0200 Subject: [PATCH 5/9] fix: fix function declaration --- superset-frontend/src/dataMask/reducer.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index c18e55b1f60c4..387f81179b71c 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -28,11 +28,13 @@ import { UpdateDataMask, } from './actions'; -export const getInitialMask = (id: string): MaskWithId => ({ - id, - extraFormData: {}, - currentState: {}, -}); +export function getInitialMask(id: string): MaskWithId { + return { + id, + extraFormData: {}, + currentState: {}, + }; +} const setUnitDataMask = ( unitName: DataMaskType, From d62fd36a666891a8932344f3bb34b593976c0588 Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Sun, 21 Mar 2021 08:04:39 +0200 Subject: [PATCH 6/9] refactor: before pull --- .../CascadeFilterControl.tsx | 4 +- .../{ => CascadeFilters}/CascadePopover.tsx | 4 +- .../FilterBar/CascadeFilters/types.ts | 24 +++ .../nativeFilters/FilterBar/FilterBar.tsx | 167 ++++-------------- .../{ => FilterControls}/FilterControl.tsx | 2 +- .../FilterControls/FilterControls.tsx | 58 ++++++ .../{ => FilterControls}/FilterValue.tsx | 4 +- .../FilterBar/FilterControls/state.ts | 38 ++++ .../FilterBar/FilterControls/utils.ts | 37 ++++ .../nativeFilters/FilterBar/Header.tsx | 131 ++++++++++++++ .../nativeFilters/FilterBar/state.ts | 49 +++-- .../nativeFilters/FilterBar/types.ts | 4 - .../nativeFilters/FilterBar/utils.ts | 17 -- 13 files changed, 358 insertions(+), 181 deletions(-) rename superset-frontend/src/dashboard/components/nativeFilters/FilterBar/{ => CascadeFilters}/CascadeFilterControl.tsx (95%) rename superset-frontend/src/dashboard/components/nativeFilters/FilterBar/{ => CascadeFilters}/CascadePopover.tsx (98%) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types.ts rename superset-frontend/src/dashboard/components/nativeFilters/FilterBar/{ => FilterControls}/FilterControl.tsx (98%) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx rename superset-frontend/src/dashboard/components/nativeFilters/FilterBar/{ => FilterControls}/FilterValue.tsx (98%) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header.tsx diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx similarity index 95% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx index a0e6d673a2259..63ed30f6b7f6f 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx @@ -19,8 +19,8 @@ import React from 'react'; import { styled, DataMask } from '@superset-ui/core'; import Icon from 'src/components/Icon'; -import FilterControl from './FilterControl'; -import { Filter } from '../types'; +import FilterControl from '../FilterControl/FilterControl'; +import { Filter } from '../../types'; import { CascadeFilter } from './types'; interface CascadeFilterControlProps { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx similarity index 98% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx index 430003bb1bafb..5968efa573a41 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx @@ -24,10 +24,10 @@ import { Pill } from 'src/dashboard/components/FiltersBadge/Styles'; import { useSelector } from 'react-redux'; import { getInitialMask } from 'src/dataMask/reducer'; import { MaskWithId } from 'src/dataMask/types'; -import FilterControl from './FilterControl'; +import FilterControl from '../FilterControl/FilterControl'; import CascadeFilterControl from './CascadeFilterControl'; import { CascadeFilter } from './types'; -import { Filter } from '../types'; +import { Filter } from '../../types'; interface CascadePopoverProps { filter: CascadeFilter; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types.ts new file mode 100644 index 0000000000000..435404669efe9 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types.ts @@ -0,0 +1,24 @@ +/** + * 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 { Filter } from '../../types'; + +export interface CascadeFilter extends Filter { + cascadeChildren: CascadeFilter[]; +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index 2e38147606ae4..6a4f4573adb13 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -20,24 +20,27 @@ /* eslint-disable no-param-reassign */ import { HandlerFunction, styled, t } from '@superset-ui/core'; import React, { useState, useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import cx from 'classnames'; -import Button from 'src/components/Button'; import Icon from 'src/components/Icon'; import { Tabs } from 'src/common/components'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { updateDataMask } from 'src/dataMask/actions'; import { DataMaskUnit, DataMaskState } from 'src/dataMask/types'; import { useImmer } from 'use-immer'; -import { getInitialMask } from 'src/dataMask/reducer'; import { areObjectsEqual } from 'src/reduxUtils'; -import FilterConfigurationLink from './FilterConfigurationLink'; import { Filter } from '../types'; -import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils'; -import CascadePopover from './CascadePopover'; +import { mapParentFiltersToChildren } from './utils'; import FilterSets from './FilterSets/FilterSets'; -import { useDataMask, useFilters, useFilterSets } from './state'; +import { + useDataMask, + useFilters, + useFilterSets, + useFiltersInitialisation, +} from './state'; import EditSection from './FilterSets/EditSection'; +import Header from './Header'; +import FilterControls from './FilterControl/FilterControls'; const barWidth = `250px`; @@ -122,18 +125,6 @@ const StyledCollapseIcon = styled(Icon)` margin-bottom: ${({ theme }) => theme.gridUnit * 3}px; `; -const TitleArea = styled.h4` - display: flex; - flex-direction: row; - justify-content: space-between; - margin: 0; - padding: ${({ theme }) => theme.gridUnit * 2}px; - - & > span { - flex-grow: 1; - } -`; - const StyledTabs = styled(Tabs)` & .ant-tabs-nav-list { width: 100%; @@ -146,28 +137,6 @@ const StyledTabs = styled(Tabs)` } `; -const ActionButtons = styled.div` - display: grid; - flex-direction: row; - justify-content: center; - align-items: center; - grid-gap: 10px; - grid-template-columns: 1fr 1fr; - ${({ theme }) => - `padding: 0 ${theme.gridUnit * 2}px ${theme.gridUnit * 2}px`}; - - .btn { - flex: 1; - } -`; - -const FilterControls = styled.div` - padding: ${({ theme }) => theme.gridUnit * 4}px; - &:hover { - cursor: pointer; - } -`; - interface FiltersBarProps { filtersOpen: boolean; toggleFiltersBar: any; @@ -198,11 +167,6 @@ const FilterBar: React.FC = ({ const filters = useFilters(); const filterValues = Object.values(filters); const dataMaskApplied = useDataMask(); - const canEdit = useSelector( - ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, - ); - const [visiblePopoverId, setVisiblePopoverId] = useState(null); - const [isInitialized, setIsInitialized] = useState(false); const handleApply = () => { const filterIds = Object.keys(dataMaskSelected); @@ -218,21 +182,10 @@ const FilterBar: React.FC = ({ setLastAppliedFilterData(() => dataMaskSelected); }; - useEffect(() => { - if (isInitialized) { - return; - } - const areFiltersInitialized = filterValues.every(filterValue => - areObjectsEqual( - filterValue?.defaultValue, - dataMaskSelected[filterValue?.id]?.currentState?.value, - ), - ); - if (areFiltersInitialized) { - handleApply(); - setIsInitialized(true); - } - }, [filterValues, dataMaskSelected, isInitialized]); + const { isInitialized } = useFiltersInitialisation( + dataMaskSelected, + handleApply, + ); useEffect(() => { if (filterValues.length === 0 && filtersOpen) { @@ -269,14 +222,6 @@ const FilterBar: React.FC = ({ [filterValues], ); - const cascadeFilters = useMemo(() => { - const filtersWithValue = filterValues.map(filter => ({ - ...filter, - currentValue: dataMaskSelected[filter.id]?.currentState?.value, - })); - return buildCascadeFiltersTree(filtersWithValue); - }, [filterValues, dataMaskSelected]); - const handleFilterSelectionChange = ( filter: Pick & Partial, dataMask: Partial, @@ -295,38 +240,6 @@ const FilterBar: React.FC = ({ }); }; - const handleClearAll = () => { - filterValues.forEach(filter => { - setDataMaskSelected(draft => { - draft[filter.id] = getInitialMask(filter.id); - }); - }); - }; - - const isClearAllDisabled = Object.values(dataMaskApplied).every( - filter => - dataMaskSelected[filter.id]?.currentState?.value === null || - (!dataMaskSelected[filter.id] && filter.currentState?.value === null), - ); - - const getFilterControls = () => ( - - {cascadeFilters.map(filter => ( - - setVisiblePopoverId(visible ? filter.id : null) - } - filter={filter} - onFilterSelectionChange={handleFilterSelectionChange} - directPathToChild={directPathToChild} - /> - ))} - - ); - const isApplyDisabled = !isInitialized || areObjectsEqual(dataMaskSelected, lastAppliedFilterData); @@ -340,38 +253,14 @@ const FilterBar: React.FC = ({ - - {t('Filters')} - {canEdit && ( - - - - )} - toggleFiltersBar(false)} /> - - - - - +
{isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? ( = ({ filterSetId={editFilterSetId} /> )} - {getFilterControls()} + = ({ ) : ( - getFilterControls() + )} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx similarity index 98% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx index 157f50e09d449..be3e9c2c431eb 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { styled } from '@superset-ui/core'; import FilterValue from './FilterValue'; -import { FilterProps } from './types'; +import { FilterProps } from '../types'; const StyledFilterControlTitle = styled.h4` width: 100%; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx new file mode 100644 index 0000000000000..470e41f4a1340 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -0,0 +1,58 @@ +import React, { FC, useMemo, useState } from 'react'; +import { DataMaskUnit } from 'src/dataMask/types'; +import { DataMask, styled } from '@superset-ui/core'; +import CascadePopover from '../CascadeFilters/CascadePopover'; +import { buildCascadeFiltersTree } from './utils'; +import { useFilters } from '../state'; +import { Filter } from '../../types'; + +const Wrapper = styled.div` + padding: ${({ theme }) => theme.gridUnit * 4}px; + &:hover { + cursor: pointer; + } +`; + +type FilterControlsProps = { + directPathToChild?: string[]; + dataMaskSelected: DataMaskUnit; + onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void; +}; + +const FilterControls: FC = ({ + directPathToChild, + dataMaskSelected, + onFilterSelectionChange, +}) => { + const [visiblePopoverId, setVisiblePopoverId] = useState(null); + const filters = useFilters(); + const filterValues = Object.values(filters); + + const cascadeFilters = useMemo(() => { + const filtersWithValue = filterValues.map(filter => ({ + ...filter, + currentValue: dataMaskSelected[filter.id]?.currentState?.value, + })); + return buildCascadeFiltersTree(filtersWithValue); + }, [filterValues, dataMaskSelected]); + + return ( + + {cascadeFilters.map(filter => ( + + setVisiblePopoverId(visible ? filter.id : null) + } + filter={filter} + onFilterSelectionChange={onFilterSelectionChange} + directPathToChild={directPathToChild} + /> + ))} + + ); +}; + +export default FilterControls; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx similarity index 98% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx index 9b5fb920799be..75da0dc2e14ec 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx @@ -29,8 +29,8 @@ import { areObjectsEqual } from 'src/reduxUtils'; import { getChartDataRequest } from 'src/chart/chartAction'; import Loading from 'src/components/Loading'; import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert'; -import { FilterProps } from './types'; -import { getFormData } from '../utils'; +import { FilterProps } from '../types'; +import { getFormData } from '../../utils'; import { useCascadingFilters } from './state'; const StyledLoadingBox = styled.div` diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts new file mode 100644 index 0000000000000..72e29c398ca94 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts @@ -0,0 +1,38 @@ +/** + * 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 { useSelector } from 'react-redux'; +import { NativeFiltersState } from 'src/dashboard/reducers/types'; +import { mergeExtraFormData } from '../../utils'; +import { useDataMask } from '../state'; + +export function useCascadingFilters(id: string) { + const { filters } = useSelector( + state => state.nativeFilters, + ); + const filter = filters[id]; + const cascadeParentIds: string[] = filter?.cascadeParentIds ?? []; + let cascadedFilters = {}; + const nativeFilters = useDataMask(); + cascadeParentIds.forEach(parentId => { + const parentState = nativeFilters[parentId] || {}; + const { extraFormData: parentExtra = {} } = parentState; + cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra); + }); + return cascadedFilters; +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts new file mode 100644 index 0000000000000..e50ffb0b92b13 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts @@ -0,0 +1,37 @@ +/** + * 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 { Filter } from '../../types'; +import { CascadeFilter } from '../CascadeFilters/types'; +import { mapParentFiltersToChildren } from '../utils'; + +export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { + const cascadeChildren = mapParentFiltersToChildren(filters); + + const getCascadeFilter = (filter: Filter): CascadeFilter => { + const children = cascadeChildren[filter.id] || []; + return { + ...filter, + cascadeChildren: children.map(getCascadeFilter), + }; + }; + + return filters + .filter(filter => !filter.cascadeParentIds?.length) + .map(getCascadeFilter); +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header.tsx new file mode 100644 index 0000000000000..0f2282a99a94b --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header.tsx @@ -0,0 +1,131 @@ +/** + * 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. + */ +/* eslint-disable no-param-reassign */ +import { styled, t } from '@superset-ui/core'; +import React, { FC } from 'react'; +import Icon from 'src/components/Icon'; +import Button from 'src/components/Button'; +import { useSelector } from 'react-redux'; +import { getInitialMask } from 'src/dataMask/reducer'; +import { DataMaskUnit, DataMaskUnitWithId } from 'src/dataMask/types'; +import FilterConfigurationLink from './FilterConfigurationLink'; +import { useFilters } from './state'; +import { Filter } from '../types'; + +const TitleArea = styled.h4` + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 0; + padding: ${({ theme }) => theme.gridUnit * 2}px; + + & > span { + flex-grow: 1; + } +`; + +const ActionButtons = styled.div` + display: grid; + flex-direction: row; + justify-content: center; + align-items: center; + grid-gap: 10px; + grid-template-columns: 1fr 1fr; + ${({ theme }) => + `padding: 0 ${theme.gridUnit * 2}px ${theme.gridUnit * 2}px`}; + + .btn { + flex: 1; + } +`; + +type HeaderProps = { + toggleFiltersBar: (arg0: boolean) => void; + onApply: () => void; + setDataMaskSelected: (arg0: (draft: DataMaskUnit) => void) => void; + dataMaskSelected: DataMaskUnit; + dataMaskApplied: DataMaskUnitWithId; + isApplyDisabled: boolean; +}; + +const Header: FC = ({ + onApply, + isApplyDisabled, + dataMaskSelected, + dataMaskApplied, + setDataMaskSelected, + toggleFiltersBar, +}) => { + const filters = useFilters(); + const filterValues = Object.values(filters); + const canEdit = useSelector( + ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, + ); + + const handleClearAll = () => { + filterValues.forEach(filter => { + setDataMaskSelected(draft => { + draft[filter.id] = getInitialMask(filter.id); + }); + }); + }; + + const isClearAllDisabled = Object.values(dataMaskApplied).every( + filter => + dataMaskSelected[filter.id]?.currentState?.value === null || + (!dataMaskSelected[filter.id] && filter.currentState?.value === null), + ); + + return ( + <> + + {t('Filters')} + {canEdit && ( + + + + )} + toggleFiltersBar(false)} /> + + + + + + + ); +}; + +export default Header; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts index db51f30c5ee19..6b38b1f31daa0 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts @@ -20,10 +20,11 @@ import { useSelector } from 'react-redux'; import { Filters, FilterSets as FilterSetsType, - NativeFiltersState, } from 'src/dashboard/reducers/types'; -import { DataMaskUnitWithId } from 'src/dataMask/types'; -import { mergeExtraFormData } from '../utils'; +import { DataMaskUnit, DataMaskUnitWithId } from 'src/dataMask/types'; +import { useEffect, useState } from 'react'; +import { areObjectsEqual } from 'src/reduxUtils'; +import { Filter } from '../types'; export const useFilterSets = () => useSelector( @@ -36,18 +37,30 @@ export const useFilters = () => export const useDataMask = () => useSelector(state => state.dataMask.nativeFilters); -export function useCascadingFilters(id: string) { - const { filters } = useSelector( - state => state.nativeFilters, - ); - const filter = filters[id]; - const cascadeParentIds: string[] = filter?.cascadeParentIds ?? []; - let cascadedFilters = {}; - const nativeFilters = useDataMask(); - cascadeParentIds.forEach(parentId => { - const parentState = nativeFilters[parentId] || {}; - const { extraFormData: parentExtra = {} } = parentState; - cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra); - }); - return cascadedFilters; -} +export const useFiltersInitialisation = ( + dataMaskSelected: DataMaskUnit, + handleApply: () => void, +) => { + const [isInitialized, setIsInitialized] = useState(false); + const filters = useFilters(); + const filterValues = Object.values(filters); + useEffect(() => { + if (isInitialized) { + return; + } + const areFiltersInitialized = filterValues.every(filterValue => + areObjectsEqual( + filterValue?.defaultValue, + dataMaskSelected[filterValue?.id]?.currentState?.value, + ), + ); + if (areFiltersInitialized) { + handleApply(); + setIsInitialized(true); + } + }, [filterValues, dataMaskSelected, isInitialized]); + + return { + isInitialized, + }; +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts index 896aa47a55efd..0ddf07d4c68c2 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts @@ -26,7 +26,3 @@ export interface FilterProps { directPathToChild?: string[]; onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void; } - -export interface CascadeFilter extends Filter { - cascadeChildren: CascadeFilter[]; -} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts index 71681a23b906d..e07dc1290ea76 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts @@ -18,7 +18,6 @@ */ import { Filter } from '../types'; -import { CascadeFilter } from './types'; export function mapParentFiltersToChildren( filters: Filter[], @@ -35,19 +34,3 @@ export function mapParentFiltersToChildren( }); return cascadeChildren; } - -export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { - const cascadeChildren = mapParentFiltersToChildren(filters); - - const getCascadeFilter = (filter: Filter): CascadeFilter => { - const children = cascadeChildren[filter.id] || []; - return { - ...filter, - cascadeChildren: children.map(getCascadeFilter), - }; - }; - - return filters - .filter(filter => !filter.cascadeParentIds?.length) - .map(getCascadeFilter); -} From c93f36dd10b87fe9f6850d4ce014cb53e795f517 Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Sun, 21 Mar 2021 19:23:26 +0200 Subject: [PATCH 7/9] refactor: refactor filter bar --- .../components/DashboardBuilder_spec.jsx | 142 +++---- .../dashboard/components/Dashboard_spec.jsx | 2 +- .../src/dashboard/components/Dashboard.jsx | 2 +- .../dashboard/components/DashboardBuilder.jsx | 371 ------------------ .../DashboardBuilder/DashboardBuilder.tsx | 243 ++++++++++++ .../DashboardBuilder/DashboardContainer.tsx | 106 +++++ .../components/DashboardBuilder/utils.ts | 53 +++ .../components/StickyVerticalBar.tsx | 2 +- .../components/dnd/DragDroppable.jsx | 4 +- .../CascadeFilters/CascadeFilterControl.tsx | 2 +- .../CascadeFilters/CascadePopover.tsx | 2 +- .../nativeFilters/FilterBar/FilterBar.tsx | 88 ++--- .../FilterControls/FilterControl.tsx | 2 +- .../FilterBar/FilterControls/FilterValue.tsx | 2 +- .../FilterBar/FilterControls/state.ts | 1 + .../FilterBar/{ => FilterControls}/types.ts | 2 +- .../nativeFilters/FilterBar/state.ts | 34 ++ .../nativeFilters/FilterBar/utils.ts | 5 + .../dashboard/containers/DashboardBuilder.jsx | 57 --- .../containers/DashboardComponent.jsx | 7 + superset-frontend/src/dashboard/types.ts | 7 +- 21 files changed, 568 insertions(+), 566 deletions(-) delete mode 100644 superset-frontend/src/dashboard/components/DashboardBuilder.jsx create mode 100644 superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx create mode 100644 superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx create mode 100644 superset-frontend/src/dashboard/components/DashboardBuilder/utils.ts rename superset-frontend/src/dashboard/components/nativeFilters/FilterBar/{ => FilterControls}/types.ts (96%) delete mode 100644 superset-frontend/src/dashboard/containers/DashboardBuilder.jsx diff --git a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx index 2b3d04dc61a6d..57c0664846730 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx @@ -18,7 +18,7 @@ */ import { Provider } from 'react-redux'; import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { mount } from 'enzyme'; import sinon from 'sinon'; import fetchMock from 'fetch-mock'; import { ParentSize } from '@vx/responsive'; @@ -27,26 +27,24 @@ import { Sticky, StickyContainer } from 'react-sticky'; import { TabContainer, TabContent, TabPane } from 'react-bootstrap'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; - import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'; -import DashboardBuilder from 'src/dashboard/components/DashboardBuilder'; +import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder'; import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; import DashboardHeader from 'src/dashboard/containers/DashboardHeader'; import DashboardGrid from 'src/dashboard/containers/DashboardGrid'; import * as dashboardStateActions from 'src/dashboard/actions/dashboardState'; - import { dashboardLayout as undoableDashboardLayout, dashboardLayoutWithTabs as undoableDashboardLayoutWithTabs, } from 'spec/fixtures/mockDashboardLayout'; - -import { mockStore, mockStoreWithTabs } from 'spec/fixtures/mockStore'; - -const dashboardLayout = undoableDashboardLayout.present; -const layoutWithTabs = undoableDashboardLayoutWithTabs.present; +import { mockStoreWithTabs, storeWithState } from 'spec/fixtures/mockStore'; +import mockState from 'spec/fixtures/mockState'; +import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants'; fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); +jest.mock('src/dashboard/actions/dashboardState'); + describe('DashboardBuilder', () => { let favStarStub; @@ -61,31 +59,25 @@ describe('DashboardBuilder', () => { favStarStub.restore(); }); - const props = { - dashboardLayout, - deleteTopLevelTabs() {}, - editMode: false, - showBuilderPane() {}, - setColorSchemeAndUnsavedChanges() {}, - colorScheme: undefined, - handleComponentDrop() {}, - setDirectPathToChild: sinon.spy(), - setMountedTab() {}, - }; - - function setup(overrideProps, useProvider = false, store = mockStore) { - const builder = ; - return useProvider - ? mount( - - {builder} - , - { - wrappingComponent: ThemeProvider, - wrappingComponentProps: { theme: supersetTheme }, - }, - ) - : shallow(builder); + function setup(overrideState = {}, overrideStore) { + const store = + overrideStore ?? + storeWithState({ + ...mockState, + dashboardLayout: undoableDashboardLayout, + ...overrideState, + }); + return mount( + + + + + , + { + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, + }, + ); } it('should render a StickyContainer with class "dashboard"', () => { @@ -96,28 +88,28 @@ describe('DashboardBuilder', () => { }); it('should add the "dashboard--editing" class if editMode=true', () => { - const wrapper = setup({ editMode: true }); - const stickyContainer = wrapper.find(StickyContainer); + const wrapper = setup({ dashboardState: { editMode: true } }); + const stickyContainer = wrapper.find(StickyContainer).first(); expect(stickyContainer.prop('className')).toBe( 'dashboard dashboard--editing', ); }); it('should render a DragDroppable DashboardHeader', () => { - const wrapper = setup(null, true); + const wrapper = setup(); expect(wrapper.find(DashboardHeader)).toExist(); }); it('should render a Sticky top-level Tabs if the dashboard has tabs', () => { const wrapper = setup( - { dashboardLayout: layoutWithTabs }, - true, + { dashboardLayout: undoableDashboardLayoutWithTabs }, mockStoreWithTabs, ); const sticky = wrapper.find(Sticky); const dashboardComponent = sticky.find(DashboardComponent); - const tabChildren = layoutWithTabs.TABS_ID.children; + const tabChildren = + undoableDashboardLayoutWithTabs.present.TABS_ID.children; expect(sticky).toHaveLength(1); expect(dashboardComponent).toHaveLength(1 + tabChildren.length); // tab + tabs expect(dashboardComponent.at(0).prop('id')).toBe('TABS_ID'); @@ -127,57 +119,65 @@ describe('DashboardBuilder', () => { }); it('should render a TabContainer and TabContent', () => { - const wrapper = setup({ dashboardLayout: layoutWithTabs }); - const parentSize = wrapper.find(ParentSize).dive(); + const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs }); + const parentSize = wrapper.find(ParentSize); expect(parentSize.find(TabContainer)).toHaveLength(1); expect(parentSize.find(TabContent)).toHaveLength(1); }); it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on TabContainer for perf', () => { - const wrapper = setup({ dashboardLayout: layoutWithTabs }); - const tabProps = wrapper.find(ParentSize).dive().find(TabContainer).props(); + const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs }); + const tabProps = wrapper.find(ParentSize).find(TabContainer).props(); expect(tabProps.animation).toBe(true); expect(tabProps.mountOnEnter).toBe(true); expect(tabProps.unmountOnExit).toBe(false); }); - it('should render a TabPane and DashboardGrid for each Tab', () => { - const wrapper = setup({ dashboardLayout: layoutWithTabs }); - const parentSize = wrapper.find(ParentSize).dive(); - - const expectedCount = layoutWithTabs.TABS_ID.children.length; + it('should render a TabPane and DashboardGrid for first Tab', () => { + const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs }); + const parentSize = wrapper.find(ParentSize); + const expectedCount = + undoableDashboardLayoutWithTabs.present.TABS_ID.children.length; expect(parentSize.find(TabPane)).toHaveLength(expectedCount); - expect(parentSize.find(DashboardGrid)).toHaveLength(expectedCount); + expect(parentSize.find(TabPane).first().find(DashboardGrid)).toHaveLength( + 1, + ); }); - it('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => { - const wrapper = setup(); - expect(wrapper.find(BuilderComponentPane)).not.toExist(); - - wrapper.setProps({ - ...props, - editMode: true, + it('should render a TabPane and DashboardGrid for second Tab', () => { + const wrapper = setup({ + dashboardLayout: undoableDashboardLayoutWithTabs, + dashboardState: { + ...mockState, + directPathToChild: [DASHBOARD_ROOT_ID, 'TABS_ID', 'TAB_ID2'], + }, }); - expect(wrapper.find(BuilderComponentPane)).toExist(); + const parentSize = wrapper.find(ParentSize); + const expectedCount = + undoableDashboardLayoutWithTabs.present.TABS_ID.children.length; + expect(parentSize.find(TabPane)).toHaveLength(expectedCount); + expect(parentSize.find(TabPane).at(1).find(DashboardGrid)).toHaveLength(1); }); - it('should render a BuilderComponentPane if editMode=true and user selects "Colors" pane', () => { + it('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => { const wrapper = setup(); expect(wrapper.find(BuilderComponentPane)).not.toExist(); + }); - wrapper.setProps({ - ...props, - editMode: true, - }); + it('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => { + const wrapper = setup({ dashboardState: { editMode: true } }); expect(wrapper.find(BuilderComponentPane)).toExist(); }); it('should change redux state if a top-level Tab is clicked', () => { - const wrapper = setup( - { dashboardLayout: layoutWithTabs }, - true, - mockStoreWithTabs, - ); + dashboardStateActions.setDirectPathToChild = jest.fn(arg0 => ({ + type: 'type', + arg0, + })); + const wrapper = setup({ + ...mockStoreWithTabs, + dashboardLayout: undoableDashboardLayoutWithTabs, + }); expect(wrapper.find(TabContainer).prop('activeKey')).toBe(0); @@ -186,6 +186,10 @@ describe('DashboardBuilder', () => { .at(1) .simulate('click'); - expect(props.setDirectPathToChild.callCount).toBe(1); + expect(dashboardStateActions.setDirectPathToChild).toHaveBeenCalledWith([ + 'ROOT_ID', + 'TABS_ID', + 'TAB_ID2', + ]); }); }); diff --git a/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx index c9c13b1a02686..61730c5e9e8e1 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/Dashboard_spec.jsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import sinon from 'sinon'; import Dashboard from 'src/dashboard/components/Dashboard'; -import DashboardBuilder from 'src/dashboard/containers/DashboardBuilder'; +import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder'; import { CHART_TYPE } from 'src/dashboard/util/componentTypes'; import newComponentFactory from 'src/dashboard/util/newComponentFactory'; diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx b/superset-frontend/src/dashboard/components/Dashboard.jsx index a373d33e60f78..b504a8d20fe35 100644 --- a/superset-frontend/src/dashboard/components/Dashboard.jsx +++ b/superset-frontend/src/dashboard/components/Dashboard.jsx @@ -24,7 +24,7 @@ import { PluginContext } from 'src/components/DynamicPlugins'; import Loading from 'src/components/Loading'; import getChartIdsFromLayout from '../util/getChartIdsFromLayout'; import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId'; -import DashboardBuilder from '../containers/DashboardBuilder'; +import DashboardBuilder from './DashboardBuilder/DashboardBuilder'; import { chartPropShape, slicePropShape, diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx deleted file mode 100644 index 308ecfa8eee11..0000000000000 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ /dev/null @@ -1,371 +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. - */ -/* eslint-env browser */ -import cx from 'classnames'; -// ParentSize uses resize observer so the dashboard will update size -// when its container size changes, due to e.g., builder side panel opening -import { ParentSize } from '@vx/responsive'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { Sticky, StickyContainer } from 'react-sticky'; -import { TabContainer, TabContent, TabPane } from 'react-bootstrap'; -import { styled } from '@superset-ui/core'; - -import ErrorBoundary from 'src/components/ErrorBoundary'; -import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'; -import DashboardHeader from 'src/dashboard/containers/DashboardHeader'; -import DashboardGrid from 'src/dashboard/containers/DashboardGrid'; -import IconButton from 'src/dashboard/components/IconButton'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; -import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; -import ToastPresenter from 'src/messageToasts/containers/ToastPresenter'; -import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; - -import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId'; - -import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex'; -import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath'; -import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; -import { URL_PARAMS } from 'src/constants'; -import { - DASHBOARD_GRID_ID, - DASHBOARD_ROOT_ID, - DASHBOARD_ROOT_DEPTH, - DashboardStandaloneMode, -} from '../util/constants'; -import FilterBar from './nativeFilters/FilterBar/FilterBar'; -import { StickyVerticalBar } from './StickyVerticalBar'; -import { getUrlParam } from '../../utils/urlUtils'; - -const TABS_HEIGHT = 47; -const HEADER_HEIGHT = 67; - -const propTypes = { - // redux - dashboardLayout: PropTypes.object.isRequired, - deleteTopLevelTabs: PropTypes.func.isRequired, - editMode: PropTypes.bool.isRequired, - showBuilderPane: PropTypes.func, - colorScheme: PropTypes.string, - setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired, - handleComponentDrop: PropTypes.func.isRequired, - directPathToChild: PropTypes.arrayOf(PropTypes.string), - focusedFilterField: PropTypes.object, - setDirectPathToChild: PropTypes.func.isRequired, - setMountedTab: PropTypes.func.isRequired, -}; - -const defaultProps = { - showBuilderPane: false, - directPathToChild: [], - colorScheme: undefined, -}; - -const StyledDashboardContent = styled.div` - display: flex; - flex-direction: row; - flex-wrap: nowrap; - height: auto; - flex-grow: 1; - - .grid-container .dashboard-component-tabs { - box-shadow: none; - padding-left: 0; - } - - .grid-container { - /* without this, the grid will not get smaller upon toggling the builder panel on */ - min-width: 0; - width: 100%; - flex-grow: 1; - position: relative; - margin: ${({ theme }) => theme.gridUnit * 6}px - ${({ theme }) => theme.gridUnit * 8}px - ${({ theme }) => theme.gridUnit * 6}px - ${({ theme, dashboardFiltersOpen }) => { - if (dashboardFiltersOpen) return theme.gridUnit * 8; - return 0; - }}px; - } - - .dashboard-component-chart-holder { - // transitionable traits to show filter relevance - transition: opacity ${({ theme }) => theme.transitionTiming}s, - border-color ${({ theme }) => theme.transitionTiming}s, - box-shadow ${({ theme }) => theme.transitionTiming}s; - border: 0px solid transparent; - } -`; - -class DashboardBuilder extends React.Component { - static shouldFocusTabs(event, container) { - // don't focus the tabs when we click on a tab - return ( - event.target.className === 'ant-tabs-nav-wrap' || - (/icon-button/.test(event.target.className) && - container.contains(event.target)) - ); - } - - static getRootLevelTabIndex(dashboardLayout, directPathToChild) { - return Math.max( - 0, - findTabIndexByComponentId({ - currentComponent: DashboardBuilder.getRootLevelTabsComponent( - dashboardLayout, - ), - directPathToChild, - }), - ); - } - - static getRootLevelTabsComponent(dashboardLayout) { - const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; - const rootChildId = dashboardRoot.children[0]; - return rootChildId === DASHBOARD_GRID_ID - ? dashboardLayout[DASHBOARD_ROOT_ID] - : dashboardLayout[rootChildId]; - } - - constructor(props) { - super(props); - - const { dashboardLayout, directPathToChild } = props; - const tabIndex = DashboardBuilder.getRootLevelTabIndex( - dashboardLayout, - directPathToChild, - ); - this.state = { - tabIndex, - dashboardFiltersOpen: true, - }; - - this.handleChangeTab = this.handleChangeTab.bind(this); - this.handleDeleteTopLevelTabs = this.handleDeleteTopLevelTabs.bind(this); - this.toggleDashboardFiltersOpen = this.toggleDashboardFiltersOpen.bind( - this, - ); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - const nextFocusComponent = getLeafComponentIdFromPath( - nextProps.directPathToChild, - ); - const currentFocusComponent = getLeafComponentIdFromPath( - this.props.directPathToChild, - ); - if (nextFocusComponent !== currentFocusComponent) { - const { dashboardLayout, directPathToChild } = nextProps; - const nextTabIndex = DashboardBuilder.getRootLevelTabIndex( - dashboardLayout, - directPathToChild, - ); - - this.setState(() => ({ tabIndex: nextTabIndex })); - } - } - - toggleDashboardFiltersOpen(visible) { - if (visible === undefined) { - this.setState(state => ({ - ...state, - dashboardFiltersOpen: !state.dashboardFiltersOpen, - })); - } else { - this.setState(state => ({ - ...state, - dashboardFiltersOpen: visible, - })); - } - } - - handleChangeTab({ pathToTabIndex }) { - this.props.setDirectPathToChild(pathToTabIndex); - } - - handleDeleteTopLevelTabs() { - this.props.deleteTopLevelTabs(); - - const { dashboardLayout } = this.props; - const firstTab = getDirectPathToTabIndex( - DashboardBuilder.getRootLevelTabsComponent(dashboardLayout), - 0, - ); - this.props.setDirectPathToChild(firstTab); - } - - render() { - const { - handleComponentDrop, - dashboardLayout, - editMode, - showBuilderPane, - setColorSchemeAndUnsavedChanges, - colorScheme, - directPathToChild, - } = this.props; - const { tabIndex } = this.state; - const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; - const rootChildId = dashboardRoot.children[0]; - const topLevelTabs = - rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId]; - - const childIds = topLevelTabs ? topLevelTabs.children : [DASHBOARD_GRID_ID]; - - const hideDashboardHeader = - getUrlParam(URL_PARAMS.standalone, 'number') === - DashboardStandaloneMode.HIDE_NAV_AND_TITLE; - - const barTopOffset = - (hideDashboardHeader ? 0 : HEADER_HEIGHT) + - (topLevelTabs ? TABS_HEIGHT : 0); - - return ( - - - {({ style }) => ( - - {({ dropIndicatorProps }) => ( -
- {!hideDashboardHeader && } - {dropIndicatorProps &&
} - {topLevelTabs && ( - , - ]} - editMode={editMode} - > - - - )} -
- )} - - )} - - - {isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && !editMode && ( - - - - - - )} -
- - {({ width }) => ( - /* - We use a TabContainer irrespective of whether top-level tabs exist to maintain - a consistent React component tree. This avoids expensive mounts/unmounts of - the entire dashboard upon adding/removing top-level tabs, which would otherwise - happen because of React's diffing algorithm - */ - - - {childIds.map((id, index) => ( - // Matching the key of the first TabPane irrespective of topLevelTabs - // lets us keep the same React component tree when !!topLevelTabs changes. - // This avoids expensive mounts/unmounts of the entire dashboard. - - - - ))} - - - )} - -
- {editMode && ( - - )} -
- - - ); - } -} - -DashboardBuilder.propTypes = propTypes; -DashboardBuilder.defaultProps = defaultProps; -DashboardBuilder.childContextTypes = { - dragDropManager: PropTypes.object.isRequired, -}; - -export default DashboardBuilder; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx new file mode 100644 index 0000000000000..17a2362d07f14 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -0,0 +1,243 @@ +/** + * 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. + */ +/* eslint-env browser */ +import cx from 'classnames'; +import React, { FC, SyntheticEvent, useEffect, useState } from 'react'; +import { Sticky, StickyContainer } from 'react-sticky'; +import { TabContainer } from 'react-bootstrap'; +import { JsonObject, styled } from '@superset-ui/core'; + +import ErrorBoundary from 'src/components/ErrorBoundary'; +import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'; +import DashboardHeader from 'src/dashboard/containers/DashboardHeader'; +import IconButton from 'src/dashboard/components/IconButton'; +import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; +import ToastPresenter from 'src/messageToasts/containers/ToastPresenter'; +import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; + +import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex'; +import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; +import { URL_PARAMS } from 'src/constants'; +import { useDispatch, useSelector } from 'react-redux'; +import { getUrlParam } from 'src/utils/urlUtils'; +import { DashboardLayout, RootState } from 'src/dashboard/types'; +import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState'; +import { + deleteTopLevelTabs, + handleComponentDrop, +} from 'src/dashboard/actions/dashboardLayout'; +import { + DASHBOARD_GRID_ID, + DASHBOARD_ROOT_ID, + DASHBOARD_ROOT_DEPTH, + DashboardStandaloneMode, +} from 'src/dashboard/util/constants'; +import FilterBar from '../nativeFilters/FilterBar/FilterBar'; +import { StickyVerticalBar } from '../StickyVerticalBar'; +import { shouldFocusTabs, getRootLevelTabsComponent } from './utils'; +import { useFilters } from '../nativeFilters/FilterBar/state'; +import { Filter } from '../nativeFilters/types'; +import DashboardContainer from './DashboardContainer'; + +const TABS_HEIGHT = 47; +const HEADER_HEIGHT = 67; + +type DashboardBuilderProps = {}; + +const StyledDashboardContent = styled.div<{ dashboardFiltersOpen: boolean }>` + display: flex; + flex-direction: row; + flex-wrap: nowrap; + height: auto; + flex-grow: 1; + + .grid-container .dashboard-component-tabs { + box-shadow: none; + padding-left: 0; + } + + .grid-container { + /* without this, the grid will not get smaller upon toggling the builder panel on */ + min-width: 0; + width: 100%; + flex-grow: 1; + position: relative; + margin: ${({ theme }) => theme.gridUnit * 6}px + ${({ theme }) => theme.gridUnit * 8}px + ${({ theme }) => theme.gridUnit * 6}px + ${({ theme, dashboardFiltersOpen }) => { + if (dashboardFiltersOpen) return theme.gridUnit * 8; + return 0; + }}px; + } + + .dashboard-component-chart-holder { + // transitionable traits to show filter relevance + transition: opacity ${({ theme }) => theme.transitionTiming}s, + border-color ${({ theme }) => theme.transitionTiming}s, + box-shadow ${({ theme }) => theme.transitionTiming}s; + border: 0 solid transparent; + } +`; + +const DashboardBuilder: FC = () => { + const dispatch = useDispatch(); + const dashboardLayout = useSelector( + state => state.dashboardLayout.present, + ); + const editMode = useSelector( + state => state.dashboardState.editMode, + ); + const directPathToChild = useSelector( + state => state.dashboardState.directPathToChild, + ); + + const filters = useFilters(); + const filterValues = Object.values(filters); + + const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(true); + + const toggleDashboardFiltersOpen = (visible?: boolean) => { + setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen); + }; + + const handleChangeTab = ({ + pathToTabIndex, + }: SyntheticEvent & { pathToTabIndex: string[] }) => { + dispatch(setDirectPathToChild(pathToTabIndex)); + }; + + const handleDeleteTopLevelTabs = () => { + dispatch(deleteTopLevelTabs()); + + const firstTab = getDirectPathToTabIndex( + getRootLevelTabsComponent(dashboardLayout), + 0, + ); + dispatch(setDirectPathToChild(firstTab)); + }; + + const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; + const rootChildId = dashboardRoot.children[0]; + const topLevelTabs = + rootChildId !== DASHBOARD_GRID_ID + ? dashboardLayout[rootChildId] + : undefined; + + const hideDashboardHeader = + getUrlParam(URL_PARAMS.standalone, 'number') === + DashboardStandaloneMode.HIDE_NAV_AND_TITLE; + + const barTopOffset = + (hideDashboardHeader ? 0 : HEADER_HEIGHT) + + (topLevelTabs ? TABS_HEIGHT : 0); + + useEffect(() => { + if (filterValues.length === 0 && dashboardFiltersOpen) { + toggleDashboardFiltersOpen(false); + } + }, [filterValues.length]); + + return ( + + + {({ style }) => ( + // @ts-ignore + dispatch(handleComponentDrop)} + editMode={editMode} + // you cannot drop on/displace tabs if they already exist + disableDragdrop={!!topLevelTabs} + style={{ + zIndex: 100, + ...style, + }} + > + {({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => ( +
+ {!hideDashboardHeader && } + {dropIndicatorProps &&
} + {topLevelTabs && ( + , + ]} + editMode={editMode} + > + {/* + // @ts-ignore */} + + + )} +
+ )} + + )} + + + {isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && !editMode && ( + + + + + + )} + + {editMode && } + + + + ); +}; + +export default DashboardBuilder; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx new file mode 100644 index 0000000000000..cd012093709ce --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx @@ -0,0 +1,106 @@ +/** + * 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. + */ +// ParentSize uses resize observer so the dashboard will update size +// when its container size changes, due to e.g., builder side panel opening +import { ParentSize } from '@vx/responsive'; +import { TabContainer, TabContent, TabPane } from 'react-bootstrap'; +import React, { FC, SyntheticEvent, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import DashboardGrid from 'src/dashboard/containers/DashboardGrid'; +import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath'; +import { DashboardLayout, LayoutItem, RootState } from 'src/dashboard/types'; +import { + DASHBOARD_GRID_ID, + DASHBOARD_ROOT_DEPTH, +} from 'src/dashboard/util/constants'; +import { getRootLevelTabIndex } from './utils'; + +type DashboardContainerProps = { + topLevelTabs?: LayoutItem; + handleChangeTab: (event: SyntheticEvent) => void; +}; + +const DashboardContainer: FC = ({ + topLevelTabs, + handleChangeTab, +}) => { + const dashboardLayout = useSelector( + state => state.dashboardLayout.present, + ); + const directPathToChild = useSelector( + state => state.dashboardState.directPathToChild, + ); + const [tabIndex, setTabIndex] = useState( + getRootLevelTabIndex(dashboardLayout, directPathToChild), + ); + + useEffect(() => { + setTabIndex(getRootLevelTabIndex(dashboardLayout, directPathToChild)); + }, [getLeafComponentIdFromPath(directPathToChild)]); + + const childIds: string[] = topLevelTabs + ? topLevelTabs.children + : [DASHBOARD_GRID_ID]; + + return ( +
+ + {({ width }) => ( + /* + We use a TabContainer irrespective of whether top-level tabs exist to maintain + a consistent React component tree. This avoids expensive mounts/unmounts of + the entire dashboard upon adding/removing top-level tabs, which would otherwise + happen because of React's diffing algorithm + */ + + + {childIds.map((id, index) => ( + // Matching the key of the first TabPane irrespective of topLevelTabs + // lets us keep the same React component tree when !!topLevelTabs changes. + // This avoids expensive mounts/unmounts of the entire dashboard. + + + + ))} + + + )} + +
+ ); +}; + +export default DashboardContainer; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/utils.ts b/superset-frontend/src/dashboard/components/DashboardBuilder/utils.ts new file mode 100644 index 0000000000000..999adf62a7c87 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/utils.ts @@ -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 { + DASHBOARD_GRID_ID, + DASHBOARD_ROOT_ID, +} from 'src/dashboard/util/constants'; +import { DashboardLayout } from 'src/dashboard/types'; +import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId'; + +export const getRootLevelTabsComponent = (dashboardLayout: DashboardLayout) => { + const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; + const rootChildId = dashboardRoot.children[0]; + return rootChildId === DASHBOARD_GRID_ID + ? dashboardLayout[DASHBOARD_ROOT_ID] + : dashboardLayout[rootChildId]; +}; + +export const shouldFocusTabs = ( + event: { target: { className: string } }, + container: { contains: (arg0: any) => any }, +) => + // don't focus the tabs when we click on a tab + event.target.className === 'ant-tabs-nav-wrap' || + (/icon-button/.test(event.target.className) && + container.contains(event.target)); + +export const getRootLevelTabIndex = ( + dashboardLayout: DashboardLayout, + directPathToChild: string[], +): number => + Math.max( + 0, + findTabIndexByComponentId({ + currentComponent: getRootLevelTabsComponent(dashboardLayout), + directPathToChild, + }), + ); diff --git a/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx b/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx index afb5a71a2eda8..80e76282e3663 100644 --- a/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx +++ b/superset-frontend/src/dashboard/components/StickyVerticalBar.tsx @@ -50,7 +50,7 @@ const Contents = styled.div` export interface SVBProps { topOffset: number; - width: number; + width?: number; filtersOpen: boolean; } diff --git a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx index 55991462aa7e1..36be1833e6183 100644 --- a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx +++ b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx @@ -37,7 +37,7 @@ const propTypes = { component: componentShape.isRequired, parentComponent: componentShape, depth: PropTypes.number.isRequired, - disableDragDrop: PropTypes.bool, + disableDragdrop: PropTypes.bool, orientation: PropTypes.oneOf(['row', 'column']), index: PropTypes.number.isRequired, style: PropTypes.object, @@ -58,7 +58,7 @@ const defaultProps = { className: null, style: null, parentComponent: null, - disableDragDrop: false, + disableDragdrop: false, children() {}, onDrop() {}, orientation: 'row', diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx index 63ed30f6b7f6f..3b5ce92b67c13 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { styled, DataMask } from '@superset-ui/core'; import Icon from 'src/components/Icon'; -import FilterControl from '../FilterControl/FilterControl'; +import FilterControl from '../FilterControls/FilterControl'; import { Filter } from '../../types'; import { CascadeFilter } from './types'; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx index 5968efa573a41..5792ece91cc3a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadePopover.tsx @@ -24,7 +24,7 @@ import { Pill } from 'src/dashboard/components/FiltersBadge/Styles'; import { useSelector } from 'react-redux'; import { getInitialMask } from 'src/dataMask/reducer'; import { MaskWithId } from 'src/dataMask/types'; -import FilterControl from '../FilterControl/FilterControl'; +import FilterControl from '../FilterControls/FilterControl'; import CascadeFilterControl from './CascadeFilterControl'; import { CascadeFilter } from './types'; import { Filter } from '../../types'; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index 6a4f4573adb13..ef30ab80737e9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -19,28 +19,29 @@ /* eslint-disable no-param-reassign */ import { HandlerFunction, styled, t } from '@superset-ui/core'; -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import cx from 'classnames'; import Icon from 'src/components/Icon'; import { Tabs } from 'src/common/components'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { updateDataMask } from 'src/dataMask/actions'; -import { DataMaskUnit, DataMaskState } from 'src/dataMask/types'; +import { DataMaskState, DataMaskUnit } from 'src/dataMask/types'; import { useImmer } from 'use-immer'; import { areObjectsEqual } from 'src/reduxUtils'; import { Filter } from '../types'; -import { mapParentFiltersToChildren } from './utils'; +import { mapParentFiltersToChildren, TabIds } from './utils'; import FilterSets from './FilterSets/FilterSets'; import { useDataMask, useFilters, useFilterSets, useFiltersInitialisation, + useFilterUpdates, } from './state'; import EditSection from './FilterSets/EditSection'; import Header from './Header'; -import FilterControls from './FilterControl/FilterControls'; +import FilterControls from './FilterControls/FilterControls'; const barWidth = `250px`; @@ -143,11 +144,6 @@ interface FiltersBarProps { directPathToChild?: string[]; } -enum TabIds { - AllFilters = 'allFilters', - FilterSets = 'filterSets', -} - const FilterBar: React.FC = ({ filtersOpen, toggleFiltersBar, @@ -162,11 +158,34 @@ const FilterBar: React.FC = ({ const dispatch = useDispatch(); const filterSets = useFilterSets(); const filterSetFilterValues = Object.values(filterSets); - const [isFilterSetChanged, setIsFilterSetChanged] = useState(false); const [tab, setTab] = useState(TabIds.AllFilters); const filters = useFilters(); const filterValues = Object.values(filters); const dataMaskApplied = useDataMask(); + const [isFilterSetChanged, setIsFilterSetChanged] = useState(false); + + const cascadeChildren = useMemo( + () => mapParentFiltersToChildren(filterValues), + [filterValues], + ); + + const handleFilterSelectionChange = ( + filter: Pick & Partial, + dataMask: Partial, + ) => { + setIsFilterSetChanged(tab !== TabIds.AllFilters); + setDataMaskSelected(draft => { + const children = cascadeChildren[filter.id] || []; + // force instant updating on initialization or for parent filters + if (filter.isInstant || children.length > 0) { + dispatch(updateDataMask(filter.id, dataMask)); + } + + if (dataMask.nativeFilters) { + draft[filter.id] = dataMask.nativeFilters; + } + }); + }; const handleApply = () => { const filterIds = Object.keys(dataMaskSelected); @@ -187,59 +206,12 @@ const FilterBar: React.FC = ({ handleApply, ); - useEffect(() => { - if (filterValues.length === 0 && filtersOpen) { - toggleFiltersBar(false); - } - }, [filterValues.length]); - - useEffect(() => { - // Remove deleted filters from local state - Object.keys(dataMaskSelected).forEach(selectedId => { - if (!filters[selectedId]) { - setDataMaskSelected(draft => { - delete draft[selectedId]; - }); - } - }); - Object.keys(dataMaskApplied).forEach(appliedId => { - if (!filters[appliedId]) { - setLastAppliedFilterData(draft => { - delete draft[appliedId]; - }); - } - }); - }, [ - dataMaskApplied, + useFilterUpdates( dataMaskSelected, - filters, setDataMaskSelected, setLastAppliedFilterData, - ]); - - const cascadeChildren = useMemo( - () => mapParentFiltersToChildren(filterValues), - [filterValues], ); - const handleFilterSelectionChange = ( - filter: Pick & Partial, - dataMask: Partial, - ) => { - setIsFilterSetChanged(tab !== TabIds.AllFilters); - setDataMaskSelected(draft => { - const children = cascadeChildren[filter.id] || []; - // force instant updating on initialization or for parent filters - if (filter.isInstant || children.length > 0) { - dispatch(updateDataMask(filter.id, dataMask)); - } - - if (dataMask.nativeFilters) { - draft[filter.id] = dataMask.nativeFilters; - } - }); - }; - const isApplyDisabled = !isInitialized || areObjectsEqual(dataMaskSelected, lastAppliedFilterData); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx index be3e9c2c431eb..157f50e09d449 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { styled } from '@superset-ui/core'; import FilterValue from './FilterValue'; -import { FilterProps } from '../types'; +import { FilterProps } from './types'; const StyledFilterControlTitle = styled.h4` width: 100%; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx index 75da0dc2e14ec..6e7a4eaca5122 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx @@ -29,7 +29,7 @@ import { areObjectsEqual } from 'src/reduxUtils'; import { getChartDataRequest } from 'src/chart/chartAction'; import Loading from 'src/components/Loading'; import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert'; -import { FilterProps } from '../types'; +import { FilterProps } from './types'; import { getFormData } from '../../utils'; import { useCascadingFilters } from './state'; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts index 72e29c398ca94..0a60c2c02239f 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts @@ -21,6 +21,7 @@ import { NativeFiltersState } from 'src/dashboard/reducers/types'; import { mergeExtraFormData } from '../../utils'; import { useDataMask } from '../state'; +// eslint-disable-next-line import/prefer-default-export export function useCascadingFilters(id: string) { const { filters } = useSelector( state => state.nativeFilters, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts similarity index 96% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts index 0ddf07d4c68c2..67e50d562f9aa 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts @@ -18,7 +18,7 @@ */ import React from 'react'; import { DataMask } from '@superset-ui/core'; -import { Filter } from '../types'; +import { Filter } from '../../types'; export interface FilterProps { filter: Filter; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts index 6b38b1f31daa0..6f3d10263a01b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +/* eslint-disable no-param-reassign */ import { useSelector } from 'react-redux'; import { Filters, @@ -64,3 +65,36 @@ export const useFiltersInitialisation = ( isInitialized, }; }; + +export const useFilterUpdates = ( + dataMaskSelected: DataMaskUnit, + setDataMaskSelected: (arg0: (arg0: DataMaskUnit) => void) => void, + setLastAppliedFilterData: (arg0: (arg0: DataMaskUnit) => void) => void, +) => { + const filters = useFilters(); + const dataMaskApplied = useDataMask(); + + useEffect(() => { + // Remove deleted filters from local state + Object.keys(dataMaskSelected).forEach(selectedId => { + if (!filters[selectedId]) { + setDataMaskSelected(draft => { + delete draft[selectedId]; + }); + } + }); + Object.keys(dataMaskApplied).forEach(appliedId => { + if (!filters[appliedId]) { + setLastAppliedFilterData(draft => { + delete draft[appliedId]; + }); + } + }); + }, [ + dataMaskApplied, + dataMaskSelected, + filters, + setDataMaskSelected, + setLastAppliedFilterData, + ]); +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts index e07dc1290ea76..dc7097a508de3 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts @@ -19,6 +19,11 @@ import { Filter } from '../types'; +export enum TabIds { + AllFilters = 'allFilters', + FilterSets = 'filterSets', +} + export function mapParentFiltersToChildren( filters: Filter[], ): { [id: string]: Filter[] } { diff --git a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx b/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx deleted file mode 100644 index dba43d18941cd..0000000000000 --- a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx +++ /dev/null @@ -1,57 +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 { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import DashboardBuilder from '../components/DashboardBuilder'; - -import { - setColorSchemeAndUnsavedChanges, - showBuilderPane, - setDirectPathToChild, -} from '../actions/dashboardState'; -import { - deleteTopLevelTabs, - handleComponentDrop, -} from '../actions/dashboardLayout'; - -function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) { - return { - dashboardLayout: undoableLayout.present, - editMode: dashboardState.editMode, - showBuilderPane: dashboardState.showBuilderPane, - directPathToChild: dashboardState.directPathToChild, - colorScheme: dashboardState.colorScheme, - focusedFilterField: dashboardState.focusedFilterField, - }; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators( - { - deleteTopLevelTabs, - handleComponentDrop, - showBuilderPane, - setColorSchemeAndUnsavedChanges, - setDirectPathToChild, - }, - dispatch, - ); -} - -export default connect(mapStateToProps, mapDispatchToProps)(DashboardBuilder); diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx index e8331b270303b..4d5e14bc035f1 100644 --- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx @@ -38,6 +38,13 @@ import { import { setDirectPathToChild } from '../actions/dashboardState'; const propTypes = { + id: PropTypes.string, + parentId: PropTypes.string, + depth: PropTypes.number, + index: PropTypes.number, + renderHoverMenu: PropTypes.bool, + renderTabContent: PropTypes.bool, + onChangeTab: PropTypes.func, component: componentShape.isRequired, parentComponent: componentShape.isRequired, createComponent: PropTypes.func.isRequired, diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 696eda40bc4a6..e74fb43c2b58e 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -40,11 +40,16 @@ export type Chart = { }; }; +export type DashboardLayout = { [key: string]: LayoutItem }; +export type DashboardLayoutState = { present: DashboardLayout }; +export type DashboardState = { editMode: boolean; directPathToChild: string[] }; + /** Root state of redux */ export type RootState = { charts: { [key: string]: Chart }; - dashboardLayout: { present: { [key: string]: LayoutItem } }; + dashboardLayout: DashboardLayoutState; dashboardFilters: {}; + dashboardState: DashboardState; dataMask: DataMaskStateWithId; }; From 32328bac7e8e133da09d7b20d851e4b799bf3844 Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Sun, 21 Mar 2021 19:27:45 +0200 Subject: [PATCH 8/9] chore: add lic --- .../FilterControls/FilterControls.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx index 470e41f4a1340..424e587217509 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -1,3 +1,21 @@ +/** + * 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, useMemo, useState } from 'react'; import { DataMaskUnit } from 'src/dataMask/types'; import { DataMask, styled } from '@superset-ui/core'; From cd68df959513f2c929c391e33359ba580f5feec2 Mon Sep 17 00:00:00 2001 From: Simcha Shats Date: Sun, 21 Mar 2021 20:12:04 +0200 Subject: [PATCH 9/9] lint: fix lint --- .../components/nativeFilters/FilterBar/FilterControls/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts index e50ffb0b92b13..19b50ddf664de 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts @@ -20,6 +20,7 @@ import { Filter } from '../../types'; import { CascadeFilter } from '../CascadeFilters/types'; import { mapParentFiltersToChildren } from '../utils'; +// eslint-disable-next-line import/prefer-default-export export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { const cascadeChildren = mapParentFiltersToChildren(filters);