mirror of https://github.com/apache/superset.git
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:
parent
c216565190
commit
9576478a5d
|
@ -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(
|
||||
|
|
|
@ -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<DashboardContainerProps> = ({ 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<DashboardContainerProps> = ({ 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: {
|
||||
|
|
|
@ -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,7 +269,9 @@ export const selectNativeIndicatorsForChart = (
|
|||
nativeFilterIndicators =
|
||||
nativeFilters &&
|
||||
Object.values(nativeFilters)
|
||||
.filter(nativeFilter =>
|
||||
.filter(
|
||||
nativeFilter =>
|
||||
nativeFilter.type === NativeFilterType.NATIVE_FILTER &&
|
||||
getTreeCheckedItems(nativeFilter.scope, dashboardLayout).some(
|
||||
layoutItem =>
|
||||
dashboardLayout[layoutItem]?.meta?.chartId === chartId,
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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<FilterControlsProps> = ({
|
|||
const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0;
|
||||
|
||||
const cascadePopoverFactory = useCallback(
|
||||
index => (
|
||||
index => {
|
||||
const filter = cascadeFilters[index];
|
||||
if (filter.type === NativeFilterType.DIVIDER) {
|
||||
return (
|
||||
<div>
|
||||
<h3>{filter.title}</h3>
|
||||
<p>{filter.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CascadePopover
|
||||
data-test="cascade-filters-control"
|
||||
key={cascadeFilters[index].id}
|
||||
key={filter.id}
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
visible={visiblePopoverId === cascadeFilters[index].id}
|
||||
visible={visiblePopoverId === filter.id}
|
||||
onVisibleChange={visible =>
|
||||
setVisiblePopoverId(visible ? cascadeFilters[index].id : null)
|
||||
setVisiblePopoverId(visible ? filter.id : null)
|
||||
}
|
||||
filter={cascadeFilters[index]}
|
||||
filter={filter}
|
||||
onFilterSelectionChange={onFilterSelectionChange}
|
||||
directPathToChild={directPathToChild}
|
||||
inView={false}
|
||||
/>
|
||||
),
|
||||
);
|
||||
},
|
||||
[
|
||||
cascadeFilters,
|
||||
JSON.stringify(dataMaskSelected),
|
||||
|
|
|
@ -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<Divider | Filter>,
|
||||
): Array<CascadeFilter | Divider> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<FiltersHeaderProps> = ({ 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) {
|
||||
|
|
|
@ -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<FiltersBarProps> = ({
|
|||
filterValues,
|
||||
);
|
||||
const isInitialized = useInitialization();
|
||||
|
||||
const tabPaneStyle = useMemo(() => ({ overflow: 'auto', height }), [height]);
|
||||
|
||||
const numberOfFilters = filterValues.filter(
|
||||
filterValue => filterValue.type === NativeFilterType.NATIVE_FILTER,
|
||||
).length;
|
||||
|
||||
return (
|
||||
<BarWrapper
|
||||
{...getFilterBarTestId()}
|
||||
|
@ -320,8 +323,8 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||
activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
|
||||
>
|
||||
<Tabs.TabPane
|
||||
tab={t('All Filters (%(filterCount)d)', {
|
||||
filterCount: filterValues.length,
|
||||
tab={t('All filters (%(filterCount)d)', {
|
||||
filterCount: numberOfFilters,
|
||||
})}
|
||||
key={TabIds.AllFilters}
|
||||
css={tabPaneStyle}
|
||||
|
@ -342,7 +345,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
disabled={!!editFilterSetId}
|
||||
tab={t('Filter Sets (%(filterSetCount)d)', {
|
||||
tab={t('Filter sets (%(filterSetCount)d)', {
|
||||
filterSetCount: filterSetFilterValues.length,
|
||||
})}
|
||||
key={TabIds.FilterSets}
|
||||
|
|
|
@ -20,19 +20,20 @@
|
|||
import { DataMaskStateWithId } from 'src/dataMask/types';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { FilterState } from '@superset-ui/core';
|
||||
import { Filter } from '../types';
|
||||
import { Filter, Divider } from '../types';
|
||||
|
||||
export enum TabIds {
|
||||
AllFilters = 'allFilters',
|
||||
FilterSets = 'filterSets',
|
||||
}
|
||||
|
||||
export function mapParentFiltersToChildren(filters: Filter[]): {
|
||||
export function mapParentFiltersToChildren(filters: Array<Filter | Divider>): {
|
||||
[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] = [];
|
||||
|
|
|
@ -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;
|
|
@ -26,7 +26,8 @@ const defaultProps = {
|
|||
children: jest.fn(),
|
||||
getFilterTitle: (id: string) => id,
|
||||
onChange: jest.fn(),
|
||||
onEdit: jest.fn(),
|
||||
onAdd: jest.fn(),
|
||||
onRemove: jest.fn(),
|
||||
onRearrange: jest.fn(),
|
||||
restoreFilter: jest.fn(),
|
||||
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 () => {
|
||||
defaultRender();
|
||||
// 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 () => {
|
||||
fireEvent(
|
||||
removeFilterIcon,
|
||||
addFilterButton,
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(defaultProps.onEdit).toHaveBeenCalledWith('', 'add');
|
||||
expect(defaultProps.onAdd).toHaveBeenCalledWith('NATIVE_FILTER');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
import { styled } from '@superset-ui/core';
|
||||
import React from 'react';
|
||||
import { NativeFilterType } from '../types';
|
||||
import FilterTitlePane from './FilterTitlePane';
|
||||
import { FilterRemoval } from './types';
|
||||
|
||||
|
@ -25,7 +26,8 @@ interface Props {
|
|||
children: (filterId: string) => React.ReactNode;
|
||||
getFilterTitle: (filterId: string) => string;
|
||||
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;
|
||||
erroredFilters: string[];
|
||||
restoreFilter: (id: string) => void;
|
||||
|
@ -52,9 +54,10 @@ const TitlesContainer = styled.div`
|
|||
const FiltureConfigurePane: React.FC<Props> = ({
|
||||
getFilterTitle,
|
||||
onChange,
|
||||
onEdit,
|
||||
onRemove,
|
||||
onRearrange,
|
||||
restoreFilter,
|
||||
onAdd,
|
||||
erroredFilters,
|
||||
children,
|
||||
currentFilterId,
|
||||
|
@ -72,9 +75,9 @@ const FiltureConfigurePane: React.FC<Props> = ({
|
|||
erroredFilters={erroredFilters}
|
||||
getFilterTitle={getFilterTitle}
|
||||
onChange={onChange}
|
||||
onEdit={onEdit}
|
||||
onAdd={(type: NativeFilterType) => onAdd(type)}
|
||||
onRearrage={onRearrange}
|
||||
onRemove={(id: string) => onEdit(id, 'remove')}
|
||||
onRemove={(id: string) => onRemove(id)}
|
||||
restoreFilter={restoreFilter}
|
||||
/>
|
||||
</TitlesContainer>
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { styled, t, useTheme } from '@superset-ui/core';
|
||||
import React from 'react';
|
||||
import { Dropdown, MainNav as Menu } from 'src/common/components';
|
||||
import { NativeFilterType } from '../types';
|
||||
import FilterTitleContainer from './FilterTitleContainer';
|
||||
import { FilterRemoval } from './types';
|
||||
|
||||
|
@ -28,13 +29,17 @@ interface Props {
|
|||
onRearrage: (dragIndex: number, targetIndex: number) => void;
|
||||
onRemove: (id: string) => void;
|
||||
onChange: (id: string) => void;
|
||||
onEdit: (filterId: string, action: 'add' | 'remove') => void;
|
||||
onAdd: (type: NativeFilterType) => void;
|
||||
removedFilters: Record<string, FilterRemoval>;
|
||||
currentFilterId: string;
|
||||
filterGroups: string[][];
|
||||
erroredFilters: string[];
|
||||
}
|
||||
|
||||
const StyledPlusButton = styled.div`
|
||||
color: ${({ theme }) => theme.colors.primary.dark1};
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
${({ theme }) => `
|
||||
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`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
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> = ({
|
||||
getFilterTitle,
|
||||
onChange,
|
||||
onEdit,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onRearrage,
|
||||
restoreFilter,
|
||||
|
@ -76,6 +79,29 @@ const FilterTitlePane: React.FC<Props> = ({
|
|||
erroredFilters,
|
||||
}) => {
|
||||
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 (
|
||||
<TabsContainer>
|
||||
<StyledHeader>Filters</StyledHeader>
|
||||
|
@ -98,28 +124,15 @@ const FilterTitlePane: React.FC<Props> = ({
|
|||
restoreFilter={restoreFilter}
|
||||
/>
|
||||
</div>
|
||||
<StyledAddFilterBox
|
||||
onClick={() => {
|
||||
onEdit('', 'add');
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById('native-filters-tabs');
|
||||
if (element) {
|
||||
const navList =
|
||||
element.getElementsByClassName('ant-tabs-nav-list')[0];
|
||||
navList.scrollTop = navList.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />{' '}
|
||||
<span
|
||||
data-test="add-filter-button"
|
||||
aria-label="Add filter"
|
||||
role="button"
|
||||
>
|
||||
{t('Add filter')}
|
||||
</span>
|
||||
</StyledAddFilterBox>
|
||||
<Dropdown overlay={menu} arrow placement="topLeft" trigger={['hover']}>
|
||||
<StyledAddBox>
|
||||
<StyledPlusButton
|
||||
data-test="new-dropdown-icon"
|
||||
className="fa fa-plus"
|
||||
/>{' '}
|
||||
<span>{t('Add')}</span>
|
||||
</StyledAddBox>
|
||||
</Dropdown>
|
||||
</TabsContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,7 +30,12 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
|
|||
import { StyledModal } from 'src/components/Modal';
|
||||
import { testWithId } from 'src/utils/testUtils';
|
||||
import { useFilterConfigMap, useFilterConfiguration } from '../state';
|
||||
import { FilterConfiguration } from '../types';
|
||||
import {
|
||||
Filter,
|
||||
FilterConfiguration,
|
||||
NativeFilterType,
|
||||
Divider,
|
||||
} from '../types';
|
||||
import FiltureConfigurePane from './FilterConfigurePane';
|
||||
import FiltersConfigForm, {
|
||||
FilterPanels,
|
||||
|
@ -40,12 +45,14 @@ import { useOpenModal, useRemoveCurrentFilter } from './state';
|
|||
import { FilterRemoval, NativeFiltersForm, FilterHierarchy } from './types';
|
||||
import {
|
||||
createHandleSave,
|
||||
createHandleTabEdit,
|
||||
createHandleRemoveItem,
|
||||
generateFilterId,
|
||||
getFilterIds,
|
||||
buildFilterGroup,
|
||||
validateForm,
|
||||
NATIVE_FILTER_DIVIDER_PREFIX,
|
||||
} from './utils';
|
||||
import DividerConfigForm from './DividerConfigForm';
|
||||
|
||||
const StyledModalWrapper = styled(StyledModal)`
|
||||
min-width: 700px;
|
||||
|
@ -153,7 +160,10 @@ export function FiltersConfigModal({
|
|||
const getInitialFilterHierarchy = () =>
|
||||
filterConfig.map(filter => ({
|
||||
id: filter.id,
|
||||
parentId: filter.cascadeParentIds[0] || null,
|
||||
parentId:
|
||||
filter.type === NativeFilterType.NATIVE_FILTER
|
||||
? filter.cascadeParentIds[0] || null
|
||||
: null,
|
||||
}));
|
||||
|
||||
const [filterHierarchy, setFilterHierarchy] = useState<FilterHierarchy>(() =>
|
||||
|
@ -175,8 +185,9 @@ export function FiltersConfigModal({
|
|||
};
|
||||
|
||||
// generates a new filter id and appends it to the newFilterIds
|
||||
const addFilter = useCallback(() => {
|
||||
const newFilterId = generateFilterId();
|
||||
const addFilter = useCallback(
|
||||
(type: NativeFilterType) => {
|
||||
const newFilterId = generateFilterId(type);
|
||||
setNewFilterIds([...newFilterIds, newFilterId]);
|
||||
setCurrentFilterId(newFilterId);
|
||||
setSaveAlertVisible(false);
|
||||
|
@ -186,13 +197,16 @@ export function FiltersConfigModal({
|
|||
]);
|
||||
setOrderedFilters([...orderedFilters, [newFilterId]]);
|
||||
setActiveFilterPanelKey(`${newFilterId}-${FilterPanels.basic.key}`);
|
||||
}, [
|
||||
},
|
||||
[
|
||||
newFilterIds,
|
||||
orderedFilters,
|
||||
setCurrentFilterId,
|
||||
setFilterHierarchy,
|
||||
setOrderedFilters,
|
||||
]);
|
||||
setNewFilterIds,
|
||||
],
|
||||
);
|
||||
|
||||
useOpenModal(isOpen, addFilter, createNewOnOpen);
|
||||
|
||||
|
@ -203,12 +217,11 @@ export function FiltersConfigModal({
|
|||
setCurrentFilterId,
|
||||
);
|
||||
|
||||
const handleTabEdit = createHandleTabEdit(
|
||||
const handleRemoveItem = createHandleRemoveItem(
|
||||
setRemovedFilters,
|
||||
setSaveAlertVisible,
|
||||
setOrderedFilters,
|
||||
setFilterHierarchy,
|
||||
addFilter,
|
||||
filterHierarchy,
|
||||
);
|
||||
|
||||
|
@ -230,21 +243,29 @@ export function FiltersConfigModal({
|
|||
form.setFieldsValue({ changed: false });
|
||||
};
|
||||
|
||||
const getFilterTitle = (id: string) =>
|
||||
formValues.filters[id]?.name ||
|
||||
filterConfigMap[id]?.name ||
|
||||
t('[untitled]');
|
||||
|
||||
const getFilterTitle = (id: string) => {
|
||||
const formValue = formValues.filters[id];
|
||||
const config = filterConfigMap[id];
|
||||
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) =>
|
||||
filterIds
|
||||
.filter(filterId => filterId !== id && !removedFilters[filterId])
|
||||
.filter(filterId =>
|
||||
CASCADING_FILTERS.includes(
|
||||
formValues.filters[filterId]
|
||||
? formValues.filters[filterId].filterType
|
||||
: filterConfigMap[filterId]?.filterType,
|
||||
),
|
||||
)
|
||||
.filter(filterId => {
|
||||
const component =
|
||||
formValues.filters[filterId] || filterConfigMap[filterId];
|
||||
return (
|
||||
component &&
|
||||
'filterType' in component &&
|
||||
CASCADING_FILTERS.includes(component.filterType)
|
||||
);
|
||||
})
|
||||
.map(id => ({
|
||||
id,
|
||||
title: getFilterTitle(id),
|
||||
|
@ -253,6 +274,9 @@ export function FiltersConfigModal({
|
|||
const cleanDeletedParents = (values: NativeFiltersForm | null) => {
|
||||
Object.keys(filterConfigMap).forEach(key => {
|
||||
const filter = filterConfigMap[key];
|
||||
if (!('cascadeParentIds' in filter)) {
|
||||
return;
|
||||
}
|
||||
const parentId = filter.cascadeParentIds?.[0];
|
||||
if (parentId && removedFilters[parentId]) {
|
||||
filter.cascadeParentIds = [];
|
||||
|
@ -263,6 +287,9 @@ export function FiltersConfigModal({
|
|||
if (filters) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
const filter = filters[key];
|
||||
if (!('parentFilter' in filter)) {
|
||||
return;
|
||||
}
|
||||
const parentId = filter.parentFilter?.value;
|
||||
if (parentId && removedFilters[parentId]) {
|
||||
filter.parentFilter = undefined;
|
||||
|
@ -369,13 +396,18 @@ export function FiltersConfigModal({
|
|||
const onValuesChange = useMemo(
|
||||
() =>
|
||||
debounce((changes: any, values: NativeFiltersForm) => {
|
||||
if (
|
||||
const didChangeFilterName =
|
||||
changes.filters &&
|
||||
Object.values(changes.filters).some(
|
||||
(filter: any) => filter.name != null,
|
||||
)
|
||||
) {
|
||||
// we only need to set this if a name changed
|
||||
(filter: any) => filter.name !== null,
|
||||
);
|
||||
const didChangeSectionTitle =
|
||||
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);
|
||||
}
|
||||
const changedFilterHierarchies = Object.keys(changes.filters)
|
||||
|
@ -402,6 +434,31 @@ export function FiltersConfigModal({
|
|||
prevErroredFilters.filter(f => !removedFilters[f]),
|
||||
);
|
||||
}, [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 (
|
||||
<StyledModalWrapper
|
||||
|
@ -434,7 +491,8 @@ export function FiltersConfigModal({
|
|||
>
|
||||
<FiltureConfigurePane
|
||||
erroredFilters={erroredFilters}
|
||||
onEdit={handleTabEdit}
|
||||
onRemove={handleRemoveItem}
|
||||
onAdd={addFilter}
|
||||
onChange={onTabChange}
|
||||
getFilterTitle={getFilterTitle}
|
||||
currentFilterId={currentFilterId}
|
||||
|
@ -443,25 +501,7 @@ export function FiltersConfigModal({
|
|||
onRearrange={onRearrage}
|
||||
filterGroups={orderedFilters}
|
||||
>
|
||||
{(id: string) => (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
{(id: string) => getForm(id)}
|
||||
</FiltureConfigurePane>
|
||||
</StyledForm>
|
||||
</StyledModalBody>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect } from 'react';
|
||||
import { FilterRemoval } from './types';
|
||||
import { usePrevious } from '../../../../common/hooks/usePrevious';
|
||||
import { NativeFilterType } from '../types';
|
||||
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
|
@ -52,7 +53,7 @@ export const useOpenModal = (
|
|||
// add a filter on modal open
|
||||
useEffect(() => {
|
||||
if (createNewOnOpen && isOpen && !wasOpen) {
|
||||
addFilter();
|
||||
addFilter(NativeFilterType.NATIVE_FILTER);
|
||||
}
|
||||
}, [createNewOnOpen, isOpen, wasOpen, addFilter]);
|
||||
};
|
||||
|
|
|
@ -44,13 +44,19 @@ export interface NativeFiltersFormItem {
|
|||
adhoc_filters?: AdhocFilter[];
|
||||
time_range?: string;
|
||||
granularity_sqla?: string;
|
||||
type: NativeFilterType;
|
||||
type: typeof NativeFilterType.NATIVE_FILTER;
|
||||
description: string;
|
||||
hierarchicalFilter?: boolean;
|
||||
}
|
||||
export interface NativeFilterDivider {
|
||||
id: string;
|
||||
type: typeof NativeFilterType.DIVIDER;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface NativeFiltersForm {
|
||||
filters: Record<string, NativeFiltersFormItem>;
|
||||
filters: Record<string, NativeFiltersFormItem | NativeFilterDivider>;
|
||||
changed?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,20 +21,27 @@ import shortid from 'shortid';
|
|||
import { getInitialDataMask } from 'src/dataMask/reducer';
|
||||
|
||||
import { t } from '@superset-ui/core';
|
||||
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
||||
import {
|
||||
FilterRemoval,
|
||||
NativeFiltersForm,
|
||||
FilterHierarchy,
|
||||
FilterHierarchyNode,
|
||||
} from './types';
|
||||
import { Filter, FilterConfiguration, Target } from '../types';
|
||||
import {
|
||||
Filter,
|
||||
FilterConfiguration,
|
||||
NativeFilterType,
|
||||
Divider,
|
||||
Target,
|
||||
} from '../types';
|
||||
|
||||
export const REMOVAL_DELAY_SECS = 5;
|
||||
|
||||
export const validateForm = async (
|
||||
form: FormInstance<NativeFiltersForm>,
|
||||
currentFilterId: string,
|
||||
filterConfigMap: Record<string, Filter>,
|
||||
filterConfigMap: Record<string, Filter | Divider>,
|
||||
filterIds: string[],
|
||||
removedFilters: Record<string, FilterRemoval>,
|
||||
setCurrentFilterId: Function,
|
||||
|
@ -77,9 +84,12 @@ export const validateForm = async (
|
|||
);
|
||||
return false;
|
||||
}
|
||||
const parentId = formValues.filters?.[filterId]
|
||||
? formValues.filters[filterId]?.parentFilter?.value
|
||||
: filterConfigMap[filterId]?.cascadeParentIds?.[0];
|
||||
const formItem = formValues.filters?.[filterId];
|
||||
const configItem = filterConfigMap[filterId];
|
||||
const parentId = formItem
|
||||
? 'parentFilter' in formItem && formItem.parentFilter?.value
|
||||
: 'cascadeParentIds' in configItem && configItem?.cascadeParentIds?.[0];
|
||||
|
||||
if (parentId) {
|
||||
return validateCycles(parentId, [...trace, filterId]);
|
||||
}
|
||||
|
@ -120,7 +130,7 @@ export const validateForm = async (
|
|||
|
||||
export const createHandleSave =
|
||||
(
|
||||
filterConfigMap: Record<string, Filter>,
|
||||
filterConfigMap: Record<string, Filter | Divider>,
|
||||
filterIds: string[],
|
||||
removedFilters: Record<string, FilterRemoval>,
|
||||
saveForm: Function,
|
||||
|
@ -134,6 +144,18 @@ export const createHandleSave =
|
|||
const formInputs = values.filters?.[id];
|
||||
// if user didn't open a filter, return the original config
|
||||
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> = {};
|
||||
if (formInputs.dataset) {
|
||||
target.datasetId = formInputs.dataset.value;
|
||||
|
@ -210,7 +232,7 @@ export function buildFilterGroup(nodes: FilterHierarchyNode[]) {
|
|||
}
|
||||
return group;
|
||||
}
|
||||
export const createHandleTabEdit =
|
||||
export const createHandleRemoveItem =
|
||||
(
|
||||
setRemovedFilters: (
|
||||
value:
|
||||
|
@ -228,10 +250,9 @@ export const createHandleTabEdit =
|
|||
| FilterHierarchy
|
||||
| ((prevState: FilterHierarchy) => FilterHierarchy),
|
||||
) => void,
|
||||
addFilter: Function,
|
||||
filterHierarchy: FilterHierarchy,
|
||||
) =>
|
||||
(filterId: string, action: 'add' | 'remove') => {
|
||||
(filterId: string) => {
|
||||
const completeFilterRemoval = (filterId: string) => {
|
||||
const buildNewFilterHierarchy = (hierarchy: FilterHierarchy) =>
|
||||
hierarchy
|
||||
|
@ -277,7 +298,6 @@ export const createHandleTabEdit =
|
|||
});
|
||||
};
|
||||
|
||||
if (action === 'remove') {
|
||||
// first set up the timer to completely remove it
|
||||
const timerId = window.setTimeout(() => {
|
||||
completeFilterRemoval(filterId);
|
||||
|
@ -288,14 +308,17 @@ export const createHandleTabEdit =
|
|||
[filterId]: { isPending: true, timerId },
|
||||
}));
|
||||
setSaveAlertVisible(false);
|
||||
} else if (action === 'add') {
|
||||
addFilter();
|
||||
}
|
||||
};
|
||||
|
||||
export const NATIVE_FILTER_PREFIX = 'NATIVE_FILTER-';
|
||||
export const generateFilterId = () =>
|
||||
`${NATIVE_FILTER_PREFIX}${shortid.generate()}`;
|
||||
export const NATIVE_FILTER_DIVIDER_PREFIX = 'NATIVE_FILTER_DIVIDER-';
|
||||
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) =>
|
||||
config.map(filter => filter.id);
|
||||
|
|
|
@ -18,7 +18,12 @@
|
|||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
import { Filter, FilterConfiguration } from './types';
|
||||
import {
|
||||
Filter,
|
||||
FilterConfiguration,
|
||||
NativeFilterType,
|
||||
Divider,
|
||||
} from './types';
|
||||
import { ActiveTabs, DashboardLayout, RootState } from '../../types';
|
||||
import { TAB_TYPE } from '../../util/componentTypes';
|
||||
import { CascadeFilter } from './FilterBar/CascadeFilters/types';
|
||||
|
@ -41,10 +46,13 @@ export function useFilterConfigMap() {
|
|||
const filterConfig = useFilterConfiguration();
|
||||
return useMemo(
|
||||
() =>
|
||||
filterConfig.reduce((acc: Record<string, Filter>, filter: Filter) => {
|
||||
filterConfig.reduce(
|
||||
(acc: Record<string, Filter | Divider>, filter: Filter) => {
|
||||
acc[filter.id] = filter;
|
||||
return acc;
|
||||
}, {} as Record<string, Filter>),
|
||||
},
|
||||
{} as Record<string, Filter | Divider>,
|
||||
),
|
||||
[filterConfig],
|
||||
);
|
||||
}
|
||||
|
@ -89,29 +97,38 @@ function useIsFilterInScope() {
|
|||
// 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 in an active tab tree if all of it's ancestors of type TAB are active
|
||||
return (filter: CascadeFilter) =>
|
||||
// Dividers are always in scope
|
||||
return (filter: CascadeFilter | Divider) => {
|
||||
const isDivider = filter.type === NativeFilterType.DIVIDER;
|
||||
return (
|
||||
isDivider ||
|
||||
('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 isFilterInScope = useIsFilterInScope();
|
||||
|
||||
return useMemo(() => {
|
||||
let filtersInScope: CascadeFilter[] = [];
|
||||
const filtersOutOfScope: CascadeFilter[] = [];
|
||||
let filtersInScope: (CascadeFilter | Divider)[] = [];
|
||||
const filtersOutOfScope: (CascadeFilter | Divider)[] = [];
|
||||
|
||||
// we check native filters scopes only on dashboards with tabs
|
||||
if (!dashboardHasTabs) {
|
||||
filtersInScope = cascadeFilters;
|
||||
} else {
|
||||
cascadeFilters.forEach((filter: CascadeFilter) => {
|
||||
cascadeFilters.forEach(filter => {
|
||||
const filterInScope = isFilterInScope(filter);
|
||||
|
||||
if (filterInScope) {
|
||||
|
|
|
@ -39,6 +39,11 @@ export interface Target {
|
|||
// clarityColumns?: Column[];
|
||||
}
|
||||
|
||||
export enum NativeFilterType {
|
||||
NATIVE_FILTER = 'NATIVE_FILTER',
|
||||
DIVIDER = 'DIVIDER',
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
cascadeParentIds: string[];
|
||||
defaultDataMask: DataMask;
|
||||
|
@ -62,13 +67,14 @@ export interface Filter {
|
|||
requiredFirst?: boolean;
|
||||
tabsInScope?: string[];
|
||||
chartsInScope?: number[];
|
||||
type: NativeFilterType;
|
||||
type: typeof NativeFilterType.NATIVE_FILTER;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type FilterConfiguration = Filter[];
|
||||
|
||||
export enum NativeFilterType {
|
||||
NATIVE_FILTER = 'NATIVE_FILTER',
|
||||
SECTION = 'SECTION',
|
||||
export interface Divider {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: typeof NativeFilterType.DIVIDER;
|
||||
}
|
||||
|
||||
export type FilterConfiguration = Array<Filter | Divider>;
|
||||
|
|
Loading…
Reference in New Issue