diff --git a/superset-frontend/spec/fixtures/mockDashboardInfo.js b/superset-frontend/spec/fixtures/mockDashboardInfo.js index c11ec7f88a35d..b2f9f11832232 100644 --- a/superset-frontend/spec/fixtures/mockDashboardInfo.js +++ b/superset-frontend/spec/fixtures/mockDashboardInfo.js @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { FilterBarOrientation } from 'src/dashboard/types'; + export default { id: 1234, slug: 'dashboardSlug', @@ -36,4 +38,5 @@ export default { flash_messages: [], conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 }, }, + filterBarOrientation: FilterBarOrientation.VERTICAL, }; diff --git a/superset-frontend/spec/fixtures/mockStore.js b/superset-frontend/spec/fixtures/mockStore.js index 5fe8f54022eb7..9f62f52b68249 100644 --- a/superset-frontend/spec/fixtures/mockStore.js +++ b/superset-frontend/spec/fixtures/mockStore.js @@ -71,60 +71,64 @@ export const mockStoreWithChartsInTabsAndRoot = export const sliceIdWithAppliedFilter = sliceId + 1; export const sliceIdWithRejectedFilter = sliceId + 2; +export const stateWithFilters = { + ...mockState, + dashboardFilters, + dataMask: dataMaskWith2Filters, + charts: { + ...mockState.charts, + [sliceIdWithAppliedFilter]: { + ...mockState.charts[sliceId], + queryResponse: { + status: 'success', + applied_filters: [{ column: 'region' }], + rejected_filters: [], + }, + }, + [sliceIdWithRejectedFilter]: { + ...mockState.charts[sliceId], + queryResponse: { + status: 'success', + applied_filters: [], + rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }], + }, + }, + }, +}; + // has one chart with a filter that has been applied, // one chart with a filter that has been rejected, // and one chart with no filters set. export const getMockStoreWithFilters = () => - createStore(rootReducer, { - ...mockState, - dashboardFilters, - dataMask: dataMaskWith2Filters, - charts: { - ...mockState.charts, - [sliceIdWithAppliedFilter]: { - ...mockState.charts[sliceId], - queryResponse: { - status: 'success', - applied_filters: [{ column: 'region' }], - rejected_filters: [], - }, + createStore(rootReducer, stateWithFilters); + +export const stateWithNativeFilters = { + ...mockState, + nativeFilters, + dataMask: dataMaskWith2Filters, + charts: { + ...mockState.charts, + [sliceIdWithAppliedFilter]: { + ...mockState.charts[sliceId], + queryResponse: { + status: 'success', + applied_filters: [{ column: 'region' }], + rejected_filters: [], }, - [sliceIdWithRejectedFilter]: { - ...mockState.charts[sliceId], - queryResponse: { - status: 'success', - applied_filters: [], - rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }], - }, + }, + [sliceIdWithRejectedFilter]: { + ...mockState.charts[sliceId], + queryResponse: { + status: 'success', + applied_filters: [], + rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }], }, }, - }); + }, +}; export const getMockStoreWithNativeFilters = () => - createStore(rootReducer, { - ...mockState, - nativeFilters, - dataMask: dataMaskWith2Filters, - charts: { - ...mockState.charts, - [sliceIdWithAppliedFilter]: { - ...mockState.charts[sliceId], - queryResponse: { - status: 'success', - applied_filters: [{ column: 'region' }], - rejected_filters: [], - }, - }, - [sliceIdWithRejectedFilter]: { - ...mockState.charts[sliceId], - queryResponse: { - status: 'success', - applied_filters: [], - rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }], - }, - }, - }, - }); + createStore(rootReducer, stateWithNativeFilters); export const stateWithoutNativeFilters = { ...mockState, diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx index 228c6889a22e7..b6c5d89a2e4e7 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/actions/dashboardInfo.ts b/superset-frontend/src/dashboard/actions/dashboardInfo.ts index 19035a2b22033..dbec0cd1cc260 100644 --- a/superset-frontend/src/dashboard/actions/dashboardInfo.ts +++ b/superset-frontend/src/dashboard/actions/dashboardInfo.ts @@ -23,7 +23,7 @@ import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { addDangerToast } from 'src/components/MessageToasts/actions'; import { DashboardInfo, - FilterBarLocation, + FilterBarOrientation, RootState, } from 'src/dashboard/types'; import { ChartConfiguration } from 'src/dashboard/reducers/types'; @@ -120,16 +120,18 @@ export const setChartConfiguration = } }; -export const SET_FILTER_BAR_LOCATION = 'SET_FILTER_BAR_LOCATION'; -export interface SetFilterBarLocation { - type: typeof SET_FILTER_BAR_LOCATION; - filterBarLocation: FilterBarLocation; +export const SET_FILTER_BAR_ORIENTATION = 'SET_FILTER_BAR_ORIENTATION'; +export interface SetFilterBarOrientation { + type: typeof SET_FILTER_BAR_ORIENTATION; + filterBarOrientation: FilterBarOrientation; } -export function setFilterBarLocation(filterBarLocation: FilterBarLocation) { - return { type: SET_FILTER_BAR_LOCATION, filterBarLocation }; +export function setFilterBarOrientation( + filterBarOrientation: FilterBarOrientation, +) { + return { type: SET_FILTER_BAR_ORIENTATION, filterBarOrientation }; } -export function saveFilterBarLocation(location: FilterBarLocation) { +export function saveFilterBarOrientation(orientation: FilterBarOrientation) { return async (dispatch: Dispatch, getState: () => RootState) => { const { id, metadata } = getState().dashboardInfo; const updateDashboard = makeApi< @@ -143,15 +145,15 @@ export function saveFilterBarLocation(location: FilterBarLocation) { const response = await updateDashboard({ json_metadata: JSON.stringify({ ...metadata, - filter_bar_location: location, + filter_bar_orientation: orientation, }), }); const updatedDashboard = response.result; const lastModifiedTime = response.last_modified_time; if (updatedDashboard.json_metadata) { const metadata = JSON.parse(updatedDashboard.json_metadata); - if (metadata.filter_bar_location) { - dispatch(setFilterBarLocation(metadata.filter_bar_location)); + if (metadata.filter_bar_orientation) { + dispatch(setFilterBarOrientation(metadata.filter_bar_orientation)); } } if (lastModifiedTime) { diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 5e3b97247a88c..ad0b38f4e7964 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -57,7 +57,7 @@ import getNativeFilterConfig from '../util/filterboxMigrationHelper'; import { updateColorSchema } from './dashboardInfo'; import { getChartIdsInFilterScope } from '../util/getChartIdsInFilterScope'; import updateComponentParentsList from '../util/updateComponentParentsList'; -import { FilterBarLocation } from '../types'; +import { FilterBarOrientation } from '../types'; export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; @@ -429,8 +429,8 @@ export const hydrateDashboard = flash_messages: common?.flash_messages, conf: common?.conf, }, - filterBarLocation: - metadata.filter_bar_location ?? FilterBarLocation.VERTICAL, + filterBarOrientation: + metadata.filter_bar_orientation ?? FilterBarOrientation.VERTICAL, }, dataMask, dashboardFilters, diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index cd0cfdb3fcd44..6a3c516afd72d 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, + FilterBarOrientation, + RootState, +} from 'src/dashboard/types'; import { setDirectPathToChild, setEditMode, @@ -241,6 +245,9 @@ const DashboardBuilder: FC = () => { const fullSizeChartId = useSelector( state => state.dashboardState.fullSizeChartId, ); + const filterBarOrientation = useSelector( + ({ dashboardInfo }) => dashboardInfo.filterBarOrientation, + ); const handleChangeTab = useCallback( ({ pathToTabIndex }: { pathToTabIndex: string[] }) => { @@ -277,6 +284,7 @@ const DashboardBuilder: FC = () => { uiConfig.hideTitle || standaloneMode === DashboardStandaloneMode.HIDE_NAV_AND_TITLE || isReport; + const [barTopOffset, setBarTopOffset] = useState(0); useEffect(() => { @@ -312,6 +320,7 @@ const DashboardBuilder: FC = () => { const filterSetEnabled = isFeatureEnabled( FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET, ); + const showFilterBar = nativeFiltersEnabled && !editMode; const offset = FILTER_BAR_HEADER_HEIGHT + @@ -354,6 +363,13 @@ const DashboardBuilder: FC = () => { ({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => (
{!hideDashboardHeader && } + {showFilterBar && + filterBarOrientation === FilterBarOrientation.HORIZONTAL && ( + + )} {dropIndicatorProps &&
} {!isReport && topLevelTabs && !uiConfig.hideNav && ( = () => {
), [ + directPathToChild, + nativeFiltersEnabled, + filterBarOrientation, editMode, handleChangeTab, handleDeleteTopLevelTabs, @@ -394,7 +413,7 @@ const DashboardBuilder: FC = () => { return ( - {nativeFiltersEnabled && !editMode && ( + {showFilterBar && filterBarOrientation === FilterBarOrientation.VERTICAL && ( <> = () => { 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 3c3f838c4ab08..525f519632463 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 5fca65c3ec6c1..c06c18df89d6c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx @@ -21,14 +21,15 @@ import { css, DataMaskState, DataMaskStateWithId, - styled, t, isDefined, + SupersetTheme, } from '@superset-ui/core'; 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 { FilterBarOrientation } from 'src/dashboard/types'; +import { getFilterBarTestId } from '../utils'; interface ActionButtonsProps { width?: number; @@ -37,61 +38,77 @@ interface ActionButtonsProps { dataMaskSelected: DataMaskState; dataMaskApplied: DataMaskStateWithId; isApplyDisabled: boolean; + filterBarOrientation?: FilterBarOrientation; } -const ActionButtonsContainer = styled.div<{ width: number }>` - ${({ theme, width }) => css` - display: flex; - flex-direction: column; - align-items: center; +const containerStyle = (theme: SupersetTheme) => css` + display: flex; - position: fixed; - z-index: 100; + && > .filter-clear-all-button { + color: ${theme.colors.grayscale.base}; + margin-left: 0; + &:hover { + color: ${theme.colors.primary.dark1}; + } - // filter bar width minus 1px for border - width: ${width - 1}px; - bottom: 0; + &[disabled], + &[disabled]:hover { + color: ${theme.colors.grayscale.light1}; + } + } +`; - padding: ${theme.gridUnit * 4}px; - padding-top: ${theme.gridUnit * 6}px; +const verticalStyle = (theme: SupersetTheme, width: number) => css` + flex-direction: column; + align-items: center; + pointer-events: none; + position: fixed; + z-index: 100; - background: linear-gradient( - ${rgba(theme.colors.grayscale.light5, 0)}, - ${theme.colors.grayscale.light5} ${theme.opacity.mediumLight} - ); + // filter bar width minus 1px for border + width: ${width - 1}px; + bottom: 0; - pointer-events: none; + padding: ${theme.gridUnit * 4}px; + padding-top: ${theme.gridUnit * 6}px; - & > button { - pointer-events: auto; - } + background: linear-gradient( + ${rgba(theme.colors.grayscale.light5, 0)}, + ${theme.colors.grayscale.light5} ${theme.opacity.mediumLight} + ); - & > .filter-apply-button { - margin-bottom: ${theme.gridUnit * 3}px; - } + & > button { + pointer-events: auto; + } - && > .filter-clear-all-button { - color: ${theme.colors.grayscale.base}; - margin-left: 0; - &:hover { - color: ${theme.colors.primary.dark1}; - } + & > .filter-apply-button { + margin-bottom: ${theme.gridUnit * 3}px; + } +`; - &[disabled], - &[disabled]:hover { - color: ${theme.colors.grayscale.light1}; - } +const horizontalStyle = (theme: SupersetTheme) => css` + 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, + filterBarOrientation = FilterBarOrientation.VERTICAL, }: ActionButtonsProps) => { const isClearAllEnabled = useMemo( () => @@ -103,9 +120,16 @@ export const ActionButtons = ({ ), [dataMaskApplied, dataMaskSelected], ); + const isVertical = filterBarOrientation === FilterBarOrientation.VERTICAL; return ( - +
[ + containerStyle(theme), + isVertical ? verticalStyle(theme, width) : horizontalStyle(theme), + ]} + data-test="filterbar-action-buttons" + > - +
); }; + +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 871f2b402647f..7a8106ecbe971 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,9 @@ 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 { FilterBarOrientation } from 'src/dashboard/types'; +import { FILTER_BAR_TEST_ID } from './utils'; +import FilterBar from '.'; import { FILTERS_CONFIG_MODAL_TEST_ID } from '../FiltersConfigModal/FiltersConfigModal'; jest.useFakeTimers(); @@ -216,12 +218,23 @@ describe('FilterBar', () => { }); const renderWrapper = (props = closedBarProps, state?: object) => - render(, { - initialState: state, - useDnd: true, - useRedux: true, - useRouter: true, - }); + render( + , + { + 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/FilterBarLocationSelect.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/FilterBarOrientationSelect.test.tsx similarity index 93% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/FilterBarLocationSelect.test.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/FilterBarOrientationSelect.test.tsx index 90b640a2c1678..28a40aad057c6 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/FilterBarLocationSelect.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/FilterBarOrientationSelect.test.tsx @@ -22,9 +22,9 @@ import fetchMock from 'fetch-mock'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render, screen, within } from 'spec/helpers/testing-library'; -import { DashboardInfo, FilterBarLocation } from 'src/dashboard/types'; +import { DashboardInfo, FilterBarOrientation } from 'src/dashboard/types'; import * as mockedMessageActions from 'src/components/MessageToasts/actions'; -import { FilterBarLocationSelect } from './index'; +import FilterBarOrientationSelect from '.'; const initialState: { dashboardInfo: DashboardInfo } = { dashboardInfo: { @@ -42,7 +42,7 @@ const initialState: { dashboardInfo: DashboardInfo } = { }, json_metadata: '', dash_edit_perm: true, - filterBarLocation: FilterBarLocation.VERTICAL, + filterBarOrientation: FilterBarOrientation.VERTICAL, common: { conf: {}, flash_messages: [], @@ -51,7 +51,7 @@ const initialState: { dashboardInfo: DashboardInfo } = { }; const setup = (dashboardInfoOverride: Partial = {}) => - render(, { + render(, { useRedux: true, initialState: { ...initialState, @@ -78,7 +78,7 @@ test('Popover opens with "Vertical" selected', async () => { }); test('Popover opens with "Horizontal" selected', async () => { - setup({ filterBarLocation: FilterBarLocation.HORIZONTAL }); + setup({ filterBarOrientation: FilterBarOrientation.HORIZONTAL }); userEvent.click(screen.getByLabelText('gear')); expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument(); @@ -93,7 +93,7 @@ test('On selection change, send request and update checked value', async () => { result: { json_metadata: JSON.stringify({ ...initialState.dashboardInfo.metadata, - filter_bar_location: 'HORIZONTAL', + filter_bar_orientation: 'HORIZONTAL', }), }, }); @@ -124,7 +124,7 @@ test('On selection change, send request and update checked value', async () => { JSON.stringify({ json_metadata: JSON.stringify({ ...initialState.dashboardInfo.metadata, - filter_bar_location: 'HORIZONTAL', + filter_bar_orientation: 'HORIZONTAL', }), }), ), diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/index.tsx similarity index 63% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/index.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/index.tsx index 82d4ba92e6ec0..70d7075c6e60f 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarLocationSelect/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/index.tsx @@ -21,60 +21,62 @@ import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { t, useTheme } from '@superset-ui/core'; import { MenuProps } from 'src/components/Menu'; -import { FilterBarLocation, RootState } from 'src/dashboard/types'; -import { saveFilterBarLocation } from 'src/dashboard/actions/dashboardInfo'; +import { FilterBarOrientation, RootState } from 'src/dashboard/types'; +import { saveFilterBarOrientation } from 'src/dashboard/actions/dashboardInfo'; import Icons from 'src/components/Icons'; import DropdownSelectableIcon from 'src/components/DropdownSelectableIcon'; -export const FilterBarLocationSelect = () => { +const FilterBarOrientationSelect = () => { const dispatch = useDispatch(); const theme = useTheme(); - const filterBarLocation = useSelector( - ({ dashboardInfo }) => dashboardInfo.filterBarLocation, + const filterBarOrientation = useSelector( + ({ dashboardInfo }) => dashboardInfo.filterBarOrientation, ); - const [selectedFilterBarLocation, setSelectedFilterBarLocation] = - useState(filterBarLocation); + const [selectedFilterBarOrientation, setSelectedFilterBarOrientation] = + useState(filterBarOrientation); - const toggleFilterBarLocation = useCallback( + const toggleFilterBarOrientation = useCallback( async ( selection: Parameters< Required>['onSelect'] >[0], ) => { - const selectedKey = selection.key as FilterBarLocation; - if (selectedKey !== filterBarLocation) { + const selectedKey = selection.key as FilterBarOrientation; + if (selectedKey !== filterBarOrientation) { // set displayed selection in local state for immediate visual response after clicking - setSelectedFilterBarLocation(selectedKey); + setSelectedFilterBarOrientation(selectedKey); try { // save selection in Redux and backend await dispatch( - saveFilterBarLocation(selection.key as FilterBarLocation), + saveFilterBarOrientation(selection.key as FilterBarOrientation), ); } catch { // revert local state in case of error when saving - setSelectedFilterBarLocation(filterBarLocation); + setSelectedFilterBarOrientation(filterBarOrientation); } } }, - [dispatch, filterBarLocation], + [dispatch, filterBarOrientation], ); return ( } menuItems={[ { - key: FilterBarLocation.VERTICAL, + key: FilterBarOrientation.VERTICAL, label: t('Vertical (Left)'), }, { - key: FilterBarLocation.HORIZONTAL, + key: FilterBarOrientation.HORIZONTAL, label: t('Horizontal (Top)'), }, ]} - selectedKeys={[selectedFilterBarLocation]} + selectedKeys={[selectedFilterBarOrientation]} /> ); }; + +export default FilterBarOrientationSelect; 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 e99171c7e839c..8319c3c9fb480 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 986572c7f0cbd..082111b94ab29 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 689eaa826627d..c4807d751fd11 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 53ea1c94c5289..14e0c88b7ed9c 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/FilterSets.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.test.tsx index 78ad4599933d3..2fe855147ab60 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.test.tsx @@ -21,7 +21,7 @@ import { render, screen } from 'spec/helpers/testing-library'; import { mockStore } from 'spec/fixtures/mockStore'; import { Provider } from 'react-redux'; import FilterSets, { FilterSetsProps } from '.'; -import { TabIds } from '../utils'; +import { TabIds } from '../types'; const createProps = () => ({ disabled: false, 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 5a7bff6527f37..5982bf515a6eb 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 df6c6ee44ebf0..7847927ff5b86 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 5fdfd481719ea..2e12425de75b8 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/index.tsx @@ -40,8 +40,8 @@ import { findExistingFilterSet } from './utils'; import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state'; import Footer from './Footer'; import FilterSetUnit from './FilterSetUnit'; -import { getFilterBarTestId } from '..'; -import { TabIds } from '../utils'; +import { getFilterBarTestId } from '../utils'; +import { TabIds } from '../types'; const FilterSetsWrapper = styled.div` display: grid; 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 a20d6a61f75c2..5057bef9b143b 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 FilterBarOrientationSelect from '../FilterBarOrientationSelect'; const TitleArea = styled.h4` display: flex; @@ -56,8 +56,13 @@ const HeaderButton = styled(Button)` `; const Wrapper = styled.div` - padding: ${({ theme }) => theme.gridUnit}px - ${({ theme }) => theme.gridUnit * 2}px; + ${({ theme }) => ` + padding: ${theme.gridUnit}px ${theme.gridUnit * 2}px; + + .ant-dropdown-trigger span { + padding-right: ${theme.gridUnit * 2}px; + } + `} `; type HeaderProps = { @@ -100,7 +105,7 @@ const Header: FC = ({ toggleFiltersBar }) => { {t('Filters')} - {canSetHorizontalFilterBar && } + {canSetHorizontalFilterBar && } ` + padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 2}px; + background: ${theme.colors.grayscale.light5}; + box-shadow: inset 0px -2px 2px -1px ${theme.colors.grayscale.light2}; + `} +`; + +const HorizontalBarContent = styled.div` + ${({ theme }) => ` + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + padding: 0 ${theme.gridUnit * 2}px; + line-height: 0; + + .loading { + margin: ${theme.gridUnit * 2}px auto ${theme.gridUnit * 2}px; + padding: 0; + } + `} +`; + +const FilterBarEmptyStateContainer = styled.div` + ${({ theme }) => ` + margin: 0 ${theme.gridUnit * 2}px 0 ${theme.gridUnit * 4}px; + font-weight: ${theme.typography.weights.bold}; + color: ${theme.colors.grayscale.base}; + font-size: ${theme.typography.sizes.s}px; + `} +`; + +const FiltersLinkContainer = styled.div<{ hasFilters: boolean }>` + ${({ theme, hasFilters }) => ` + padding: 0 ${theme.gridUnit * 2}px; + border-right: ${ + hasFilters ? `1px solid ${theme.colors.grayscale.light2}` : 0 + }; + + button { + display: flex; + align-items: center; + text-transform: capitalize; + font-weight: ${theme.typography.weights.normal}; + color: ${theme.colors.primary.base}; + > .anticon { + height: 24px; + padding-right: ${theme.gridUnit * 2}px; + } + > .anticon + span, > .anticon { + margin-right: 0; + margin-left: 0; + } + } + `} +`; + +const HorizontalFilterBar: React.FC = ({ + actions, + canEdit, + dashboardId, + dataMaskSelected, + filterValues, + isInitialized, + directPathToChild, + onSelectionChange, +}) => { + const hasFilters = filterValues.length > 0; + + return ( + + + {!isInitialized ? ( + + ) : ( + <> + {canEdit && } + {!hasFilters && ( + + {t('No filters are currently added to this dashboard.')} + + )} + {canEdit && ( + + + {t('Add/Edit Filters')} + + + )} + {hasFilters && ( + + )} + {actions} + + )} + + + ); +}; +export default React.memo(HorizontalFilterBar); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/HorizontalFilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/HorizontalFilterBar.test.tsx new file mode 100644 index 0000000000000..4116aef6b7985 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/HorizontalFilterBar.test.tsx @@ -0,0 +1,105 @@ +/** + * 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 { NativeFilterType } from '@superset-ui/core'; +import React from 'react'; +import { render, screen, waitFor } from 'spec/helpers/testing-library'; +import HorizontalBar from './Horizontal'; + +const defaultProps = { + actions: null, + canEdit: true, + dashboardId: 1, + dataMaskSelected: {}, + filterValues: [], + isInitialized: true, + onSelectionChange: jest.fn(), +}; + +const renderWrapper = (overrideProps?: Record) => + waitFor(() => + render(, { + useRedux: true, + }), + ); + +test('should render', async () => { + const { container } = await renderWrapper(); + expect(container).toBeInTheDocument(); +}); + +test('should not render the empty message', async () => { + await renderWrapper({ + filterValues: [ + { + id: 'test', + type: NativeFilterType.NATIVE_FILTER, + }, + ], + }); + expect( + screen.queryByText('No filters are currently added to this dashboard.'), + ).not.toBeInTheDocument(); +}); + +test('should render the empty message', async () => { + await renderWrapper(); + expect( + screen.getByText('No filters are currently added to this dashboard.'), + ).toBeInTheDocument(); +}); + +test('should render the gear icon', async () => { + await renderWrapper(); + expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument(); +}); + +test('should not render the gear icon', async () => { + await renderWrapper({ + canEdit: false, + }); + + expect(screen.queryByRole('img', { name: 'gear' })).not.toBeInTheDocument(); +}); + +test('should not render the loading icon', async () => { + await renderWrapper(); + expect( + screen.queryByRole('status', { name: 'Loading' }), + ).not.toBeInTheDocument(); +}); + +test('should render the loading icon', async () => { + await renderWrapper({ + isInitialized: false, + }); + expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument(); +}); + +test('should render Add/Edit Filters', async () => { + await renderWrapper(); + expect(screen.getByText('Add/Edit Filters')).toBeInTheDocument(); +}); + +test('should not render Add/Edit Filters', async () => { + await renderWrapper({ + canEdit: false, + }); + expect(screen.queryByText('Add/Edit Filters')).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx new file mode 100644 index 0000000000000..258489cc2a2de --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -0,0 +1,306 @@ +/** + * 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 throttle from 'lodash/throttle'; +import React, { + useEffect, + useState, + useCallback, + useMemo, + useRef, + createContext, +} from 'react'; +import cx from 'classnames'; +import { HandlerFunction, styled, t, isNativeFilter } from '@superset-ui/core'; +import Icons from 'src/components/Icons'; +import { AntdTabs } from 'src/components'; +import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; +import Loading from 'src/components/Loading'; +import { EmptyStateSmall } from 'src/components/EmptyState'; +import { getFilterBarTestId } from './utils'; +import { TabIds, VerticalBarProps } from './types'; +import FilterSets from './FilterSets'; +import { useFilterSets } from './state'; +import EditSection from './FilterSets/EditSection'; +import Header from './Header'; +import FilterControls from './FilterControls/FilterControls'; + +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 }>` + ${({ theme, width }) => ` + & .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}px; + background: ${theme.colors.grayscale.light5}; + border-right: 1px solid ${theme.colors.grayscale.light2}; + border-bottom: 1px solid ${theme.colors.grayscale.light2}; + min-height: 100%; + display: none; + &.open { + display: flex; + } + `} +`; + +const CollapsedBar = styled.div<{ offset: number }>` + ${({ theme, offset }) => ` + position: absolute; + top: ${offset}px; + left: 0; + height: 100%; + width: ${theme.gridUnit * 8}px; + padding-top: ${theme.gridUnit * 2}px; + display: none; + text-align: center; + &.open { + display: flex; + flex-direction: column; + align-items: center; + padding: ${theme.gridUnit * 2}px; + } + svg { + cursor: pointer; + } + `} +`; + +const StyledCollapseIcon = styled(Icons.Collapse)` + ${({ theme }) => ` + color: ${theme.colors.primary.base}; + margin-bottom: ${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 const FilterBarScrollContext = createContext(false); +const VerticalFilterBar: React.FC = ({ + actions, + canEdit, + dataMaskSelected, + directPathToChild, + filtersOpen, + filterValues, + height, + isDisabled, + isInitialized, + offset, + onSelectionChange, + toggleFiltersBar, + width, +}) => { + const [editFilterSetId, setEditFilterSetId] = useState(null); + const filterSets = useFilterSets(); + const filterSetFilterValues = Object.values(filterSets); + const [tab, setTab] = useState(TabIds.AllFilters); + const nativeFilterValues = filterValues.filter(isNativeFilter); + const [isScrolling, setIsScrolling] = useState(false); + const timeout = useRef(); + + const openFiltersBar = useCallback( + () => toggleFiltersBar(true), + [toggleFiltersBar], + ); + + const onScroll = useMemo( + () => + throttle(() => { + clearTimeout(timeout.current); + setIsScrolling(true); + timeout.current = setTimeout(() => { + setIsScrolling(false); + }, 300); + }, 200), + [], + ); + + useEffect(() => { + document.onscroll = onScroll; + return () => { + document.onscroll = null; + }; + }, [onScroll]); + + const tabPaneStyle = useMemo( + () => ({ overflow: 'auto', height, overscrollBehavior: 'contain' }), + [height], + ); + + const numberOfFilters = nativeFilterValues.length; + + return ( + + + + + + + +
+ {!isInitialized ? ( +
+ +
+ ) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? ( + + + {editFilterSetId && ( + setEditFilterSetId(null)} + filterSetId={editFilterSetId} + /> + )} + {filterValues.length === 0 ? ( + + + + ) : ( + + )} + + + + + + ) : ( +
+ {filterValues.length === 0 ? ( + + + + ) : ( + + )} +
+ )} + {actions} + + + + ); +}; +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 1b13f0583a9c4..2905c6a075acf 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx @@ -18,152 +18,38 @@ */ /* 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, 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 { FilterBarOrientation, RootState } from 'src/dashboard/types'; +import { checkIsApplyDisabled } from './utils'; +import { FiltersBarProps } from './types'; 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; -} +import ActionButtons from './ActionButtons'; +import Horizontal from './Horizontal'; +import Vertical from './Vertical'; const EXCLUDED_URL_PARAMS: string[] = [ URL_PARAMS.nativeFilters.name, @@ -225,29 +111,22 @@ const publishDataMask = debounce( export const FilterBarScrollContext = createContext(false); const FilterBar: React.FC = ({ - filtersOpen, - toggleFiltersBar, directPathToChild, - width, - height, - offset, + orientation = FilterBarOrientation.VERTICAL, + verticalConfig, }) => { const history = useHistory(); const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask(); - const [editFilterSetId, setEditFilterSetId] = useState(null); const [dataMaskSelected, setDataMaskSelected] = useImmer(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( + const dashboardId = useSelector( ({ dashboardInfo }) => dashboardInfo?.id, ); const previousDashboardId = usePrevious(dashboardId); @@ -255,9 +134,6 @@ const FilterBar: React.FC = ({ ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ); - const [isScrolling, setIsScrolling] = useState(false); - const timeout = useRef(); - const handleFilterSelectionChange = useCallback( ( filter: Pick & Partial, @@ -352,29 +228,6 @@ const FilterBar: React.FC = ({ }); }, [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 +235,46 @@ const FilterBar: React.FC = ({ nativeFilterValues, ); const isInitialized = useInitialization(); - const tabPaneStyle = useMemo( - () => ({ overflow: 'auto', height, overscrollBehavior: 'contain' }), - [height], - ); - const numberOfFilters = nativeFilterValues.length; - - return ( - - - - - - - -
- {!isInitialized ? ( -
- -
- ) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? ( - - - {editFilterSetId && ( - setEditFilterSetId(null)} - filterSetId={editFilterSetId} - /> - )} - {filterValues.length === 0 ? ( - - - - ) : ( - - )} - - - - - - ) : ( -
- {filterValues.length === 0 ? ( - - - - ) : ( - - )} -
- )} - - - - + const actions = ( + ); + + return orientation === FilterBarOrientation.HORIZONTAL ? ( + + ) : verticalConfig ? ( + + ) : null; }; export default React.memo(FilterBar); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts new file mode 100644 index 0000000000000..ae1368eff83d6 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts @@ -0,0 +1,67 @@ +/** + * 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 { + DataMask, + DataMaskStateWithId, + Divider, + Filter, +} from '@superset-ui/core'; +import { FilterBarOrientation } from 'src/dashboard/types'; + +interface CommonFiltersBarProps { + actions: React.ReactNode; + canEdit: boolean; + dataMaskSelected: DataMaskStateWithId; + directPathToChild?: string[]; + filterValues: (Filter | Divider)[]; + isInitialized: boolean; + onSelectionChange: ( + filter: Pick & Partial, + dataMask: Partial, + ) => void; +} + +interface VerticalBarConfig { + filtersOpen: boolean; + height: number | string; + offset: number; + toggleFiltersBar: any; + width: number; +} + +export interface FiltersBarProps + extends Pick { + orientation: FilterBarOrientation; + verticalConfig?: VerticalBarConfig; +} + +export type HorizontalBarProps = CommonFiltersBarProps & { + dashboardId: number; +}; + +export type VerticalBarProps = Omit & + CommonFiltersBarProps & + VerticalBarConfig & { + isDisabled: boolean; + }; + +export enum TabIds { + AllFilters = 'allFilters', + FilterSets = 'filterSets', +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts index 842bb440542c3..9aaa02fc54eac 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts @@ -19,11 +19,7 @@ import { areObjectsEqual } from 'src/reduxUtils'; import { DataMaskStateWithId, Filter, FilterState } from '@superset-ui/core'; - -export enum TabIds { - AllFilters = 'allFilters', - FilterSets = 'filterSets', -} +import { testWithId } from 'src/utils/testUtils'; export const getOnlyExtraFormData = (data: DataMaskStateWithId) => Object.values(data).reduce( @@ -65,3 +61,6 @@ export const checkIsApplyDisabled = ( ) ); }; + +export const FILTER_BAR_TEST_ID = 'filter-bar'; +export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID); diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js index 8a01e6122c779..030fd60250569 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -19,7 +19,7 @@ import { DASHBOARD_INFO_UPDATED, - SET_FILTER_BAR_LOCATION, + SET_FILTER_BAR_ORIENTATION, } from '../actions/dashboardInfo'; import { HYDRATE_DASHBOARD } from '../actions/hydrate'; @@ -38,10 +38,10 @@ export default function dashboardStateReducer(state = {}, action) { ...action.data.dashboardInfo, // set async api call data }; - case SET_FILTER_BAR_LOCATION: + case SET_FILTER_BAR_ORIENTATION: return { ...state, - filterBarLocation: action.filterBarLocation, + filterBarOrientation: action.filterBarOrientation, }; default: return state; diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index b809f405ac026..1bdd1c14a1c7a 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -52,7 +52,7 @@ export type Chart = ChartState & { }; }; -export enum FilterBarLocation { +export enum FilterBarOrientation { VERTICAL = 'VERTICAL', HORIZONTAL = 'HORIZONTAL', } @@ -108,7 +108,7 @@ export type DashboardInfo = { label_colors: JsonObject; shared_label_colors: JsonObject; }; - filterBarLocation: FilterBarLocation; + filterBarOrientation: FilterBarOrientation; }; export type ChartsState = { [key: string]: Chart }; diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py index e6db0b5688b6e..d3a9444980966 100644 --- a/superset/dashboards/schemas.py +++ b/superset/dashboards/schemas.py @@ -133,7 +133,7 @@ class DashboardJSONMetadataSchema(Schema): # used for v0 import/export import_time = fields.Integer() remote_id = fields.Integer() - filter_bar_location = fields.Str(allow_none=True) + filter_bar_orientation = fields.Str(allow_none=True) class UserSchema(Schema):