diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx index b153d32cd5288..51c119653bab9 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx @@ -25,6 +25,7 @@ import { Provider } from 'react-redux'; import { mockStore } from 'spec/fixtures/mockStore'; import { styledMount as mount } from 'spec/helpers/theming'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; +import { Dropdown, Menu } from 'src/common/components'; import Alert from 'src/components/Alert'; import { FiltersConfigModal } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal'; @@ -60,7 +61,7 @@ jest.mock('@superset-ui/core', () => ({ describe('FiltersConfigModal', () => { const mockedProps = { isOpen: true, - initialFilterId: 'DefaultsID', + initialFilterId: 'NATIVE_FILTER-1', createNewOnOpen: true, onCancel: jest.fn(), onSave: jest.fn(), @@ -112,9 +113,13 @@ describe('FiltersConfigModal', () => { await waitForComponentToPaint(wrapper); } - function addFilter() { + async function addFilter() { act(() => { - wrapper.find('[aria-label="Add filter"]').at(0).simulate('click'); + wrapper.find(Dropdown).at(0).simulate('mouseEnter'); + }); + await waitForComponentToPaint(wrapper, 300); + act(() => { + wrapper.find(Menu.Item).at(0).simulate('click'); }); } @@ -124,7 +129,7 @@ describe('FiltersConfigModal', () => { }); it('shows correct alert message for unsaved filters', async () => { - addFilter(); + await addFilter(); await clickCancel(); expect(onCancel.mock.calls).toHaveLength(0); expect(wrapper.find(Alert).text()).toContain( diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx index 511b643d9d8dd..cce9bf7a04718 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx @@ -36,6 +36,7 @@ import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters'; import findTabIndexByComponentId from '../../util/findTabIndexByComponentId'; import { findTabsWithChartsInScope } from '../nativeFilters/utils'; import { setInScopeStatusOfFilters } from '../../actions/nativeFilters'; +import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils'; type DashboardContainerProps = { topLevelTabs?: LayoutItem; @@ -71,6 +72,7 @@ const DashboardContainer: FC = ({ topLevelTabs }) => { const filterScopes = Object.values(nativeFilters ?? {}).map(filter => ({ id: filter.id, scope: filter.scope, + type: filter.type, })); useEffect(() => { if ( @@ -80,6 +82,13 @@ const DashboardContainer: FC = ({ topLevelTabs }) => { return; } const scopes = filterScopes.map(filterScope => { + if (filterScope.id.startsWith(NATIVE_FILTER_DIVIDER_PREFIX)) { + return { + filterId: filterScope.id, + tabsInScope: [], + chartsInScope: [], + }; + } const { scope } = filterScope; const chartsInScope: number[] = getChartIdsInFilterScope({ filterScope: { diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts index ea850ae2146b7..695e2f94bc790 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts @@ -29,6 +29,7 @@ import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types'; import { areObjectsEqual } from 'src/reduxUtils'; import { Layout } from '../../types'; import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils'; +import { NativeFilterType } from '../nativeFilters/types'; export enum IndicatorStatus { Unset = 'UNSET', @@ -268,11 +269,13 @@ export const selectNativeIndicatorsForChart = ( nativeFilterIndicators = nativeFilters && Object.values(nativeFilters) - .filter(nativeFilter => - getTreeCheckedItems(nativeFilter.scope, dashboardLayout).some( - layoutItem => - dashboardLayout[layoutItem]?.meta?.chartId === chartId, - ), + .filter( + nativeFilter => + nativeFilter.type === NativeFilterType.NATIVE_FILTER && + getTreeCheckedItems(nativeFilter.scope, dashboardLayout).some( + layoutItem => + dashboardLayout[layoutItem]?.meta?.chartId === chartId, + ), ) .map(nativeFilter => { const column = nativeFilter.targets[0]?.column?.name; 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 90ff4ef86fada..9be4a35cba096 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx @@ -88,12 +88,12 @@ const addFilterFlow = async () => { userEvent.click(screen.getByText('Time range')); userEvent.type(screen.getByTestId(getModalTestId('name-input')), FILTER_NAME); userEvent.click(screen.getByText('Save')); - await screen.findByText('All Filters (1)'); + await screen.findByText('All filters (1)'); }; const addFilterSetFlow = async () => { // add filter set - userEvent.click(screen.getByText('Filter Sets (0)')); + userEvent.click(screen.getByText('Filter sets (0)')); // check description expect(screen.getByText('Filters (1)')).toBeInTheDocument(); @@ -301,6 +301,40 @@ describe('FilterBar', () => { expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); }); + it('renders dividers', async () => { + const divider = { + id: 'NATIVE_FILTER_DIVIDER-1', + type: 'DIVIDER', + scope: { + rootPath: ['ROOT_ID'], + excluded: [], + }, + title: 'Select time range', + description: 'Select year/month etc..', + chartsInScope: [], + tabsInScope: [], + }; + const stateWithDivider = { + ...stateWithoutNativeFilters, + nativeFilters: { + filters: { + 'NATIVE_FILTER_DIVIDER-1': divider, + }, + }, + }; + + renderWrapper(openedBarProps, stateWithDivider); + + const title = await screen.findByText('Select time range'); + const description = await screen.findByText('Select year/month etc..'); + + expect(title.tagName).toBe('H3'); + expect(description.tagName).toBe('P'); + // Do not enable buttons if there are not filters + expect(screen.getByTestId(getTestId('clear-button'))).toBeDisabled(); + expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); + }); + it('create filter and apply it flow', async () => { // @ts-ignore global.featureFlags = { @@ -332,7 +366,7 @@ describe('FilterBar', () => { // change filter expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); - userEvent.click(await screen.findByText('All Filters (1)')); + userEvent.click(await screen.findByText('All filters (1)')); await changeFilterValue(); await waitFor(() => expect(screen.getAllByText('Last day').length).toBe(2)); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx index 99331f97c5c14..19f18a1e6839a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -30,7 +30,10 @@ import { useDashboardHasTabs, useSelectFiltersInScope, } from 'src/dashboard/components/nativeFilters/state'; -import { Filter } from 'src/dashboard/components/nativeFilters/types'; +import { + Filter, + NativeFilterType, +} from 'src/dashboard/components/nativeFilters/types'; import CascadePopover from '../CascadeFilters/CascadePopover'; import { useFilters } from '../state'; import { buildCascadeFiltersTree } from './utils'; @@ -79,21 +82,32 @@ const FilterControls: FC = ({ const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0; const cascadePopoverFactory = useCallback( - index => ( - - setVisiblePopoverId(visible ? cascadeFilters[index].id : null) - } - filter={cascadeFilters[index]} - onFilterSelectionChange={onFilterSelectionChange} - directPathToChild={directPathToChild} - inView={false} - /> - ), + index => { + const filter = cascadeFilters[index]; + if (filter.type === NativeFilterType.DIVIDER) { + return ( +
+

{filter.title}

+

{filter.description}

+
+ ); + } + return ( + + setVisiblePopoverId(visible ? filter.id : null) + } + filter={filter} + onFilterSelectionChange={onFilterSelectionChange} + directPathToChild={directPathToChild} + inView={false} + /> + ); + }, [ cascadeFilters, JSON.stringify(dataMaskSelected), diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts index 0b6d9a78f3ef5..12ca897a3072b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/utils.ts @@ -22,12 +22,14 @@ import { setFocusedNativeFilter, unsetFocusedNativeFilter, } from 'src/dashboard/actions/nativeFilters'; -import { Filter } from '../../types'; +import { Filter, NativeFilterType, Divider } from '../../types'; import { CascadeFilter } from '../CascadeFilters/types'; import { mapParentFiltersToChildren } from '../utils'; // eslint-disable-next-line import/prefer-default-export -export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { +export function buildCascadeFiltersTree( + filters: Array, +): Array { const cascadeChildren = mapParentFiltersToChildren(filters); const getCascadeFilter = (filter: Filter): CascadeFilter => { @@ -39,7 +41,11 @@ export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { }; return filters - .filter(filter => !filter.cascadeParentIds?.length) + .filter( + filter => + filter.type === NativeFilterType.DIVIDER || + !(filter as Filter).cascadeParentIds?.length, + ) .map(getCascadeFilter); } 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 cd451e9f621c7..c263da6a69f08 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FiltersHeader.tsx @@ -26,6 +26,7 @@ import { FilterSet } from 'src/dashboard/reducers/types'; import { getFilterValueForDisplay } from './utils'; import { useFilters } from '../state'; import { getFilterBarTestId } from '../index'; +import { NativeFilterType } from '../../types'; const FilterHeader = styled.div` display: flex; @@ -68,7 +69,9 @@ export type FiltersHeaderProps = { const FiltersHeader: FC = ({ dataMask, filterSet }) => { const theme = useTheme(); const filters = useFilters(); - const filterValues = Object.values(filters); + const filterValues = Object.values(filters).filter( + nativeFilter => nativeFilter.type === NativeFilterType.NATIVE_FILTER, + ); let resultFilters = filterValues ?? []; if (filterSet?.nativeFilters) { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx index 9a8a9fde605b3..2427821d41a7c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx @@ -33,7 +33,10 @@ import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types'; import { useImmer } from 'use-immer'; import { isEmpty, isEqual } from 'lodash'; import { testWithId } from 'src/utils/testUtils'; -import { Filter } from 'src/dashboard/components/nativeFilters/types'; +import { + Filter, + NativeFilterType, +} from 'src/dashboard/components/nativeFilters/types'; import Loading from 'src/components/Loading'; import { getInitialDataMask } from 'src/dataMask/reducer'; import { URL_PARAMS } from 'src/constants'; @@ -82,7 +85,6 @@ const Bar = styled.div<{ width: number }>` border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; min-height: 100%; display: none; - &.open { display: flex; } @@ -97,14 +99,12 @@ const CollapsedBar = styled.div<{ offset: number }>` 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; } @@ -278,9 +278,12 @@ const FilterBar: React.FC = ({ filterValues, ); const isInitialized = useInitialization(); - const tabPaneStyle = useMemo(() => ({ overflow: 'auto', height }), [height]); + const numberOfFilters = filterValues.filter( + filterValue => filterValue.type === NativeFilterType.NATIVE_FILTER, + ).length; + return ( = ({ activeKey={editFilterSetId ? TabIds.AllFilters : undefined} > = ({ ): { [id: string]: Filter[]; } { const cascadeChildren = {}; filters.forEach(filter => { - const [parentId] = filter.cascadeParentIds || []; + const [parentId] = + ('cascadeParentIds' in filter && filter.cascadeParentIds) || []; if (parentId) { if (!cascadeChildren[parentId]) { cascadeChildren[parentId] = []; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/DividerConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/DividerConfigForm.tsx new file mode 100644 index 0000000000000..7ab2b100206f6 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/DividerConfigForm.tsx @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { FormItem } from 'src/components/Form'; +import { Input, TextArea } from 'src/common/components'; +import { styled, t } from '@superset-ui/core'; +import { NativeFilterType } from '../types'; + +interface Props { + componentId: string; + divider?: { + title: string; + description: string; + }; +} +const Container = styled.div` + ${({ theme }) => ` + padding: ${theme.gridUnit * 4}px; + `} +`; + +const DividerConfigForm: React.FC = ({ componentId, divider }) => ( + + + + + +