feat(dashboard): Add divider component in native filters (#17410)

* Tests are working, type errors are fixed

* Fix filterset

* add license header to the new file

* fix test

* PR comments

* Linting

* test fix

* small fix
This commit is contained in:
Ajay M 2021-11-23 16:13:56 -08:00 committed by GitHub
parent c216565190
commit 9576478a5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 473 additions and 201 deletions

View File

@ -25,6 +25,7 @@ import { Provider } from 'react-redux';
import { mockStore } from 'spec/fixtures/mockStore'; import { mockStore } from 'spec/fixtures/mockStore';
import { styledMount as mount } from 'spec/helpers/theming'; import { styledMount as mount } from 'spec/helpers/theming';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { Dropdown, Menu } from 'src/common/components';
import Alert from 'src/components/Alert'; import Alert from 'src/components/Alert';
import { FiltersConfigModal } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal'; import { FiltersConfigModal } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
@ -60,7 +61,7 @@ jest.mock('@superset-ui/core', () => ({
describe('FiltersConfigModal', () => { describe('FiltersConfigModal', () => {
const mockedProps = { const mockedProps = {
isOpen: true, isOpen: true,
initialFilterId: 'DefaultsID', initialFilterId: 'NATIVE_FILTER-1',
createNewOnOpen: true, createNewOnOpen: true,
onCancel: jest.fn(), onCancel: jest.fn(),
onSave: jest.fn(), onSave: jest.fn(),
@ -112,9 +113,13 @@ describe('FiltersConfigModal', () => {
await waitForComponentToPaint(wrapper); await waitForComponentToPaint(wrapper);
} }
function addFilter() { async function addFilter() {
act(() => { 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 () => { it('shows correct alert message for unsaved filters', async () => {
addFilter(); await addFilter();
await clickCancel(); await clickCancel();
expect(onCancel.mock.calls).toHaveLength(0); expect(onCancel.mock.calls).toHaveLength(0);
expect(wrapper.find(Alert).text()).toContain( expect(wrapper.find(Alert).text()).toContain(

View File

@ -36,6 +36,7 @@ import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters';
import findTabIndexByComponentId from '../../util/findTabIndexByComponentId'; import findTabIndexByComponentId from '../../util/findTabIndexByComponentId';
import { findTabsWithChartsInScope } from '../nativeFilters/utils'; import { findTabsWithChartsInScope } from '../nativeFilters/utils';
import { setInScopeStatusOfFilters } from '../../actions/nativeFilters'; import { setInScopeStatusOfFilters } from '../../actions/nativeFilters';
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
type DashboardContainerProps = { type DashboardContainerProps = {
topLevelTabs?: LayoutItem; topLevelTabs?: LayoutItem;
@ -71,6 +72,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const filterScopes = Object.values(nativeFilters ?? {}).map(filter => ({ const filterScopes = Object.values(nativeFilters ?? {}).map(filter => ({
id: filter.id, id: filter.id,
scope: filter.scope, scope: filter.scope,
type: filter.type,
})); }));
useEffect(() => { useEffect(() => {
if ( if (
@ -80,6 +82,13 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
return; return;
} }
const scopes = filterScopes.map(filterScope => { const scopes = filterScopes.map(filterScope => {
if (filterScope.id.startsWith(NATIVE_FILTER_DIVIDER_PREFIX)) {
return {
filterId: filterScope.id,
tabsInScope: [],
chartsInScope: [],
};
}
const { scope } = filterScope; const { scope } = filterScope;
const chartsInScope: number[] = getChartIdsInFilterScope({ const chartsInScope: number[] = getChartIdsInFilterScope({
filterScope: { filterScope: {

View File

@ -29,6 +29,7 @@ import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
import { areObjectsEqual } from 'src/reduxUtils'; import { areObjectsEqual } from 'src/reduxUtils';
import { Layout } from '../../types'; import { Layout } from '../../types';
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils'; import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
import { NativeFilterType } from '../nativeFilters/types';
export enum IndicatorStatus { export enum IndicatorStatus {
Unset = 'UNSET', Unset = 'UNSET',
@ -268,11 +269,13 @@ export const selectNativeIndicatorsForChart = (
nativeFilterIndicators = nativeFilterIndicators =
nativeFilters && nativeFilters &&
Object.values(nativeFilters) Object.values(nativeFilters)
.filter(nativeFilter => .filter(
getTreeCheckedItems(nativeFilter.scope, dashboardLayout).some( nativeFilter =>
layoutItem => nativeFilter.type === NativeFilterType.NATIVE_FILTER &&
dashboardLayout[layoutItem]?.meta?.chartId === chartId, getTreeCheckedItems(nativeFilter.scope, dashboardLayout).some(
), layoutItem =>
dashboardLayout[layoutItem]?.meta?.chartId === chartId,
),
) )
.map(nativeFilter => { .map(nativeFilter => {
const column = nativeFilter.targets[0]?.column?.name; const column = nativeFilter.targets[0]?.column?.name;

View File

@ -88,12 +88,12 @@ const addFilterFlow = async () => {
userEvent.click(screen.getByText('Time range')); userEvent.click(screen.getByText('Time range'));
userEvent.type(screen.getByTestId(getModalTestId('name-input')), FILTER_NAME); userEvent.type(screen.getByTestId(getModalTestId('name-input')), FILTER_NAME);
userEvent.click(screen.getByText('Save')); userEvent.click(screen.getByText('Save'));
await screen.findByText('All Filters (1)'); await screen.findByText('All filters (1)');
}; };
const addFilterSetFlow = async () => { const addFilterSetFlow = async () => {
// add filter set // add filter set
userEvent.click(screen.getByText('Filter Sets (0)')); userEvent.click(screen.getByText('Filter sets (0)'));
// check description // check description
expect(screen.getByText('Filters (1)')).toBeInTheDocument(); expect(screen.getByText('Filters (1)')).toBeInTheDocument();
@ -301,6 +301,40 @@ describe('FilterBar', () => {
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); 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 () => { it('create filter and apply it flow', async () => {
// @ts-ignore // @ts-ignore
global.featureFlags = { global.featureFlags = {
@ -332,7 +366,7 @@ describe('FilterBar', () => {
// change filter // change filter
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); 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 changeFilterValue();
await waitFor(() => expect(screen.getAllByText('Last day').length).toBe(2)); await waitFor(() => expect(screen.getAllByText('Last day').length).toBe(2));

View File

@ -30,7 +30,10 @@ import {
useDashboardHasTabs, useDashboardHasTabs,
useSelectFiltersInScope, useSelectFiltersInScope,
} from 'src/dashboard/components/nativeFilters/state'; } 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 CascadePopover from '../CascadeFilters/CascadePopover';
import { useFilters } from '../state'; import { useFilters } from '../state';
import { buildCascadeFiltersTree } from './utils'; import { buildCascadeFiltersTree } from './utils';
@ -79,21 +82,32 @@ const FilterControls: FC<FilterControlsProps> = ({
const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0; const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0;
const cascadePopoverFactory = useCallback( const cascadePopoverFactory = useCallback(
index => ( index => {
<CascadePopover const filter = cascadeFilters[index];
data-test="cascade-filters-control" if (filter.type === NativeFilterType.DIVIDER) {
key={cascadeFilters[index].id} return (
dataMaskSelected={dataMaskSelected} <div>
visible={visiblePopoverId === cascadeFilters[index].id} <h3>{filter.title}</h3>
onVisibleChange={visible => <p>{filter.description}</p>
setVisiblePopoverId(visible ? cascadeFilters[index].id : null) </div>
} );
filter={cascadeFilters[index]} }
onFilterSelectionChange={onFilterSelectionChange} return (
directPathToChild={directPathToChild} <CascadePopover
inView={false} data-test="cascade-filters-control"
/> key={filter.id}
), dataMaskSelected={dataMaskSelected}
visible={visiblePopoverId === filter.id}
onVisibleChange={visible =>
setVisiblePopoverId(visible ? filter.id : null)
}
filter={filter}
onFilterSelectionChange={onFilterSelectionChange}
directPathToChild={directPathToChild}
inView={false}
/>
);
},
[ [
cascadeFilters, cascadeFilters,
JSON.stringify(dataMaskSelected), JSON.stringify(dataMaskSelected),

View File

@ -22,12 +22,14 @@ import {
setFocusedNativeFilter, setFocusedNativeFilter,
unsetFocusedNativeFilter, unsetFocusedNativeFilter,
} from 'src/dashboard/actions/nativeFilters'; } from 'src/dashboard/actions/nativeFilters';
import { Filter } from '../../types'; import { Filter, NativeFilterType, Divider } from '../../types';
import { CascadeFilter } from '../CascadeFilters/types'; import { CascadeFilter } from '../CascadeFilters/types';
import { mapParentFiltersToChildren } from '../utils'; import { mapParentFiltersToChildren } from '../utils';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { export function buildCascadeFiltersTree(
filters: Array<Divider | Filter>,
): Array<CascadeFilter | Divider> {
const cascadeChildren = mapParentFiltersToChildren(filters); const cascadeChildren = mapParentFiltersToChildren(filters);
const getCascadeFilter = (filter: Filter): CascadeFilter => { const getCascadeFilter = (filter: Filter): CascadeFilter => {
@ -39,7 +41,11 @@ export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] {
}; };
return filters return filters
.filter(filter => !filter.cascadeParentIds?.length) .filter(
filter =>
filter.type === NativeFilterType.DIVIDER ||
!(filter as Filter).cascadeParentIds?.length,
)
.map(getCascadeFilter); .map(getCascadeFilter);
} }

View File

@ -26,6 +26,7 @@ import { FilterSet } from 'src/dashboard/reducers/types';
import { getFilterValueForDisplay } from './utils'; import { getFilterValueForDisplay } from './utils';
import { useFilters } from '../state'; import { useFilters } from '../state';
import { getFilterBarTestId } from '../index'; import { getFilterBarTestId } from '../index';
import { NativeFilterType } from '../../types';
const FilterHeader = styled.div` const FilterHeader = styled.div`
display: flex; display: flex;
@ -68,7 +69,9 @@ export type FiltersHeaderProps = {
const FiltersHeader: FC<FiltersHeaderProps> = ({ dataMask, filterSet }) => { const FiltersHeader: FC<FiltersHeaderProps> = ({ dataMask, filterSet }) => {
const theme = useTheme(); const theme = useTheme();
const filters = useFilters(); const filters = useFilters();
const filterValues = Object.values(filters); const filterValues = Object.values(filters).filter(
nativeFilter => nativeFilter.type === NativeFilterType.NATIVE_FILTER,
);
let resultFilters = filterValues ?? []; let resultFilters = filterValues ?? [];
if (filterSet?.nativeFilters) { if (filterSet?.nativeFilters) {

View File

@ -33,7 +33,10 @@ import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types';
import { useImmer } from 'use-immer'; import { useImmer } from 'use-immer';
import { isEmpty, isEqual } from 'lodash'; import { isEmpty, isEqual } from 'lodash';
import { testWithId } from 'src/utils/testUtils'; 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 Loading from 'src/components/Loading';
import { getInitialDataMask } from 'src/dataMask/reducer'; import { getInitialDataMask } from 'src/dataMask/reducer';
import { URL_PARAMS } from 'src/constants'; 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}; border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
min-height: 100%; min-height: 100%;
display: none; display: none;
&.open { &.open {
display: flex; display: flex;
} }
@ -97,14 +99,12 @@ const CollapsedBar = styled.div<{ offset: number }>`
padding-top: ${({ theme }) => theme.gridUnit * 2}px; padding-top: ${({ theme }) => theme.gridUnit * 2}px;
display: none; display: none;
text-align: center; text-align: center;
&.open { &.open {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: ${({ theme }) => theme.gridUnit * 2}px; padding: ${({ theme }) => theme.gridUnit * 2}px;
} }
svg { svg {
cursor: pointer; cursor: pointer;
} }
@ -278,9 +278,12 @@ const FilterBar: React.FC<FiltersBarProps> = ({
filterValues, filterValues,
); );
const isInitialized = useInitialization(); const isInitialized = useInitialization();
const tabPaneStyle = useMemo(() => ({ overflow: 'auto', height }), [height]); const tabPaneStyle = useMemo(() => ({ overflow: 'auto', height }), [height]);
const numberOfFilters = filterValues.filter(
filterValue => filterValue.type === NativeFilterType.NATIVE_FILTER,
).length;
return ( return (
<BarWrapper <BarWrapper
{...getFilterBarTestId()} {...getFilterBarTestId()}
@ -320,8 +323,8 @@ const FilterBar: React.FC<FiltersBarProps> = ({
activeKey={editFilterSetId ? TabIds.AllFilters : undefined} activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
> >
<Tabs.TabPane <Tabs.TabPane
tab={t('All Filters (%(filterCount)d)', { tab={t('All filters (%(filterCount)d)', {
filterCount: filterValues.length, filterCount: numberOfFilters,
})} })}
key={TabIds.AllFilters} key={TabIds.AllFilters}
css={tabPaneStyle} css={tabPaneStyle}
@ -342,7 +345,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
disabled={!!editFilterSetId} disabled={!!editFilterSetId}
tab={t('Filter Sets (%(filterSetCount)d)', { tab={t('Filter sets (%(filterSetCount)d)', {
filterSetCount: filterSetFilterValues.length, filterSetCount: filterSetFilterValues.length,
})} })}
key={TabIds.FilterSets} key={TabIds.FilterSets}

View File

@ -20,19 +20,20 @@
import { DataMaskStateWithId } from 'src/dataMask/types'; import { DataMaskStateWithId } from 'src/dataMask/types';
import { areObjectsEqual } from 'src/reduxUtils'; import { areObjectsEqual } from 'src/reduxUtils';
import { FilterState } from '@superset-ui/core'; import { FilterState } from '@superset-ui/core';
import { Filter } from '../types'; import { Filter, Divider } from '../types';
export enum TabIds { export enum TabIds {
AllFilters = 'allFilters', AllFilters = 'allFilters',
FilterSets = 'filterSets', FilterSets = 'filterSets',
} }
export function mapParentFiltersToChildren(filters: Filter[]): { export function mapParentFiltersToChildren(filters: Array<Filter | Divider>): {
[id: string]: Filter[]; [id: string]: Filter[];
} { } {
const cascadeChildren = {}; const cascadeChildren = {};
filters.forEach(filter => { filters.forEach(filter => {
const [parentId] = filter.cascadeParentIds || []; const [parentId] =
('cascadeParentIds' in filter && filter.cascadeParentIds) || [];
if (parentId) { if (parentId) {
if (!cascadeChildren[parentId]) { if (!cascadeChildren[parentId]) {
cascadeChildren[parentId] = []; cascadeChildren[parentId] = [];

View File

@ -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<Props> = ({ componentId, divider }) => (
<Container>
<FormItem
initialValue={divider ? divider.title : ''}
label={t('Title')}
name={['filters', componentId, 'title']}
rules={[
{ required: true, message: t('Title is required'), whitespace: true },
]}
>
<Input />
</FormItem>
<FormItem
initialValue={divider ? divider.description : ''}
label={t('Description')}
name={['filters', componentId, 'description']}
>
<TextArea rows={4} />
</FormItem>
<FormItem
hidden
name={['filters', componentId, 'type']}
initialValue={NativeFilterType.DIVIDER}
/>
</Container>
);
export default DividerConfigForm;

View File

@ -26,7 +26,8 @@ const defaultProps = {
children: jest.fn(), children: jest.fn(),
getFilterTitle: (id: string) => id, getFilterTitle: (id: string) => id,
onChange: jest.fn(), onChange: jest.fn(),
onEdit: jest.fn(), onAdd: jest.fn(),
onRemove: jest.fn(),
onRearrange: jest.fn(), onRearrange: jest.fn(),
restoreFilter: jest.fn(), restoreFilter: jest.fn(),
currentFilterId: 'NATIVE_FILTER-1', currentFilterId: 'NATIVE_FILTER-1',
@ -93,22 +94,41 @@ test('remove filter', async () => {
}), }),
); );
}); });
expect(defaultProps.onEdit).toHaveBeenCalledWith('NATIVE_FILTER-2', 'remove'); expect(defaultProps.onRemove).toHaveBeenCalledWith('NATIVE_FILTER-2');
}); });
test('add filter', async () => { test('add filter', async () => {
defaultRender(); defaultRender();
// First trash icon // First trash icon
const removeFilterIcon = screen.getByText('Add filter')!; const addButton = screen.getByText('Add')!;
fireEvent.mouseOver(addButton);
const addFilterButton = await screen.findByText('Filter');
await act(async () => { await act(async () => {
fireEvent( fireEvent(
removeFilterIcon, addFilterButton,
new MouseEvent('click', { new MouseEvent('click', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
}), }),
); );
}); });
expect(defaultProps.onAdd).toHaveBeenCalledWith('NATIVE_FILTER');
expect(defaultProps.onEdit).toHaveBeenCalledWith('', 'add'); });
test('add divider', async () => {
defaultRender();
const addButton = screen.getByText('Add')!;
fireEvent.mouseOver(addButton);
const addFilterButton = await screen.findByText('Divider');
await act(async () => {
fireEvent(
addFilterButton,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
});
expect(defaultProps.onAdd).toHaveBeenCalledWith('DIVIDER');
}); });

View File

@ -18,6 +18,7 @@
*/ */
import { styled } from '@superset-ui/core'; import { styled } from '@superset-ui/core';
import React from 'react'; import React from 'react';
import { NativeFilterType } from '../types';
import FilterTitlePane from './FilterTitlePane'; import FilterTitlePane from './FilterTitlePane';
import { FilterRemoval } from './types'; import { FilterRemoval } from './types';
@ -25,7 +26,8 @@ interface Props {
children: (filterId: string) => React.ReactNode; children: (filterId: string) => React.ReactNode;
getFilterTitle: (filterId: string) => string; getFilterTitle: (filterId: string) => string;
onChange: (activeKey: string) => void; onChange: (activeKey: string) => void;
onEdit: (filterId: string, action: 'add' | 'remove') => void; onAdd: (type: NativeFilterType) => void;
onRemove: (id: string) => void;
onRearrange: (dragIndex: number, targetIndex: number) => void; onRearrange: (dragIndex: number, targetIndex: number) => void;
erroredFilters: string[]; erroredFilters: string[];
restoreFilter: (id: string) => void; restoreFilter: (id: string) => void;
@ -52,9 +54,10 @@ const TitlesContainer = styled.div`
const FiltureConfigurePane: React.FC<Props> = ({ const FiltureConfigurePane: React.FC<Props> = ({
getFilterTitle, getFilterTitle,
onChange, onChange,
onEdit, onRemove,
onRearrange, onRearrange,
restoreFilter, restoreFilter,
onAdd,
erroredFilters, erroredFilters,
children, children,
currentFilterId, currentFilterId,
@ -72,9 +75,9 @@ const FiltureConfigurePane: React.FC<Props> = ({
erroredFilters={erroredFilters} erroredFilters={erroredFilters}
getFilterTitle={getFilterTitle} getFilterTitle={getFilterTitle}
onChange={onChange} onChange={onChange}
onEdit={onEdit} onAdd={(type: NativeFilterType) => onAdd(type)}
onRearrage={onRearrange} onRearrage={onRearrange}
onRemove={(id: string) => onEdit(id, 'remove')} onRemove={(id: string) => onRemove(id)}
restoreFilter={restoreFilter} restoreFilter={restoreFilter}
/> />
</TitlesContainer> </TitlesContainer>

View File

@ -16,9 +16,10 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { PlusOutlined } from '@ant-design/icons';
import { styled, t, useTheme } from '@superset-ui/core'; import { styled, t, useTheme } from '@superset-ui/core';
import React from 'react'; import React from 'react';
import { Dropdown, MainNav as Menu } from 'src/common/components';
import { NativeFilterType } from '../types';
import FilterTitleContainer from './FilterTitleContainer'; import FilterTitleContainer from './FilterTitleContainer';
import { FilterRemoval } from './types'; import { FilterRemoval } from './types';
@ -28,13 +29,17 @@ interface Props {
onRearrage: (dragIndex: number, targetIndex: number) => void; onRearrage: (dragIndex: number, targetIndex: number) => void;
onRemove: (id: string) => void; onRemove: (id: string) => void;
onChange: (id: string) => void; onChange: (id: string) => void;
onEdit: (filterId: string, action: 'add' | 'remove') => void; onAdd: (type: NativeFilterType) => void;
removedFilters: Record<string, FilterRemoval>; removedFilters: Record<string, FilterRemoval>;
currentFilterId: string; currentFilterId: string;
filterGroups: string[][]; filterGroups: string[][];
erroredFilters: string[]; erroredFilters: string[];
} }
const StyledPlusButton = styled.div`
color: ${({ theme }) => theme.colors.primary.dark1};
`;
const StyledHeader = styled.div` const StyledHeader = styled.div`
${({ theme }) => ` ${({ theme }) => `
color: ${theme.colors.grayscale.dark1}; color: ${theme.colors.grayscale.dark1};
@ -46,27 +51,25 @@ const StyledHeader = styled.div`
`} `}
`; `;
const StyledAddBox = styled.div`
${({ theme }) => `
cursor: pointer;
margin: ${theme.gridUnit * 4}px;
&:hover {
color: ${theme.colors.primary.base};
}
`}
`;
const TabsContainer = styled.div` const TabsContainer = styled.div`
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`; `;
const StyledAddFilterBox = styled.div`
color: ${({ theme }) => theme.colors.primary.dark1};
padding: ${({ theme }) => theme.gridUnit * 2}px;
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
cursor: pointer;
margin-top: auto;
&:hover {
color: ${({ theme }) => theme.colors.primary.base};
}
`;
const FilterTitlePane: React.FC<Props> = ({ const FilterTitlePane: React.FC<Props> = ({
getFilterTitle, getFilterTitle,
onChange, onChange,
onEdit, onAdd,
onRemove, onRemove,
onRearrage, onRearrage,
restoreFilter, restoreFilter,
@ -76,6 +79,29 @@ const FilterTitlePane: React.FC<Props> = ({
erroredFilters, erroredFilters,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const options = [
{ label: 'Filter', type: NativeFilterType.NATIVE_FILTER },
{ label: 'Divider', type: NativeFilterType.DIVIDER },
];
const handleOnAdd = (type: NativeFilterType) => {
onAdd(type);
setTimeout(() => {
const element = document.getElementById('native-filters-tabs');
if (element) {
const navList = element.getElementsByClassName('ant-tabs-nav-list')[0];
navList.scrollTop = navList.scrollHeight;
}
}, 0);
};
const menu = (
<Menu mode="horizontal">
{options.map(item => (
<Menu.Item onClick={() => handleOnAdd(item.type)}>
{item.label}
</Menu.Item>
))}
</Menu>
);
return ( return (
<TabsContainer> <TabsContainer>
<StyledHeader>Filters</StyledHeader> <StyledHeader>Filters</StyledHeader>
@ -98,28 +124,15 @@ const FilterTitlePane: React.FC<Props> = ({
restoreFilter={restoreFilter} restoreFilter={restoreFilter}
/> />
</div> </div>
<StyledAddFilterBox <Dropdown overlay={menu} arrow placement="topLeft" trigger={['hover']}>
onClick={() => { <StyledAddBox>
onEdit('', 'add'); <StyledPlusButton
setTimeout(() => { data-test="new-dropdown-icon"
const element = document.getElementById('native-filters-tabs'); className="fa fa-plus"
if (element) { />{' '}
const navList = <span>{t('Add')}</span>
element.getElementsByClassName('ant-tabs-nav-list')[0]; </StyledAddBox>
navList.scrollTop = navList.scrollHeight; </Dropdown>
}
}, 0);
}}
>
<PlusOutlined />{' '}
<span
data-test="add-filter-button"
aria-label="Add filter"
role="button"
>
{t('Add filter')}
</span>
</StyledAddFilterBox>
</TabsContainer> </TabsContainer>
); );
}; };

View File

@ -30,7 +30,12 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
import { StyledModal } from 'src/components/Modal'; import { StyledModal } from 'src/components/Modal';
import { testWithId } from 'src/utils/testUtils'; import { testWithId } from 'src/utils/testUtils';
import { useFilterConfigMap, useFilterConfiguration } from '../state'; import { useFilterConfigMap, useFilterConfiguration } from '../state';
import { FilterConfiguration } from '../types'; import {
Filter,
FilterConfiguration,
NativeFilterType,
Divider,
} from '../types';
import FiltureConfigurePane from './FilterConfigurePane'; import FiltureConfigurePane from './FilterConfigurePane';
import FiltersConfigForm, { import FiltersConfigForm, {
FilterPanels, FilterPanels,
@ -40,12 +45,14 @@ import { useOpenModal, useRemoveCurrentFilter } from './state';
import { FilterRemoval, NativeFiltersForm, FilterHierarchy } from './types'; import { FilterRemoval, NativeFiltersForm, FilterHierarchy } from './types';
import { import {
createHandleSave, createHandleSave,
createHandleTabEdit, createHandleRemoveItem,
generateFilterId, generateFilterId,
getFilterIds, getFilterIds,
buildFilterGroup, buildFilterGroup,
validateForm, validateForm,
NATIVE_FILTER_DIVIDER_PREFIX,
} from './utils'; } from './utils';
import DividerConfigForm from './DividerConfigForm';
const StyledModalWrapper = styled(StyledModal)` const StyledModalWrapper = styled(StyledModal)`
min-width: 700px; min-width: 700px;
@ -153,7 +160,10 @@ export function FiltersConfigModal({
const getInitialFilterHierarchy = () => const getInitialFilterHierarchy = () =>
filterConfig.map(filter => ({ filterConfig.map(filter => ({
id: filter.id, id: filter.id,
parentId: filter.cascadeParentIds[0] || null, parentId:
filter.type === NativeFilterType.NATIVE_FILTER
? filter.cascadeParentIds[0] || null
: null,
})); }));
const [filterHierarchy, setFilterHierarchy] = useState<FilterHierarchy>(() => const [filterHierarchy, setFilterHierarchy] = useState<FilterHierarchy>(() =>
@ -175,24 +185,28 @@ export function FiltersConfigModal({
}; };
// generates a new filter id and appends it to the newFilterIds // generates a new filter id and appends it to the newFilterIds
const addFilter = useCallback(() => { const addFilter = useCallback(
const newFilterId = generateFilterId(); (type: NativeFilterType) => {
setNewFilterIds([...newFilterIds, newFilterId]); const newFilterId = generateFilterId(type);
setCurrentFilterId(newFilterId); setNewFilterIds([...newFilterIds, newFilterId]);
setSaveAlertVisible(false); setCurrentFilterId(newFilterId);
setFilterHierarchy(previousState => [ setSaveAlertVisible(false);
...previousState, setFilterHierarchy(previousState => [
{ id: newFilterId, parentId: null }, ...previousState,
]); { id: newFilterId, parentId: null },
setOrderedFilters([...orderedFilters, [newFilterId]]); ]);
setActiveFilterPanelKey(`${newFilterId}-${FilterPanels.basic.key}`); setOrderedFilters([...orderedFilters, [newFilterId]]);
}, [ setActiveFilterPanelKey(`${newFilterId}-${FilterPanels.basic.key}`);
newFilterIds, },
orderedFilters, [
setCurrentFilterId, newFilterIds,
setFilterHierarchy, orderedFilters,
setOrderedFilters, setCurrentFilterId,
]); setFilterHierarchy,
setOrderedFilters,
setNewFilterIds,
],
);
useOpenModal(isOpen, addFilter, createNewOnOpen); useOpenModal(isOpen, addFilter, createNewOnOpen);
@ -203,12 +217,11 @@ export function FiltersConfigModal({
setCurrentFilterId, setCurrentFilterId,
); );
const handleTabEdit = createHandleTabEdit( const handleRemoveItem = createHandleRemoveItem(
setRemovedFilters, setRemovedFilters,
setSaveAlertVisible, setSaveAlertVisible,
setOrderedFilters, setOrderedFilters,
setFilterHierarchy, setFilterHierarchy,
addFilter,
filterHierarchy, filterHierarchy,
); );
@ -230,21 +243,29 @@ export function FiltersConfigModal({
form.setFieldsValue({ changed: false }); form.setFieldsValue({ changed: false });
}; };
const getFilterTitle = (id: string) => const getFilterTitle = (id: string) => {
formValues.filters[id]?.name || const formValue = formValues.filters[id];
filterConfigMap[id]?.name || const config = filterConfigMap[id];
t('[untitled]'); return (
(formValue && 'name' in formValue && formValue.name) ||
(formValue && 'title' in formValue && formValue.title) ||
(config && 'name' in config && config.name) ||
(config && 'title' in config && config.title) ||
'[untitled]'
);
};
const getParentFilters = (id: string) => const getParentFilters = (id: string) =>
filterIds filterIds
.filter(filterId => filterId !== id && !removedFilters[filterId]) .filter(filterId => filterId !== id && !removedFilters[filterId])
.filter(filterId => .filter(filterId => {
CASCADING_FILTERS.includes( const component =
formValues.filters[filterId] formValues.filters[filterId] || filterConfigMap[filterId];
? formValues.filters[filterId].filterType return (
: filterConfigMap[filterId]?.filterType, component &&
), 'filterType' in component &&
) CASCADING_FILTERS.includes(component.filterType)
);
})
.map(id => ({ .map(id => ({
id, id,
title: getFilterTitle(id), title: getFilterTitle(id),
@ -253,6 +274,9 @@ export function FiltersConfigModal({
const cleanDeletedParents = (values: NativeFiltersForm | null) => { const cleanDeletedParents = (values: NativeFiltersForm | null) => {
Object.keys(filterConfigMap).forEach(key => { Object.keys(filterConfigMap).forEach(key => {
const filter = filterConfigMap[key]; const filter = filterConfigMap[key];
if (!('cascadeParentIds' in filter)) {
return;
}
const parentId = filter.cascadeParentIds?.[0]; const parentId = filter.cascadeParentIds?.[0];
if (parentId && removedFilters[parentId]) { if (parentId && removedFilters[parentId]) {
filter.cascadeParentIds = []; filter.cascadeParentIds = [];
@ -263,6 +287,9 @@ export function FiltersConfigModal({
if (filters) { if (filters) {
Object.keys(filters).forEach(key => { Object.keys(filters).forEach(key => {
const filter = filters[key]; const filter = filters[key];
if (!('parentFilter' in filter)) {
return;
}
const parentId = filter.parentFilter?.value; const parentId = filter.parentFilter?.value;
if (parentId && removedFilters[parentId]) { if (parentId && removedFilters[parentId]) {
filter.parentFilter = undefined; filter.parentFilter = undefined;
@ -369,13 +396,18 @@ export function FiltersConfigModal({
const onValuesChange = useMemo( const onValuesChange = useMemo(
() => () =>
debounce((changes: any, values: NativeFiltersForm) => { debounce((changes: any, values: NativeFiltersForm) => {
if ( const didChangeFilterName =
changes.filters && changes.filters &&
Object.values(changes.filters).some( Object.values(changes.filters).some(
(filter: any) => filter.name != null, (filter: any) => filter.name !== null,
) );
) { const didChangeSectionTitle =
// we only need to set this if a name changed changes.filters &&
Object.values(changes.filters).some(
(filter: any) => filter.title !== null,
);
if (didChangeFilterName || didChangeSectionTitle) {
// we only need to set this if a name/title changed
setFormValues(values); setFormValues(values);
} }
const changedFilterHierarchies = Object.keys(changes.filters) const changedFilterHierarchies = Object.keys(changes.filters)
@ -402,6 +434,31 @@ export function FiltersConfigModal({
prevErroredFilters.filter(f => !removedFilters[f]), prevErroredFilters.filter(f => !removedFilters[f]),
); );
}, [removedFilters]); }, [removedFilters]);
const getForm = (id: string) => {
const isDivider = id.startsWith(NATIVE_FILTER_DIVIDER_PREFIX);
return isDivider ? (
<DividerConfigForm
componentId={id}
divider={filterConfigMap[id] as Divider}
/>
) : (
<FiltersConfigForm
ref={configFormRef}
form={form}
filterId={id}
filterToEdit={filterConfigMap[id] as Filter}
removedFilters={removedFilters}
restoreFilter={restoreFilter}
parentFilters={getParentFilters(id)}
onFilterHierarchyChange={handleFilterHierarchyChange}
key={id}
activeFilterPanelKeys={activeFilterPanelKey}
handleActiveFilterPanelChange={key => setActiveFilterPanelKey(key)}
isActive={currentFilterId === id}
setErroredFilters={setErroredFilters}
/>
);
};
return ( return (
<StyledModalWrapper <StyledModalWrapper
@ -434,7 +491,8 @@ export function FiltersConfigModal({
> >
<FiltureConfigurePane <FiltureConfigurePane
erroredFilters={erroredFilters} erroredFilters={erroredFilters}
onEdit={handleTabEdit} onRemove={handleRemoveItem}
onAdd={addFilter}
onChange={onTabChange} onChange={onTabChange}
getFilterTitle={getFilterTitle} getFilterTitle={getFilterTitle}
currentFilterId={currentFilterId} currentFilterId={currentFilterId}
@ -443,25 +501,7 @@ export function FiltersConfigModal({
onRearrange={onRearrage} onRearrange={onRearrage}
filterGroups={orderedFilters} filterGroups={orderedFilters}
> >
{(id: string) => ( {(id: string) => getForm(id)}
<FiltersConfigForm
ref={configFormRef}
form={form}
filterId={id}
filterToEdit={filterConfigMap[id]}
removedFilters={removedFilters}
restoreFilter={restoreFilter}
parentFilters={getParentFilters(id)}
onFilterHierarchyChange={handleFilterHierarchyChange}
key={id}
activeFilterPanelKeys={activeFilterPanelKey}
handleActiveFilterPanelChange={key =>
setActiveFilterPanelKey(key)
}
isActive={currentFilterId === id}
setErroredFilters={setErroredFilters}
/>
)}
</FiltureConfigurePane> </FiltureConfigurePane>
</StyledForm> </StyledForm>
</StyledModalBody> </StyledModalBody>

View File

@ -1,6 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FilterRemoval } from './types'; import { FilterRemoval } from './types';
import { usePrevious } from '../../../../common/hooks/usePrevious'; import { usePrevious } from '../../../../common/hooks/usePrevious';
import { NativeFilterType } from '../types';
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
@ -52,7 +53,7 @@ export const useOpenModal = (
// add a filter on modal open // add a filter on modal open
useEffect(() => { useEffect(() => {
if (createNewOnOpen && isOpen && !wasOpen) { if (createNewOnOpen && isOpen && !wasOpen) {
addFilter(); addFilter(NativeFilterType.NATIVE_FILTER);
} }
}, [createNewOnOpen, isOpen, wasOpen, addFilter]); }, [createNewOnOpen, isOpen, wasOpen, addFilter]);
}; };

View File

@ -44,13 +44,19 @@ export interface NativeFiltersFormItem {
adhoc_filters?: AdhocFilter[]; adhoc_filters?: AdhocFilter[];
time_range?: string; time_range?: string;
granularity_sqla?: string; granularity_sqla?: string;
type: NativeFilterType; type: typeof NativeFilterType.NATIVE_FILTER;
description: string; description: string;
hierarchicalFilter?: boolean; hierarchicalFilter?: boolean;
} }
export interface NativeFilterDivider {
id: string;
type: typeof NativeFilterType.DIVIDER;
title: string;
description: string;
}
export interface NativeFiltersForm { export interface NativeFiltersForm {
filters: Record<string, NativeFiltersFormItem>; filters: Record<string, NativeFiltersFormItem | NativeFilterDivider>;
changed?: boolean; changed?: boolean;
} }

View File

@ -21,20 +21,27 @@ import shortid from 'shortid';
import { getInitialDataMask } from 'src/dataMask/reducer'; import { getInitialDataMask } from 'src/dataMask/reducer';
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import { import {
FilterRemoval, FilterRemoval,
NativeFiltersForm, NativeFiltersForm,
FilterHierarchy, FilterHierarchy,
FilterHierarchyNode, FilterHierarchyNode,
} from './types'; } from './types';
import { Filter, FilterConfiguration, Target } from '../types'; import {
Filter,
FilterConfiguration,
NativeFilterType,
Divider,
Target,
} from '../types';
export const REMOVAL_DELAY_SECS = 5; export const REMOVAL_DELAY_SECS = 5;
export const validateForm = async ( export const validateForm = async (
form: FormInstance<NativeFiltersForm>, form: FormInstance<NativeFiltersForm>,
currentFilterId: string, currentFilterId: string,
filterConfigMap: Record<string, Filter>, filterConfigMap: Record<string, Filter | Divider>,
filterIds: string[], filterIds: string[],
removedFilters: Record<string, FilterRemoval>, removedFilters: Record<string, FilterRemoval>,
setCurrentFilterId: Function, setCurrentFilterId: Function,
@ -77,9 +84,12 @@ export const validateForm = async (
); );
return false; return false;
} }
const parentId = formValues.filters?.[filterId] const formItem = formValues.filters?.[filterId];
? formValues.filters[filterId]?.parentFilter?.value const configItem = filterConfigMap[filterId];
: filterConfigMap[filterId]?.cascadeParentIds?.[0]; const parentId = formItem
? 'parentFilter' in formItem && formItem.parentFilter?.value
: 'cascadeParentIds' in configItem && configItem?.cascadeParentIds?.[0];
if (parentId) { if (parentId) {
return validateCycles(parentId, [...trace, filterId]); return validateCycles(parentId, [...trace, filterId]);
} }
@ -120,7 +130,7 @@ export const validateForm = async (
export const createHandleSave = export const createHandleSave =
( (
filterConfigMap: Record<string, Filter>, filterConfigMap: Record<string, Filter | Divider>,
filterIds: string[], filterIds: string[],
removedFilters: Record<string, FilterRemoval>, removedFilters: Record<string, FilterRemoval>,
saveForm: Function, saveForm: Function,
@ -134,6 +144,18 @@ export const createHandleSave =
const formInputs = values.filters?.[id]; const formInputs = values.filters?.[id];
// if user didn't open a filter, return the original config // if user didn't open a filter, return the original config
if (!formInputs) return filterConfigMap[id]; if (!formInputs) return filterConfigMap[id];
if (formInputs.type === NativeFilterType.DIVIDER) {
return {
id,
type: NativeFilterType.DIVIDER,
scope: {
rootPath: [DASHBOARD_ROOT_ID],
excluded: [],
},
title: formInputs.title,
description: formInputs.description,
};
}
const target: Partial<Target> = {}; const target: Partial<Target> = {};
if (formInputs.dataset) { if (formInputs.dataset) {
target.datasetId = formInputs.dataset.value; target.datasetId = formInputs.dataset.value;
@ -210,7 +232,7 @@ export function buildFilterGroup(nodes: FilterHierarchyNode[]) {
} }
return group; return group;
} }
export const createHandleTabEdit = export const createHandleRemoveItem =
( (
setRemovedFilters: ( setRemovedFilters: (
value: value:
@ -228,10 +250,9 @@ export const createHandleTabEdit =
| FilterHierarchy | FilterHierarchy
| ((prevState: FilterHierarchy) => FilterHierarchy), | ((prevState: FilterHierarchy) => FilterHierarchy),
) => void, ) => void,
addFilter: Function,
filterHierarchy: FilterHierarchy, filterHierarchy: FilterHierarchy,
) => ) =>
(filterId: string, action: 'add' | 'remove') => { (filterId: string) => {
const completeFilterRemoval = (filterId: string) => { const completeFilterRemoval = (filterId: string) => {
const buildNewFilterHierarchy = (hierarchy: FilterHierarchy) => const buildNewFilterHierarchy = (hierarchy: FilterHierarchy) =>
hierarchy hierarchy
@ -277,25 +298,27 @@ export const createHandleTabEdit =
}); });
}; };
if (action === 'remove') { // first set up the timer to completely remove it
// first set up the timer to completely remove it const timerId = window.setTimeout(() => {
const timerId = window.setTimeout(() => { completeFilterRemoval(filterId);
completeFilterRemoval(filterId); }, REMOVAL_DELAY_SECS * 1000);
}, REMOVAL_DELAY_SECS * 1000); // mark the filter state as "removal in progress"
// mark the filter state as "removal in progress" setRemovedFilters(removedFilters => ({
setRemovedFilters(removedFilters => ({ ...removedFilters,
...removedFilters, [filterId]: { isPending: true, timerId },
[filterId]: { isPending: true, timerId }, }));
})); setSaveAlertVisible(false);
setSaveAlertVisible(false);
} else if (action === 'add') {
addFilter();
}
}; };
export const NATIVE_FILTER_PREFIX = 'NATIVE_FILTER-'; export const NATIVE_FILTER_PREFIX = 'NATIVE_FILTER-';
export const generateFilterId = () => export const NATIVE_FILTER_DIVIDER_PREFIX = 'NATIVE_FILTER_DIVIDER-';
`${NATIVE_FILTER_PREFIX}${shortid.generate()}`; export const generateFilterId = (type: NativeFilterType) => {
const prefix =
type === NativeFilterType.NATIVE_FILTER
? NATIVE_FILTER_PREFIX
: NATIVE_FILTER_DIVIDER_PREFIX;
return `${prefix}${shortid.generate()}`;
};
export const getFilterIds = (config: FilterConfiguration) => export const getFilterIds = (config: FilterConfiguration) =>
config.map(filter => filter.id); config.map(filter => filter.id);

View File

@ -18,7 +18,12 @@
*/ */
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Filter, FilterConfiguration } from './types'; import {
Filter,
FilterConfiguration,
NativeFilterType,
Divider,
} from './types';
import { ActiveTabs, DashboardLayout, RootState } from '../../types'; import { ActiveTabs, DashboardLayout, RootState } from '../../types';
import { TAB_TYPE } from '../../util/componentTypes'; import { TAB_TYPE } from '../../util/componentTypes';
import { CascadeFilter } from './FilterBar/CascadeFilters/types'; import { CascadeFilter } from './FilterBar/CascadeFilters/types';
@ -41,10 +46,13 @@ export function useFilterConfigMap() {
const filterConfig = useFilterConfiguration(); const filterConfig = useFilterConfiguration();
return useMemo( return useMemo(
() => () =>
filterConfig.reduce((acc: Record<string, Filter>, filter: Filter) => { filterConfig.reduce(
acc[filter.id] = filter; (acc: Record<string, Filter | Divider>, filter: Filter) => {
return acc; acc[filter.id] = filter;
}, {} as Record<string, Filter>), return acc;
},
{} as Record<string, Filter | Divider>,
),
[filterConfig], [filterConfig],
); );
} }
@ -89,29 +97,38 @@ function useIsFilterInScope() {
// Filter is in scope if any of it's charts is visible. // Filter is in scope if any of it's charts is visible.
// Chart is visible if it's placed in an active tab tree or if it's not attached to any tab. // Chart is visible if it's placed in an active tab tree or if it's not attached to any tab.
// Chart is in an active tab tree if all of it's ancestors of type TAB are active // Chart is in an active tab tree if all of it's ancestors of type TAB are active
return (filter: CascadeFilter) => // Dividers are always in scope
filter.chartsInScope?.some((chartId: number) => { return (filter: CascadeFilter | Divider) => {
const tabParents = selectChartTabParents(chartId); const isDivider = filter.type === NativeFilterType.DIVIDER;
return ( return (
tabParents?.length === 0 || isDivider ||
tabParents?.every(tab => activeTabs.includes(tab)) ('chartsInScope' in filter &&
); filter.chartsInScope?.some((chartId: number) => {
}); const tabParents = selectChartTabParents(chartId);
return (
tabParents?.length === 0 ||
tabParents?.every(tab => activeTabs.includes(tab))
);
}))
);
};
} }
export function useSelectFiltersInScope(cascadeFilters: CascadeFilter[]) { export function useSelectFiltersInScope(
cascadeFilters: (CascadeFilter | Divider)[],
) {
const dashboardHasTabs = useDashboardHasTabs(); const dashboardHasTabs = useDashboardHasTabs();
const isFilterInScope = useIsFilterInScope(); const isFilterInScope = useIsFilterInScope();
return useMemo(() => { return useMemo(() => {
let filtersInScope: CascadeFilter[] = []; let filtersInScope: (CascadeFilter | Divider)[] = [];
const filtersOutOfScope: CascadeFilter[] = []; const filtersOutOfScope: (CascadeFilter | Divider)[] = [];
// we check native filters scopes only on dashboards with tabs // we check native filters scopes only on dashboards with tabs
if (!dashboardHasTabs) { if (!dashboardHasTabs) {
filtersInScope = cascadeFilters; filtersInScope = cascadeFilters;
} else { } else {
cascadeFilters.forEach((filter: CascadeFilter) => { cascadeFilters.forEach(filter => {
const filterInScope = isFilterInScope(filter); const filterInScope = isFilterInScope(filter);
if (filterInScope) { if (filterInScope) {

View File

@ -39,6 +39,11 @@ export interface Target {
// clarityColumns?: Column[]; // clarityColumns?: Column[];
} }
export enum NativeFilterType {
NATIVE_FILTER = 'NATIVE_FILTER',
DIVIDER = 'DIVIDER',
}
export interface Filter { export interface Filter {
cascadeParentIds: string[]; cascadeParentIds: string[];
defaultDataMask: DataMask; defaultDataMask: DataMask;
@ -62,13 +67,14 @@ export interface Filter {
requiredFirst?: boolean; requiredFirst?: boolean;
tabsInScope?: string[]; tabsInScope?: string[];
chartsInScope?: number[]; chartsInScope?: number[];
type: NativeFilterType; type: typeof NativeFilterType.NATIVE_FILTER;
description: string; description: string;
} }
export interface Divider {
export type FilterConfiguration = Filter[]; id: string;
title: string;
export enum NativeFilterType { description: string;
NATIVE_FILTER = 'NATIVE_FILTER', type: typeof NativeFilterType.DIVIDER;
SECTION = 'SECTION',
} }
export type FilterConfiguration = Array<Filter | Divider>;