This is an automated email from the ASF dual-hosted git repository. diegopucci pushed a commit to branch feat/horizontal-filterbar-states in repository https://gitbox.apache.org/repos/asf/superset.git
commit 509dc08ddc795924a2f5db9c9e474b23c36b1514 Author: geido <[email protected]> AuthorDate: Fri Nov 4 20:47:18 2022 +0200 Separate components --- .../components/DropdownSelectableIcon/index.tsx | 1 + .../DashboardBuilder/DashboardBuilder.tsx | 95 ++-- .../FilterBar/ActionButtons/ActionButtons.test.tsx | 2 +- .../FilterBar/ActionButtons/index.tsx | 96 ++-- .../nativeFilters/FilterBar/FilterBar.test.tsx | 18 +- .../FilterBar/FilterBarLocationSelect/index.tsx | 6 +- .../FilterBar/FilterConfigurationLink/index.tsx | 2 +- .../FilterBar/FilterControls/FilterControl.tsx | 2 +- .../FilterBar/FilterSets/EditSection.tsx | 2 +- .../FilterBar/FilterSets/FilterSetUnit.tsx | 2 +- .../FilterBar/FilterSets/FiltersHeader.tsx | 2 +- .../nativeFilters/FilterBar/FilterSets/Footer.tsx | 2 +- .../nativeFilters/FilterBar/FilterSets/index.tsx | 2 +- .../nativeFilters/FilterBar/Header/index.tsx | 8 +- .../FilterBar/{index.tsx => Horizontal.tsx} | 342 ++++---------- .../FilterBar/{index.tsx => Vertical.tsx} | 27 +- .../components/nativeFilters/FilterBar/index.tsx | 501 +-------------------- .../components/nativeFilters/FilterBar/utils.ts | 4 + 18 files changed, 245 insertions(+), 869 deletions(-) diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx index 228c6889a2..b6c5d89a2e 100644 --- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx +++ b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx @@ -43,6 +43,7 @@ const StyledDropdownButton = styled( height: unset; padding: 0; border: none; + width: auto !important; .anticon { line-height: 0; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index cd0cfdb3fc..d97410685b 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -40,7 +40,11 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex'; import { URL_PARAMS } from 'src/constants'; import { getUrlParam } from 'src/utils/urlUtils'; -import { DashboardLayout, RootState } from 'src/dashboard/types'; +import { + DashboardLayout, + FilterBarLocation, + RootState, +} from 'src/dashboard/types'; import { setDirectPathToChild, setEditMode, @@ -57,7 +61,10 @@ import { DASHBOARD_ROOT_DEPTH, DashboardStandaloneMode, } from 'src/dashboard/util/constants'; -import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar'; +import { + VerticalFilterBar, + HorizontalFilterBar, +} from 'src/dashboard/components/nativeFilters/FilterBar'; import Loading from 'src/components/Loading'; import { EmptyStateBig } from 'src/components/EmptyState'; import { useUiConfig } from 'src/components/UiConfigContext'; @@ -241,6 +248,9 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => { const fullSizeChartId = useSelector<RootState, number | null>( state => state.dashboardState.fullSizeChartId, ); + const filterBarLocation = useSelector<RootState, FilterBarLocation>( + ({ dashboardInfo }) => dashboardInfo.filterBarLocation, + ); const handleChangeTab = useCallback( ({ pathToTabIndex }: { pathToTabIndex: string[] }) => { @@ -354,6 +364,11 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => { ({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => ( <div> {!hideDashboardHeader && <DashboardHeader />} + {nativeFiltersEnabled && + !editMode && + filterBarLocation === FilterBarLocation.HORIZONTAL && ( + <HorizontalFilterBar directPathToChild={directPathToChild} /> + )} {dropIndicatorProps && <div {...dropIndicatorProps} />} {!isReport && topLevelTabs && !uiConfig.hideNav && ( <WithPopoverMenu @@ -382,6 +397,8 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => { </div> ), [ + nativeFiltersEnabled, + filterBarLocation, editMode, handleChangeTab, handleDeleteTopLevelTabs, @@ -394,42 +411,44 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => { return ( <StyledDiv> - {nativeFiltersEnabled && !editMode && ( - <> - <ResizableSidebar - id={`dashboard:${dashboardId}`} - enable={dashboardFiltersOpen} - minWidth={OPEN_FILTER_BAR_WIDTH} - maxWidth={OPEN_FILTER_BAR_MAX_WIDTH} - initialWidth={OPEN_FILTER_BAR_WIDTH} - > - {adjustedWidth => { - const filterBarWidth = dashboardFiltersOpen - ? adjustedWidth - : CLOSED_FILTER_BAR_WIDTH; - return ( - <FiltersPanel - width={filterBarWidth} - data-test="dashboard-filters-panel" - > - <StickyPanel ref={containerRef} width={filterBarWidth}> - <ErrorBoundary> - <FilterBar - filtersOpen={dashboardFiltersOpen} - toggleFiltersBar={toggleDashboardFiltersOpen} - directPathToChild={directPathToChild} - width={filterBarWidth} - height={filterBarHeight} - offset={filterBarOffset} - /> - </ErrorBoundary> - </StickyPanel> - </FiltersPanel> - ); - }} - </ResizableSidebar> - </> - )} + {nativeFiltersEnabled && + !editMode && + filterBarLocation === FilterBarLocation.VERTICAL && ( + <> + <ResizableSidebar + id={`dashboard:${dashboardId}`} + enable={dashboardFiltersOpen} + minWidth={OPEN_FILTER_BAR_WIDTH} + maxWidth={OPEN_FILTER_BAR_MAX_WIDTH} + initialWidth={OPEN_FILTER_BAR_WIDTH} + > + {adjustedWidth => { + const filterBarWidth = dashboardFiltersOpen + ? adjustedWidth + : CLOSED_FILTER_BAR_WIDTH; + return ( + <FiltersPanel + width={filterBarWidth} + data-test="dashboard-filters-panel" + > + <StickyPanel ref={containerRef} width={filterBarWidth}> + <ErrorBoundary> + <VerticalFilterBar + filtersOpen={dashboardFiltersOpen} + toggleFiltersBar={toggleDashboardFiltersOpen} + directPathToChild={directPathToChild} + width={filterBarWidth} + height={filterBarHeight} + offset={filterBarOffset} + /> + </ErrorBoundary> + </StickyPanel> + </FiltersPanel> + ); + }} + </ResizableSidebar> + </> + )} <StyledHeader ref={headerRef}> {/* @ts-ignore */} <DragDroppable diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/ActionButtons.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/ActionButtons.test.tsx index 3c3f838c4a..525f519632 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/ActionButtons.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/ActionButtons.test.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants'; import userEvent from '@testing-library/user-event'; import { render, screen } from 'spec/helpers/testing-library'; -import { ActionButtons } from './index'; +import ActionButtons from './index'; const createProps = () => ({ onApply: jest.fn(), diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx index 5fca65c3ec..eac032eb70 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx @@ -28,7 +28,8 @@ import { import Button from 'src/components/Button'; import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants'; import { rgba } from 'emotion-rgba'; -import { getFilterBarTestId } from '../index'; +import { getFilterBarTestId } from '../utils'; +import { FilterBarLocation } from 'src/dashboard/types'; interface ActionButtonsProps { width?: number; @@ -37,38 +38,15 @@ interface ActionButtonsProps { dataMaskSelected: DataMaskState; dataMaskApplied: DataMaskStateWithId; isApplyDisabled: boolean; + orientation?: FilterBarLocation; } -const ActionButtonsContainer = styled.div<{ width: number }>` - ${({ theme, width }) => css` +const ActionButtonsContainer = styled.div<{ + width: number; + orientation: FilterBarLocation; +}>` + ${({ theme, width, orientation }) => css` display: flex; - flex-direction: column; - align-items: center; - - position: fixed; - z-index: 100; - - // filter bar width minus 1px for border - width: ${width - 1}px; - bottom: 0; - - padding: ${theme.gridUnit * 4}px; - padding-top: ${theme.gridUnit * 6}px; - - background: linear-gradient( - ${rgba(theme.colors.grayscale.light5, 0)}, - ${theme.colors.grayscale.light5} ${theme.opacity.mediumLight} - ); - - pointer-events: none; - - & > button { - pointer-events: auto; - } - - & > .filter-apply-button { - margin-bottom: ${theme.gridUnit * 3}px; - } && > .filter-clear-all-button { color: ${theme.colors.grayscale.base}; @@ -82,16 +60,61 @@ const ActionButtonsContainer = styled.div<{ width: number }>` color: ${theme.colors.grayscale.light1}; } } + ${orientation === FilterBarLocation.VERTICAL && + ` + flex-direction: column; + align-items: center; + pointer-events: none; + position: fixed; + z-index: 100; + + // filter bar width minus 1px for border + width: ${width - 1}px; + bottom: 0; + + padding: ${theme.gridUnit * 4}px; + padding-top: ${theme.gridUnit * 6}px; + + background: linear-gradient( + ${rgba(theme.colors.grayscale.light5, 0)}, + ${theme.colors.grayscale.light5} ${theme.opacity.mediumLight} + ); + + & > button { + pointer-events: auto; + } + + & > .filter-apply-button { + margin-bottom: ${theme.gridUnit * 3}px; + } + `}; + + ${orientation === FilterBarLocation.HORIZONTAL && + ` + margin: 0 ${theme.gridUnit * 2}px; + && > .filter-clear-all-button { + text-transform: capitalize; + font-weight: ${theme.typography.weights.normal}; + } + & > .filter-apply-button { + &[disabled], + &[disabled]:hover { + color: ${theme.colors.grayscale.light1}; + background: ${theme.colors.grayscale.light3}; + } + } + `}; `}; `; -export const ActionButtons = ({ +const ActionButtons = ({ width = OPEN_FILTER_BAR_WIDTH, onApply, onClearAll, dataMaskApplied, dataMaskSelected, isApplyDisabled, + orientation = FilterBarLocation.VERTICAL, }: ActionButtonsProps) => { const isClearAllEnabled = useMemo( () => @@ -103,9 +126,14 @@ export const ActionButtons = ({ ), [dataMaskApplied, dataMaskSelected], ); + const isVertical = orientation === FilterBarLocation.VERTICAL; return ( - <ActionButtonsContainer data-test="filterbar-action-buttons" width={width}> + <ActionButtonsContainer + data-test="filterbar-action-buttons" + orientation={orientation} + width={width} + > <Button disabled={isApplyDisabled} buttonStyle="primary" @@ -114,7 +142,7 @@ export const ActionButtons = ({ onClick={onApply} {...getFilterBarTestId('apply-button')} > - {t('Apply filters')} + {isVertical ? t('Apply filters') : t('Apply')} </Button> <Button disabled={!isClearAllEnabled} @@ -129,3 +157,5 @@ export const ActionButtons = ({ </ActionButtonsContainer> ); }; + +export default ActionButtons; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx index 871f2b4026..8a9bd48a12 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx @@ -29,7 +29,8 @@ import { TimeFilterPlugin, SelectFilterPlugin } from 'src/filters/components'; import { DATE_FILTER_TEST_KEY } from 'src/explore/components/controls/DateFilterControl'; import fetchMock from 'fetch-mock'; import { waitFor } from '@testing-library/react'; -import FilterBar, { FILTER_BAR_TEST_ID } from '.'; +import { FILTER_BAR_TEST_ID } from './utils'; +import { VerticalFilterBar } from '.'; import { FILTERS_CONFIG_MODAL_TEST_ID } from '../FiltersConfigModal/FiltersConfigModal'; jest.useFakeTimers(); @@ -216,12 +217,15 @@ describe('FilterBar', () => { }); const renderWrapper = (props = closedBarProps, state?: object) => - render(<FilterBar {...props} width={280} height={400} offset={0} />, { - initialState: state, - useDnd: true, - useRedux: true, - useRouter: true, - }); + render( + <VerticalFilterBar {...props} width={280} height={400} offset={0} />, + { + initialState: state, + useDnd: true, + useRedux: true, + useRouter: true, + }, + ); it('should render', () => { const { container } = renderWrapper(); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/index.tsx index 82d4ba92e6..48260deb60 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/index.tsx @@ -26,7 +26,7 @@ import { saveFilterBarLocation } from 'src/dashboard/actions/dashboardInfo'; import Icons from 'src/components/Icons'; import DropdownSelectableIcon from 'src/components/DropdownSelectableIcon'; -export const FilterBarLocationSelect = () => { +const FilterBarLocationSelect = () => { const dispatch = useDispatch(); const theme = useTheme(); const filterBarLocation = useSelector<RootState, FilterBarLocation>( @@ -62,7 +62,7 @@ export const FilterBarLocationSelect = () => { return ( <DropdownSelectableIcon onSelect={toggleFilterBarLocation} - info={t('Placement of filter bar')} + info={t('Orientation of filter bar')} icon={<Icons.Gear name="gear" iconColor={theme.colors.grayscale.base} />} menuItems={[ { @@ -78,3 +78,5 @@ export const FilterBarLocationSelect = () => { /> ); }; + +export default FilterBarLocationSelect; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx index e99171c7e8..8319c3c9fb 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx @@ -22,7 +22,7 @@ import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters'; import Button from 'src/components/Button'; import { FilterConfiguration, styled } from '@superset-ui/core'; import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal'; -import { getFilterBarTestId } from '..'; +import { getFilterBarTestId } from '../utils'; export interface FCBProps { createNewOnOpen?: boolean; 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 986572c7f0..082111b94a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx @@ -24,7 +24,7 @@ import { checkIsMissingRequiredValue } from '../utils'; import FilterValue from './FilterValue'; import { FilterProps } from './types'; import { FilterCard } from '../../FilterCard'; -import { FilterBarScrollContext } from '../index'; +import { FilterBarScrollContext } from '../Vertical'; const StyledIcon = styled.div` position: absolute; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx index 689eaa8266..c4807d751f 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx @@ -27,7 +27,7 @@ import { ActionButtons } from './Footer'; import { useNativeFiltersDataMask, useFilters, useFilterSets } from '../state'; import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils'; import { useFilterSetNameDuplicated } from './state'; -import { getFilterBarTestId } from '../index'; +import { getFilterBarTestId } from '../utils'; const Wrapper = styled.div` display: grid; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSetUnit.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSetUnit.tsx index 53ea1c94c5..14e0c88b7e 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSetUnit.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSetUnit.tsx @@ -31,7 +31,7 @@ import { CheckOutlined, EllipsisOutlined } from '@ant-design/icons'; import Button from 'src/components/Button'; import { Tooltip } from 'src/components/Tooltip'; import FiltersHeader from './FiltersHeader'; -import { getFilterBarTestId } from '..'; +import { getFilterBarTestId } from '../utils'; const HeaderButton = styled(Button)` padding: 0; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx index 5a7bff6527..5982bf515a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx @@ -30,7 +30,7 @@ import Icons from 'src/components/Icons'; import { areObjectsEqual } from 'src/reduxUtils'; import { getFilterValueForDisplay } from './utils'; import { useFilters } from '../state'; -import { getFilterBarTestId } from '../index'; +import { getFilterBarTestId } from '../utils'; const FilterHeader = styled.div` display: flex; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx index df6c6ee44e..7847927ff5 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/Footer.tsx @@ -22,7 +22,7 @@ import Button from 'src/components/Button'; import { Tooltip } from 'src/components/Tooltip'; import { APPLY_FILTERS_HINT } from './utils'; import { useFilterSetNameDuplicated } from './state'; -import { getFilterBarTestId } from '..'; +import { getFilterBarTestId } from '../utils'; export type FooterProps = { filterSetName: string; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx index 5fdfd48171..2c727f278f 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx @@ -40,7 +40,7 @@ import { findExistingFilterSet } from './utils'; import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state'; import Footer from './Footer'; import FilterSetUnit from './FilterSetUnit'; -import { getFilterBarTestId } from '..'; +import { getFilterBarTestId } from '../utils'; import { TabIds } from '../utils'; const FilterSetsWrapper = styled.div` diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx index a20d6a61f7..77ec3203fb 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx @@ -32,8 +32,8 @@ import { useSelector } from 'react-redux'; import FilterConfigurationLink from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink'; import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state'; import { RootState } from 'src/dashboard/types'; -import { getFilterBarTestId } from '..'; -import { FilterBarLocationSelect } from '../FilterBarLocationSelect'; +import { getFilterBarTestId } from '../utils'; +import FilterBarLocationSelect from '../FilterBarLocationSelect'; const TitleArea = styled.h4` display: flex; @@ -58,6 +58,10 @@ const HeaderButton = styled(Button)` const Wrapper = styled.div` padding: ${({ theme }) => theme.gridUnit}px ${({ theme }) => theme.gridUnit * 2}px; + + .ant-dropdown-trigger span { + padding-right: ${({ theme }) => theme.gridUnit * 2}px; + } `; type HeaderProps = { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx similarity index 50% copy from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx copy to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx index 1b13f0583a..87aff179f9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx @@ -17,154 +17,90 @@ * under the License. */ -/* eslint-disable no-param-reassign */ -import throttle from 'lodash/throttle'; -import React, { - useEffect, - useState, - useCallback, - useMemo, - useRef, - createContext, -} from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import cx from 'classnames'; import { DataMaskStateWithId, DataMaskWithId, Filter, DataMask, - HandlerFunction, styled, t, SLOW_DEBOUNCE, isNativeFilter, } from '@superset-ui/core'; import Icons from 'src/components/Icons'; -import { AntdTabs } from 'src/components'; import { useHistory } from 'react-router-dom'; import { usePrevious } from 'src/hooks/usePrevious'; -import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { updateDataMask, clearDataMask } from 'src/dataMask/actions'; import { useImmer } from 'use-immer'; import { isEmpty, isEqual, debounce } from 'lodash'; -import { testWithId } from 'src/utils/testUtils'; import Loading from 'src/components/Loading'; import { getInitialDataMask } from 'src/dataMask/reducer'; import { URL_PARAMS } from 'src/constants'; import { getUrlParam } from 'src/utils/urlUtils'; -import { EmptyStateSmall } from 'src/components/EmptyState'; import { useTabId } from 'src/hooks/useTabId'; -import { RootState } from 'src/dashboard/types'; -import { checkIsApplyDisabled, TabIds } from './utils'; -import FilterSets from './FilterSets'; +import { FilterBarLocation, RootState } from 'src/dashboard/types'; +import { checkIsApplyDisabled } from './utils'; import { useNativeFiltersDataMask, useFilters, - useFilterSets, useFilterUpdates, useInitialization, } from './state'; import { createFilterKey, updateFilterKey } from './keyValue'; -import EditSection from './FilterSets/EditSection'; -import Header from './Header'; import FilterControls from './FilterControls/FilterControls'; -import { ActionButtons } from './ActionButtons'; +import ActionButtons from './ActionButtons'; +import { getFilterBarTestId } from './utils'; +import FilterBarLocationSelect from './FilterBarLocationSelect'; +import FilterConfigurationLink from './FilterConfigurationLink'; -export const FILTER_BAR_TEST_ID = 'filter-bar'; -export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID); - -const BarWrapper = styled.div<{ width: number }>` - width: ${({ theme }) => theme.gridUnit * 8}px; - - & .ant-tabs-top > .ant-tabs-nav { - margin: 0; - } - &.open { - width: ${({ width }) => width}px; // arbitrary... - } -`; - -const Bar = styled.div<{ width: number }>` - & .ant-typography-edit-content { - left: 0; - margin-top: 0; - width: 100%; - } - position: absolute; - top: 0; - left: 0; - flex-direction: column; - flex-grow: 1; - width: ${({ width }) => width}px; - background: ${({ theme }) => theme.colors.grayscale.light5}; - border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - min-height: 100%; - display: none; - &.open { - display: flex; - } -`; +export interface HorizontalFiltersBarProps { + directPathToChild?: string[]; +} -const CollapsedBar = styled.div<{ offset: number }>` - position: absolute; - top: ${({ offset }) => offset}px; - left: 0; - height: 100%; - width: ${({ theme }) => theme.gridUnit * 8}px; - padding-top: ${({ theme }) => theme.gridUnit * 2}px; - display: none; - text-align: center; - &.open { - display: flex; - flex-direction: column; - align-items: center; - padding: ${({ theme }) => theme.gridUnit * 2}px; - } - svg { - cursor: pointer; - } +const HorizontalBar = styled.div` + ${({ theme }) => ` + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + height: 48px; + background: ${theme.colors.grayscale.light5}; + box-shadow: inset 0px -2px 2px -1px ${theme.colors.grayscale.light2}; + padding: 0 ${theme.gridUnit * 4}px; +`} `; -const StyledCollapseIcon = styled(Icons.Collapse)` - color: ${({ theme }) => theme.colors.primary.base}; - margin-bottom: ${({ theme }) => theme.gridUnit * 3}px; +const FilterBarEmptyStateContainer = styled.div` + ${({ theme }) => ` + margin: 0 ${theme.gridUnit * 2}px; + font-weight: ${theme.typography.weights.bold}; +`} `; -const StyledFilterIcon = styled(Icons.Filter)` - color: ${({ theme }) => theme.colors.grayscale.base}; -`; +const FiltersLinkContainer = styled.div<{ hasFilters: boolean }>` + ${({ theme, hasFilters }) => ` + padding: 0 ${theme.gridUnit * 2}px; + border-right: ${ + hasFilters ? `1px solid ${theme.colors.grayscale.light2}` : 0 + }; -const StyledTabs = styled(AntdTabs)` - & .ant-tabs-nav-list { - width: 100%; - } - & .ant-tabs-tab { + button { display: flex; - justify-content: center; - margin: 0; - flex: 1; - } - - & > .ant-tabs-nav .ant-tabs-nav-operations { - display: none; + align-items: center; + text-transform: capitalize; + font-weight: ${theme.typography.weights.normal}; + color: ${theme.colors.primary.base}; + > .anticon + span, > .anticon { + margin-right: 0; + margin-left: 0; + } } +`} `; -const FilterBarEmptyStateContainer = styled.div` - margin-top: ${({ theme }) => theme.gridUnit * 8}px; -`; - -export interface FiltersBarProps { - filtersOpen: boolean; - toggleFiltersBar: any; - directPathToChild?: string[]; - width: number; - height: number | string; - offset: number; -} - const EXCLUDED_URL_PARAMS: string[] = [ URL_PARAMS.nativeFilters.name, URL_PARAMS.permalinkKey.name, @@ -223,31 +159,21 @@ const publishDataMask = debounce( SLOW_DEBOUNCE, ); -export const FilterBarScrollContext = createContext(false); -const FilterBar: React.FC<FiltersBarProps> = ({ - filtersOpen, - toggleFiltersBar, +const HorizontalFilterBar: React.FC<HorizontalFiltersBarProps> = ({ directPathToChild, - width, - height, - offset, }) => { const history = useHistory(); const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask(); - const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null); const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskStateWithId>(dataMaskApplied); const dispatch = useDispatch(); const [updateKey, setUpdateKey] = useState(0); const tabId = useTabId(); - const filterSets = useFilterSets(); - const filterSetFilterValues = Object.values(filterSets); - const [tab, setTab] = useState(TabIds.AllFilters); const filters = useFilters(); const previousFilters = usePrevious(filters); const filterValues = Object.values(filters); const nativeFilterValues = filterValues.filter(isNativeFilter); - const dashboardId = useSelector<any, string>( + const dashboardId = useSelector<any, number>( ({ dashboardInfo }) => dashboardInfo?.id, ); const previousDashboardId = usePrevious(dashboardId); @@ -255,9 +181,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({ ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ); - const [isScrolling, setIsScrolling] = useState(false); - const timeout = useRef<any>(); - const handleFilterSelectionChange = useCallback( ( filter: Pick<Filter, 'id'> & Partial<Filter>, @@ -352,29 +275,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({ }); }, [dataMaskSelected, dispatch, setDataMaskSelected]); - const openFiltersBar = useCallback( - () => toggleFiltersBar(true), - [toggleFiltersBar], - ); - - const onScroll = useCallback( - throttle(() => { - clearTimeout(timeout.current); - setIsScrolling(true); - timeout.current = setTimeout(() => { - setIsScrolling(false); - }, 300); - }, 200), - [], - ); - - useEffect(() => { - document.onscroll = onScroll; - return () => { - document.onscroll = null; - }; - }, [onScroll]); - useFilterUpdates(dataMaskSelected, setDataMaskSelected); const isApplyDisabled = checkIsApplyDisabled( dataMaskSelected, @@ -382,136 +282,48 @@ const FilterBar: React.FC<FiltersBarProps> = ({ nativeFilterValues, ); const isInitialized = useInitialization(); - const tabPaneStyle = useMemo( - () => ({ overflow: 'auto', height, overscrollBehavior: 'contain' }), - [height], - ); - - const numberOfFilters = nativeFilterValues.length; + const hasFilters = filterValues.length > 0; return ( - <FilterBarScrollContext.Provider value={isScrolling}> - <BarWrapper - {...getFilterBarTestId()} - className={cx({ open: filtersOpen })} - width={width} - > - <CollapsedBar - {...getFilterBarTestId('collapsable')} - className={cx({ open: !filtersOpen })} - onClick={openFiltersBar} - offset={offset} - > - <StyledCollapseIcon - {...getFilterBarTestId('expand-button')} - iconSize="l" - /> - <StyledFilterIcon - {...getFilterBarTestId('filter-icon')} - iconSize="l" - /> - </CollapsedBar> - <Bar className={cx({ open: filtersOpen })} width={width}> - <Header toggleFiltersBar={toggleFiltersBar} /> - {!isInitialized ? ( - <div css={{ height }}> - <Loading /> - </div> - ) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? ( - <StyledTabs - centered - onChange={setTab as HandlerFunction} - defaultActiveKey={TabIds.AllFilters} - activeKey={editFilterSetId ? TabIds.AllFilters : undefined} - > - <AntdTabs.TabPane - tab={t('All filters (%(filterCount)d)', { - filterCount: numberOfFilters, - })} - key={TabIds.AllFilters} - css={tabPaneStyle} - > - {editFilterSetId && ( - <EditSection - dataMaskSelected={dataMaskSelected} - disabled={!isApplyDisabled} - onCancel={() => setEditFilterSetId(null)} - filterSetId={editFilterSetId} - /> - )} - {filterValues.length === 0 ? ( - <FilterBarEmptyStateContainer> - <EmptyStateSmall - title={t('No filters are currently added')} - image="filter.svg" - description={ - canEdit && - t( - 'Click the button above to add a filter to the dashboard', - ) - } - /> - </FilterBarEmptyStateContainer> - ) : ( - <FilterControls - dataMaskSelected={dataMaskSelected} - directPathToChild={directPathToChild} - onFilterSelectionChange={handleFilterSelectionChange} - /> - )} - </AntdTabs.TabPane> - <AntdTabs.TabPane - disabled={!!editFilterSetId} - tab={t('Filter sets (%(filterSetCount)d)', { - filterSetCount: filterSetFilterValues.length, - })} - key={TabIds.FilterSets} - css={tabPaneStyle} + <HorizontalBar {...getFilterBarTestId()}> + {!isInitialized ? ( + <Loading position="inline-centered" /> + ) : ( + <> + {canEdit && <FilterBarLocationSelect />} + {!hasFilters && ( + <FilterBarEmptyStateContainer> + {t('No filters are currently added to this dashboard.')} + </FilterBarEmptyStateContainer> + )} + {canEdit && ( + <FiltersLinkContainer hasFilters={hasFilters}> + <FilterConfigurationLink + dashboardId={dashboardId} + createNewOnOpen={filterValues.length === 0} > - <FilterSets - onEditFilterSet={setEditFilterSetId} - disabled={!isApplyDisabled} - dataMaskSelected={dataMaskSelected} - tab={tab} - onFilterSelectionChange={handleFilterSelectionChange} - /> - </AntdTabs.TabPane> - </StyledTabs> - ) : ( - <div css={tabPaneStyle} onScroll={onScroll}> - {filterValues.length === 0 ? ( - <FilterBarEmptyStateContainer> - <EmptyStateSmall - title={t('No filters are currently added')} - image="filter.svg" - description={ - canEdit && - t( - 'Click the button above to add a filter to the dashboard', - ) - } - /> - </FilterBarEmptyStateContainer> - ) : ( - <FilterControls - dataMaskSelected={dataMaskSelected} - directPathToChild={directPathToChild} - onFilterSelectionChange={handleFilterSelectionChange} - /> - )} - </div> + <Icons.PlusSmall /> {t('Add/Edit Filters')} + </FilterConfigurationLink> + </FiltersLinkContainer> + )} + {hasFilters && ( + <FilterControls + dataMaskSelected={dataMaskSelected} + directPathToChild={directPathToChild} + onFilterSelectionChange={handleFilterSelectionChange} + /> )} <ActionButtons - width={width} + orientation={FilterBarLocation.HORIZONTAL} onApply={handleApply} onClearAll={handleClearAll} dataMaskSelected={dataMaskSelected} dataMaskApplied={dataMaskApplied} isApplyDisabled={isApplyDisabled} /> - </Bar> - </BarWrapper> - </FilterBarScrollContext.Provider> + </> + )} + </HorizontalBar> ); }; -export default React.memo(FilterBar); +export default React.memo(HorizontalFilterBar); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx similarity index 97% copy from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx copy to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index 1b13f0583a..569a9e3554 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -48,7 +48,6 @@ import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { updateDataMask, clearDataMask } from 'src/dataMask/actions'; import { useImmer } from 'use-immer'; import { isEmpty, isEqual, debounce } from 'lodash'; -import { testWithId } from 'src/utils/testUtils'; import Loading from 'src/components/Loading'; import { getInitialDataMask } from 'src/dataMask/reducer'; import { URL_PARAMS } from 'src/constants'; @@ -69,10 +68,17 @@ import { createFilterKey, updateFilterKey } from './keyValue'; import EditSection from './FilterSets/EditSection'; import Header from './Header'; import FilterControls from './FilterControls/FilterControls'; -import { ActionButtons } from './ActionButtons'; +import ActionButtons from './ActionButtons'; +import { getFilterBarTestId } from './utils'; -export const FILTER_BAR_TEST_ID = 'filter-bar'; -export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID); +export interface VerticalFiltersBarProps { + filtersOpen: boolean; + toggleFiltersBar: any; + directPathToChild?: string[]; + width: number; + height: number | string; + offset: number; +} const BarWrapper = styled.div<{ width: number }>` width: ${({ theme }) => theme.gridUnit * 8}px; @@ -156,15 +162,6 @@ const FilterBarEmptyStateContainer = styled.div` margin-top: ${({ theme }) => theme.gridUnit * 8}px; `; -export interface FiltersBarProps { - filtersOpen: boolean; - toggleFiltersBar: any; - directPathToChild?: string[]; - width: number; - height: number | string; - offset: number; -} - const EXCLUDED_URL_PARAMS: string[] = [ URL_PARAMS.nativeFilters.name, URL_PARAMS.permalinkKey.name, @@ -224,7 +221,7 @@ const publishDataMask = debounce( ); export const FilterBarScrollContext = createContext(false); -const FilterBar: React.FC<FiltersBarProps> = ({ +const VerticalFilterBar: React.FC<VerticalFiltersBarProps> = ({ filtersOpen, toggleFiltersBar, directPathToChild, @@ -514,4 +511,4 @@ const FilterBar: React.FC<FiltersBarProps> = ({ </FilterBarScrollContext.Provider> ); }; -export default React.memo(FilterBar); +export default React.memo(VerticalFilterBar); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx index 1b13f0583a..91644eda25 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx @@ -16,502 +16,5 @@ * specific language governing permissions and limitations * under the License. */ - -/* eslint-disable no-param-reassign */ -import throttle from 'lodash/throttle'; -import React, { - useEffect, - useState, - useCallback, - useMemo, - useRef, - createContext, -} from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import cx from 'classnames'; -import { - DataMaskStateWithId, - DataMaskWithId, - Filter, - DataMask, - HandlerFunction, - styled, - t, - SLOW_DEBOUNCE, - isNativeFilter, -} from '@superset-ui/core'; -import Icons from 'src/components/Icons'; -import { AntdTabs } from 'src/components'; -import { useHistory } from 'react-router-dom'; -import { usePrevious } from 'src/hooks/usePrevious'; -import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; -import { updateDataMask, clearDataMask } from 'src/dataMask/actions'; -import { useImmer } from 'use-immer'; -import { isEmpty, isEqual, debounce } from 'lodash'; -import { testWithId } from 'src/utils/testUtils'; -import Loading from 'src/components/Loading'; -import { getInitialDataMask } from 'src/dataMask/reducer'; -import { URL_PARAMS } from 'src/constants'; -import { getUrlParam } from 'src/utils/urlUtils'; -import { EmptyStateSmall } from 'src/components/EmptyState'; -import { useTabId } from 'src/hooks/useTabId'; -import { RootState } from 'src/dashboard/types'; -import { checkIsApplyDisabled, TabIds } from './utils'; -import FilterSets from './FilterSets'; -import { - useNativeFiltersDataMask, - useFilters, - useFilterSets, - useFilterUpdates, - useInitialization, -} from './state'; -import { createFilterKey, updateFilterKey } from './keyValue'; -import EditSection from './FilterSets/EditSection'; -import Header from './Header'; -import FilterControls from './FilterControls/FilterControls'; -import { ActionButtons } from './ActionButtons'; - -export const FILTER_BAR_TEST_ID = 'filter-bar'; -export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID); - -const BarWrapper = styled.div<{ width: number }>` - width: ${({ theme }) => theme.gridUnit * 8}px; - - & .ant-tabs-top > .ant-tabs-nav { - margin: 0; - } - &.open { - width: ${({ width }) => width}px; // arbitrary... - } -`; - -const Bar = styled.div<{ width: number }>` - & .ant-typography-edit-content { - left: 0; - margin-top: 0; - width: 100%; - } - position: absolute; - top: 0; - left: 0; - flex-direction: column; - flex-grow: 1; - width: ${({ width }) => width}px; - background: ${({ theme }) => theme.colors.grayscale.light5}; - border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - min-height: 100%; - display: none; - &.open { - display: flex; - } -`; - -const CollapsedBar = styled.div<{ offset: number }>` - position: absolute; - top: ${({ offset }) => offset}px; - left: 0; - height: 100%; - width: ${({ theme }) => theme.gridUnit * 8}px; - padding-top: ${({ theme }) => theme.gridUnit * 2}px; - display: none; - text-align: center; - &.open { - display: flex; - flex-direction: column; - align-items: center; - padding: ${({ theme }) => theme.gridUnit * 2}px; - } - svg { - cursor: pointer; - } -`; - -const StyledCollapseIcon = styled(Icons.Collapse)` - color: ${({ theme }) => theme.colors.primary.base}; - margin-bottom: ${({ theme }) => theme.gridUnit * 3}px; -`; - -const StyledFilterIcon = styled(Icons.Filter)` - color: ${({ theme }) => theme.colors.grayscale.base}; -`; - -const StyledTabs = styled(AntdTabs)` - & .ant-tabs-nav-list { - width: 100%; - } - & .ant-tabs-tab { - display: flex; - justify-content: center; - margin: 0; - flex: 1; - } - - & > .ant-tabs-nav .ant-tabs-nav-operations { - display: none; - } -`; - -const FilterBarEmptyStateContainer = styled.div` - margin-top: ${({ theme }) => theme.gridUnit * 8}px; -`; - -export interface FiltersBarProps { - filtersOpen: boolean; - toggleFiltersBar: any; - directPathToChild?: string[]; - width: number; - height: number | string; - offset: number; -} - -const EXCLUDED_URL_PARAMS: string[] = [ - URL_PARAMS.nativeFilters.name, - URL_PARAMS.permalinkKey.name, -]; - -const publishDataMask = debounce( - async ( - history, - dashboardId, - updateKey, - dataMaskSelected: DataMaskStateWithId, - tabId, - ) => { - const { location } = history; - const { search } = location; - const previousParams = new URLSearchParams(search); - const newParams = new URLSearchParams(); - let dataMaskKey: string | null; - previousParams.forEach((value, key) => { - if (!EXCLUDED_URL_PARAMS.includes(key)) { - newParams.append(key, value); - } - }); - - const nativeFiltersCacheKey = getUrlParam(URL_PARAMS.nativeFiltersKey); - const dataMask = JSON.stringify(dataMaskSelected); - if ( - updateKey && - nativeFiltersCacheKey && - (await updateFilterKey( - dashboardId, - dataMask, - nativeFiltersCacheKey, - tabId, - )) - ) { - dataMaskKey = nativeFiltersCacheKey; - } else { - dataMaskKey = await createFilterKey(dashboardId, dataMask, tabId); - } - if (dataMaskKey) { - newParams.set(URL_PARAMS.nativeFiltersKey.name, dataMaskKey); - } - - // pathname could be updated somewhere else through window.history - // keep react router history in sync with window history - // replace params only when current page is /superset/dashboard - // this prevents a race condition between updating filters and navigating to Explore - if (window.location.pathname.includes('/superset/dashboard')) { - history.location.pathname = window.location.pathname; - history.replace({ - search: newParams.toString(), - }); - } - }, - SLOW_DEBOUNCE, -); - -export const FilterBarScrollContext = createContext(false); -const FilterBar: React.FC<FiltersBarProps> = ({ - filtersOpen, - toggleFiltersBar, - directPathToChild, - width, - height, - offset, -}) => { - const history = useHistory(); - const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask(); - const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null); - const [dataMaskSelected, setDataMaskSelected] = - useImmer<DataMaskStateWithId>(dataMaskApplied); - const dispatch = useDispatch(); - const [updateKey, setUpdateKey] = useState(0); - const tabId = useTabId(); - const filterSets = useFilterSets(); - const filterSetFilterValues = Object.values(filterSets); - const [tab, setTab] = useState(TabIds.AllFilters); - const filters = useFilters(); - const previousFilters = usePrevious(filters); - const filterValues = Object.values(filters); - const nativeFilterValues = filterValues.filter(isNativeFilter); - const dashboardId = useSelector<any, string>( - ({ dashboardInfo }) => dashboardInfo?.id, - ); - const previousDashboardId = usePrevious(dashboardId); - const canEdit = useSelector<RootState, boolean>( - ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, - ); - - const [isScrolling, setIsScrolling] = useState(false); - const timeout = useRef<any>(); - - const handleFilterSelectionChange = useCallback( - ( - filter: Pick<Filter, 'id'> & Partial<Filter>, - dataMask: Partial<DataMask>, - ) => { - setDataMaskSelected(draft => { - // force instant updating on initialization for filters with `requiredFirst` is true or instant filters - if ( - // filterState.value === undefined - means that value not initialized - dataMask.filterState?.value !== undefined && - dataMaskSelected[filter.id]?.filterState?.value === undefined && - filter.requiredFirst - ) { - dispatch(updateDataMask(filter.id, dataMask)); - } - - draft[filter.id] = { - ...(getInitialDataMask(filter.id) as DataMaskWithId), - ...dataMask, - }; - }); - }, - [dataMaskSelected, dispatch, setDataMaskSelected], - ); - - useEffect(() => { - if (previousFilters && dashboardId === previousDashboardId) { - const updates = {}; - Object.values(filters).forEach(currentFilter => { - const previousFilter = previousFilters?.[currentFilter.id]; - if (!previousFilter) { - return; - } - const currentType = currentFilter.filterType; - const currentTargets = currentFilter.targets; - const currentDataMask = currentFilter.defaultDataMask; - const previousType = previousFilter?.filterType; - const previousTargets = previousFilter?.targets; - const previousDataMask = previousFilter?.defaultDataMask; - const typeChanged = currentType !== previousType; - const targetsChanged = !isEqual(currentTargets, previousTargets); - const dataMaskChanged = !isEqual(currentDataMask, previousDataMask); - - if (typeChanged || targetsChanged || dataMaskChanged) { - updates[currentFilter.id] = getInitialDataMask(currentFilter.id); - } - }); - - if (!isEmpty(updates)) { - setDataMaskSelected(draft => ({ ...draft, ...updates })); - Object.keys(updates).forEach(key => dispatch(clearDataMask(key))); - } - } - }, [ - JSON.stringify(filters), - JSON.stringify(previousFilters), - previousDashboardId, - ]); - - const dataMaskAppliedText = JSON.stringify(dataMaskApplied); - - useEffect(() => { - setDataMaskSelected(() => dataMaskApplied); - }, [dataMaskAppliedText, setDataMaskSelected]); - - useEffect(() => { - publishDataMask(history, dashboardId, updateKey, dataMaskApplied, tabId); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dashboardId, dataMaskAppliedText, history, updateKey, tabId]); - - const handleApply = useCallback(() => { - const filterIds = Object.keys(dataMaskSelected); - setUpdateKey(1); - filterIds.forEach(filterId => { - if (dataMaskSelected[filterId]) { - dispatch(updateDataMask(filterId, dataMaskSelected[filterId])); - } - }); - }, [dataMaskSelected, dispatch]); - - const handleClearAll = useCallback(() => { - const filterIds = Object.keys(dataMaskSelected); - filterIds.forEach(filterId => { - if (dataMaskSelected[filterId]) { - dispatch(clearDataMask(filterId)); - setDataMaskSelected(draft => { - if (draft[filterId].filterState?.value !== undefined) { - draft[filterId].filterState!.value = undefined; - } - }); - } - }); - }, [dataMaskSelected, dispatch, setDataMaskSelected]); - - const openFiltersBar = useCallback( - () => toggleFiltersBar(true), - [toggleFiltersBar], - ); - - const onScroll = useCallback( - throttle(() => { - clearTimeout(timeout.current); - setIsScrolling(true); - timeout.current = setTimeout(() => { - setIsScrolling(false); - }, 300); - }, 200), - [], - ); - - useEffect(() => { - document.onscroll = onScroll; - return () => { - document.onscroll = null; - }; - }, [onScroll]); - - useFilterUpdates(dataMaskSelected, setDataMaskSelected); - const isApplyDisabled = checkIsApplyDisabled( - dataMaskSelected, - dataMaskApplied, - nativeFilterValues, - ); - const isInitialized = useInitialization(); - const tabPaneStyle = useMemo( - () => ({ overflow: 'auto', height, overscrollBehavior: 'contain' }), - [height], - ); - - const numberOfFilters = nativeFilterValues.length; - - return ( - <FilterBarScrollContext.Provider value={isScrolling}> - <BarWrapper - {...getFilterBarTestId()} - className={cx({ open: filtersOpen })} - width={width} - > - <CollapsedBar - {...getFilterBarTestId('collapsable')} - className={cx({ open: !filtersOpen })} - onClick={openFiltersBar} - offset={offset} - > - <StyledCollapseIcon - {...getFilterBarTestId('expand-button')} - iconSize="l" - /> - <StyledFilterIcon - {...getFilterBarTestId('filter-icon')} - iconSize="l" - /> - </CollapsedBar> - <Bar className={cx({ open: filtersOpen })} width={width}> - <Header toggleFiltersBar={toggleFiltersBar} /> - {!isInitialized ? ( - <div css={{ height }}> - <Loading /> - </div> - ) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? ( - <StyledTabs - centered - onChange={setTab as HandlerFunction} - defaultActiveKey={TabIds.AllFilters} - activeKey={editFilterSetId ? TabIds.AllFilters : undefined} - > - <AntdTabs.TabPane - tab={t('All filters (%(filterCount)d)', { - filterCount: numberOfFilters, - })} - key={TabIds.AllFilters} - css={tabPaneStyle} - > - {editFilterSetId && ( - <EditSection - dataMaskSelected={dataMaskSelected} - disabled={!isApplyDisabled} - onCancel={() => setEditFilterSetId(null)} - filterSetId={editFilterSetId} - /> - )} - {filterValues.length === 0 ? ( - <FilterBarEmptyStateContainer> - <EmptyStateSmall - title={t('No filters are currently added')} - image="filter.svg" - description={ - canEdit && - t( - 'Click the button above to add a filter to the dashboard', - ) - } - /> - </FilterBarEmptyStateContainer> - ) : ( - <FilterControls - dataMaskSelected={dataMaskSelected} - directPathToChild={directPathToChild} - onFilterSelectionChange={handleFilterSelectionChange} - /> - )} - </AntdTabs.TabPane> - <AntdTabs.TabPane - disabled={!!editFilterSetId} - tab={t('Filter sets (%(filterSetCount)d)', { - filterSetCount: filterSetFilterValues.length, - })} - key={TabIds.FilterSets} - css={tabPaneStyle} - > - <FilterSets - onEditFilterSet={setEditFilterSetId} - disabled={!isApplyDisabled} - dataMaskSelected={dataMaskSelected} - tab={tab} - onFilterSelectionChange={handleFilterSelectionChange} - /> - </AntdTabs.TabPane> - </StyledTabs> - ) : ( - <div css={tabPaneStyle} onScroll={onScroll}> - {filterValues.length === 0 ? ( - <FilterBarEmptyStateContainer> - <EmptyStateSmall - title={t('No filters are currently added')} - image="filter.svg" - description={ - canEdit && - t( - 'Click the button above to add a filter to the dashboard', - ) - } - /> - </FilterBarEmptyStateContainer> - ) : ( - <FilterControls - dataMaskSelected={dataMaskSelected} - directPathToChild={directPathToChild} - onFilterSelectionChange={handleFilterSelectionChange} - /> - )} - </div> - )} - <ActionButtons - width={width} - onApply={handleApply} - onClearAll={handleClearAll} - dataMaskSelected={dataMaskSelected} - dataMaskApplied={dataMaskApplied} - isApplyDisabled={isApplyDisabled} - /> - </Bar> - </BarWrapper> - </FilterBarScrollContext.Provider> - ); -}; -export default React.memo(FilterBar); +export { default as VerticalFilterBar } from './Vertical'; +export { default as HorizontalFilterBar } from './Horizontal'; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts index 842bb44054..1fa11db467 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts @@ -19,6 +19,7 @@ import { areObjectsEqual } from 'src/reduxUtils'; import { DataMaskStateWithId, Filter, FilterState } from '@superset-ui/core'; +import { testWithId } from 'src/utils/testUtils'; export enum TabIds { AllFilters = 'allFilters', @@ -65,3 +66,6 @@ export const checkIsApplyDisabled = ( ) ); }; + +export const FILTER_BAR_TEST_ID = 'filter-bar'; +export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID);
