diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 9d6d3a51eb55f..99360801c5982 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -445,6 +445,9 @@ const DashboardBuilder: FC = () => { const fullSizeChartId = useSelector( state => state.dashboardState.fullSizeChartId, ); + const crossFiltersEnabled = isFeatureEnabled( + FeatureFlag.DASHBOARD_CROSS_FILTERS, + ); const filterBarOrientation = useSelector( ({ dashboardInfo }) => isFeatureEnabled(FeatureFlag.HORIZONTAL_FILTER_BAR) @@ -523,7 +526,8 @@ const DashboardBuilder: FC = () => { const filterSetEnabled = isFeatureEnabled( FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET, ); - const showFilterBar = nativeFiltersEnabled && !editMode; + const showFilterBar = + (crossFiltersEnabled || nativeFiltersEnabled) && !editMode; const offset = FILTER_BAR_HEADER_HEIGHT + diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx index 8d2b121ff6b0d..3c3e25ee56a8e 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/DetailsPanel.test.tsx @@ -19,7 +19,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { render, screen } from 'spec/helpers/testing-library'; -import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors'; +import { Indicator } from 'src/dashboard/components/nativeFilters/selectors'; import DetailsPanel from '.'; const createProps = () => ({ diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx index 3531ab1be9b25..022dab796875f 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx @@ -29,7 +29,7 @@ import { Reset, Title, } from 'src/dashboard/components/FiltersBadge/Styles'; -import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors'; +import { Indicator } from 'src/dashboard/components/nativeFilters/selectors'; import FilterIndicator from 'src/dashboard/components/FiltersBadge/FilterIndicator'; import { RootState } from 'src/dashboard/types'; diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx index fdded804b8ba0..de49516a64e09 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/FilterIndicator.test.tsx @@ -19,7 +19,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { render, screen } from 'spec/helpers/testing-library'; -import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors'; +import { Indicator } from 'src/dashboard/components/nativeFilters/selectors'; import FilterIndicator from '.'; const createProps = () => ({ diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx index e7675a3ef0e3d..2954474e64eac 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/FilterIndicator/index.tsx @@ -28,7 +28,7 @@ import { ItemIcon, Title, } from 'src/dashboard/components/FiltersBadge/Styles'; -import { Indicator } from 'src/dashboard/components/FiltersBadge/selectors'; +import { Indicator } from 'src/dashboard/components/nativeFilters/selectors'; export interface IndicatorProps { indicator: Indicator; diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx index c7748849a88a5..fb0718bf54cb4 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx @@ -24,12 +24,6 @@ import { DataMaskStateWithId, Filters } from '@superset-ui/core'; import Icons from 'src/components/Icons'; import { usePrevious } from 'src/hooks/usePrevious'; import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState'; -import { - ChartsState, - DashboardInfo, - DashboardLayout, - RootState, -} from 'src/dashboard/types'; import DetailsPanelPopover from './DetailsPanel'; import { Pill } from './Styles'; import { @@ -37,7 +31,13 @@ import { IndicatorStatus, selectIndicatorsForChart, selectNativeIndicatorsForChart, -} from './selectors'; +} from '../nativeFilters/selectors'; +import { + ChartsState, + DashboardInfo, + DashboardLayout, + RootState, +} from '../../types'; export interface FiltersBadgeProps { chartId: number; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx new file mode 100644 index 0000000000000..85dc3bf705472 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx @@ -0,0 +1,82 @@ +/** + * 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 { render, screen } from 'spec/helpers/testing-library'; +import { FilterBarOrientation } from 'src/dashboard/types'; +import { IndicatorStatus } from '../../selectors'; +import CrossFilter from './CrossFilter'; + +const mockedProps = { + filter: { + name: 'test', + emitterId: 1, + column: 'country_name', + value: 'Italy', + status: IndicatorStatus.CrossFilterApplied, + path: ['test-path'], + }, + orientation: FilterBarOrientation.HORIZONTAL, + last: false, +}; + +const setup = (props: typeof mockedProps) => + render(, { + useRedux: true, + }); + +test('CrossFilter should render', () => { + const { container } = setup(mockedProps); + expect(container).toBeInTheDocument(); +}); + +test('Title should render', () => { + setup(mockedProps); + expect(screen.getByText('test')).toBeInTheDocument(); +}); + +test('Search icon should be visible', () => { + setup(mockedProps); + expect( + screen.getByTestId('cross-filters-highlight-emitter'), + ).toBeInTheDocument(); +}); + +test('Column and value should be visible', () => { + setup(mockedProps); + expect(screen.getByText('country_name')).toBeInTheDocument(); + expect(screen.getByText('Italy')).toBeInTheDocument(); +}); + +test('Tag should be closable', () => { + setup(mockedProps); + expect(screen.getByRole('img', { name: 'close' })).toBeInTheDocument(); +}); + +test('Divider should not be visible', () => { + setup(mockedProps); + expect(screen.queryByTestId('cross-filters-divider')).not.toBeInTheDocument(); +}); + +test('Divider should be visible', () => { + setup({ + ...mockedProps, + last: true, + }); + expect(screen.getByTestId('cross-filters-divider')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.tsx new file mode 100644 index 0000000000000..f03e777b2504a --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.tsx @@ -0,0 +1,114 @@ +/** + * 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, { useCallback } from 'react'; +import { css, useTheme } from '@superset-ui/core'; +import { CrossFilterIndicator } from 'src/dashboard/components/nativeFilters/selectors'; +import { useDispatch } from 'react-redux'; +import { setFocusedNativeFilter } from 'src/dashboard/actions/nativeFilters'; +import { FilterBarOrientation } from 'src/dashboard/types'; +import { updateDataMask } from 'src/dataMask/actions'; +import CrossFilterTag from './CrossFilterTag'; +import CrossFilterTitle from './CrossFilterTitle'; + +const CrossFilter = (props: { + filter: CrossFilterIndicator; + orientation: FilterBarOrientation; + last?: boolean; +}) => { + const { filter, orientation, last } = props; + const theme = useTheme(); + const dispatch = useDispatch(); + + const handleHighlightFilterSource = useCallback( + (path?: string[]) => { + if (path) { + dispatch(setFocusedNativeFilter(path[0])); + } + }, + [dispatch], + ); + + const handleRemoveCrossFilter = (chartId: number) => { + dispatch( + updateDataMask(chartId, { + extraFormData: { + filters: [], + }, + filterState: { + value: null, + selectedValues: null, + }, + }), + ); + }; + + return ( +
+ handleHighlightFilterSource(filter.path)} + /> + {(filter.column || filter.value) && ( + + )} + {last && ( + + )} +
+ ); +}; + +export default CrossFilter; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.test.tsx new file mode 100644 index 0000000000000..72f831dcfa391 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.test.tsx @@ -0,0 +1,61 @@ +/** + * 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 userEvent from '@testing-library/user-event'; +import React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import { FilterBarOrientation } from 'src/dashboard/types'; +import { IndicatorStatus } from '../../selectors'; +import CrossFilterTag from './CrossFilterTag'; + +const mockedProps = { + filter: { + name: 'test', + emitterId: 1, + column: 'country_name', + value: 'Italy', + status: IndicatorStatus.CrossFilterApplied, + path: ['test-path'], + }, + orientation: FilterBarOrientation.HORIZONTAL, + removeCrossFilter: jest.fn(), +}; + +const setup = (props: typeof mockedProps) => + render(, { + useRedux: true, + }); + +test('CrossFilterTag should render', () => { + const { container } = setup(mockedProps); + expect(container).toBeInTheDocument(); +}); + +test('Column and value should be visible', () => { + setup(mockedProps); + expect(screen.getByText('country_name')).toBeInTheDocument(); + expect(screen.getByText('Italy')).toBeInTheDocument(); +}); + +test('Tag should be closable', () => { + setup(mockedProps); + const close = screen.getByRole('img', { name: 'close' }); + expect(close).toBeInTheDocument(); + userEvent.click(close); + expect(mockedProps.removeCrossFilter).toHaveBeenCalledWith(1); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.tsx new file mode 100644 index 0000000000000..e59070df7aff5 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.tsx @@ -0,0 +1,93 @@ +/** + * 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 { styled, css, useTheme } from '@superset-ui/core'; +import { CrossFilterIndicator } from 'src/dashboard/components/nativeFilters/selectors'; +import { Tag } from 'src/components'; +import { Tooltip } from 'src/components/Tooltip'; +import useCSSTextTruncation from 'src/hooks/useTruncation/useCSSTextTruncation'; +import { FilterBarOrientation } from 'src/dashboard/types'; +import { ellipsisCss } from './styles'; + +const StyledCrossFilterValue = styled.b` + ${({ theme }) => ` + max-width: ${theme.gridUnit * 25}px; + `} + ${ellipsisCss} +`; + +const StyledCrossFilterColumn = styled('span')` + ${({ theme }) => ` + max-width: ${theme.gridUnit * 25}px; + padding-right: ${theme.gridUnit}px; + `} + ${ellipsisCss} +`; + +const StyledTag = styled(Tag)` + ${({ theme }) => ` + border: 1px solid ${theme.colors.grayscale.light3}; + border-radius: 2px; + .anticon-close { + vertical-align: middle; + } + `} +`; + +const CrossFilterTag = (props: { + filter: CrossFilterIndicator; + orientation: FilterBarOrientation; + removeCrossFilter: (filterId: number) => void; +}) => { + const { filter, orientation, removeCrossFilter } = props; + const theme = useTheme(); + const [columnRef, columnIsTruncated] = + useCSSTextTruncation(); + const [valueRef, valueIsTruncated] = useCSSTextTruncation(); + + return ( + removeCrossFilter(filter.emitterId)} + > + + + {filter.column} + + + + + {filter.value} + + + + ); +}; + +export default CrossFilterTag; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.test.tsx new file mode 100644 index 0000000000000..16987440a247d --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.test.tsx @@ -0,0 +1,52 @@ +/** + * 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 userEvent from '@testing-library/user-event'; +import React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import { FilterBarOrientation } from 'src/dashboard/types'; +import CrossFilterTitle from './CrossFilterTitle'; + +const mockedProps = { + title: 'test-title', + orientation: FilterBarOrientation.HORIZONTAL, + onHighlightFilterSource: jest.fn(), +}; + +const setup = (props: typeof mockedProps) => + render(, { + useRedux: true, + }); + +test('CrossFilterTitle should render', () => { + const { container } = setup(mockedProps); + expect(container).toBeInTheDocument(); +}); + +test('Title should be visible', () => { + setup(mockedProps); + expect(screen.getByText('test-title')).toBeInTheDocument(); +}); + +test('Search icon should highlight emitter', () => { + setup(mockedProps); + const search = screen.getByTestId('cross-filters-highlight-emitter'); + expect(search).toBeInTheDocument(); + userEvent.click(search); + expect(mockedProps.onHighlightFilterSource).toHaveBeenCalled(); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx new file mode 100644 index 0000000000000..70f0ad2cd91b2 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx @@ -0,0 +1,90 @@ +/** + * 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 { t, css, styled, useTheme } from '@superset-ui/core'; +import { Tooltip } from 'src/components/Tooltip'; +import useCSSTextTruncation from 'src/hooks/useTruncation/useCSSTextTruncation'; +import { FilterBarOrientation } from 'src/dashboard/types'; +import Icons from 'src/components/Icons'; +import { ellipsisCss } from './styles'; + +const StyledCrossFilterTitle = styled.div` + ${({ theme }) => ` + display: flex; + font-size: ${theme.typography.sizes.s}px; + color: ${theme.colors.grayscale.base}; + vertical-align: middle; + align-items: center; + `} +`; + +const StyledIconSearch = styled(Icons.SearchOutlined)` + ${({ theme }) => ` + & > span.anticon.anticon-search { + color: ${theme.colors.grayscale.light1}; + margin-left: ${theme.gridUnit}px; + transition: 0.3s; + vertical-align: middle; + line-height: 0; + &:hover { + color: ${theme.colors.grayscale.base}; + } + } + `} +`; + +const CrossFilterChartTitle = (props: { + title: string; + orientation: FilterBarOrientation; + onHighlightFilterSource: () => void; +}) => { + const { title, orientation, onHighlightFilterSource } = props; + const [titleRef, titleIsTruncated] = useCSSTextTruncation(); + const theme = useTheme(); + return ( + + + + {title} + + + + + + + ); +}; + +export default CrossFilterChartTitle; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/Vertical.tsx new file mode 100644 index 0000000000000..93fb649d686f6 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/Vertical.tsx @@ -0,0 +1,46 @@ +/** + * 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 { DataMaskStateWithId } from '@superset-ui/core'; +import { useSelector } from 'react-redux'; +import { DashboardInfo, DashboardLayout, RootState } from 'src/dashboard/types'; +import crossFiltersSelector from './selectors'; +import VerticalCollapse from './VerticalCollapse'; + +const CrossFiltersVertical = () => { + const dataMask = useSelector( + state => state.dataMask, + ); + const dashboardInfo = useSelector( + state => state.dashboardInfo, + ); + const dashboardLayout = useSelector( + state => state.dashboardLayout.present, + ); + const selectedCrossFilters = crossFiltersSelector({ + dataMask, + dashboardInfo, + dashboardLayout, + }); + + return ; +}; + +export default CrossFiltersVertical; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.test.tsx new file mode 100644 index 0000000000000..92aaf8cfc64a0 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.test.tsx @@ -0,0 +1,107 @@ +/** + * 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 { render, screen } from 'spec/helpers/testing-library'; +import { IndicatorStatus } from '../../selectors'; +import VerticalCollapse from './VerticalCollapse'; + +const mockedProps = { + crossFilters: [ + { + name: 'test', + emitterId: 1, + column: 'country_name', + value: 'Italy', + status: IndicatorStatus.CrossFilterApplied, + path: ['test-path'], + }, + { + name: 'test-b', + emitterId: 2, + column: 'country_code', + value: 'IT', + status: IndicatorStatus.CrossFilterApplied, + path: ['test-path-2'], + }, + ], +}; + +const setup = (props: typeof mockedProps) => + render(, { + useRedux: true, + }); + +test('VerticalCollapse should render', () => { + const { container } = setup(mockedProps); + expect(container).toBeInTheDocument(); +}); + +test('Collapse with title should render', () => { + setup(mockedProps); + expect(screen.getByText('Cross-filters')).toBeInTheDocument(); +}); + +test('Collapse should not render when empty', () => { + setup({ + crossFilters: [], + }); + expect(screen.queryByText('Cross-filters')).not.toBeInTheDocument(); + expect(screen.queryByText('test')).not.toBeInTheDocument(); + expect(screen.queryByText('test-b')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('cross-filters-highlight-emitter'), + ).not.toBeInTheDocument(); + expect(screen.queryByRole('img', { name: 'close' })).not.toBeInTheDocument(); + expect(screen.queryByText('country_name')).not.toBeInTheDocument(); + expect(screen.queryByText('Italy')).not.toBeInTheDocument(); + expect(screen.queryByText('country_code')).not.toBeInTheDocument(); + expect(screen.queryByText('IT')).not.toBeInTheDocument(); + expect(screen.queryByTestId('cross-filters-divider')).not.toBeInTheDocument(); +}); + +test('Titles should be visible', () => { + setup(mockedProps); + expect(screen.getByText('test')).toBeInTheDocument(); + expect(screen.getByText('test-b')).toBeInTheDocument(); +}); + +test('Search icons should be visible', () => { + setup(mockedProps); + expect(screen.getAllByTestId('cross-filters-highlight-emitter')).toHaveLength( + 2, + ); +}); + +test('Tags should be visible', () => { + setup(mockedProps); + expect(screen.getByText('country_name')).toBeInTheDocument(); + expect(screen.getByText('Italy')).toBeInTheDocument(); + expect(screen.getByText('country_code')).toBeInTheDocument(); + expect(screen.getByText('IT')).toBeInTheDocument(); +}); + +test('Tags should be closable', () => { + setup(mockedProps); + expect(screen.getAllByRole('img', { name: 'close' })).toHaveLength(2); +}); + +test('Divider should be visible', () => { + setup(mockedProps); + expect(screen.getByTestId('cross-filters-divider')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx new file mode 100644 index 0000000000000..a748f9ed9977a --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx @@ -0,0 +1,102 @@ +/** + * 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, { useMemo } from 'react'; +import Collapse from 'src/components/Collapse'; +import { styled, t, useTheme, css } from '@superset-ui/core'; +import { FilterBarOrientation } from 'src/dashboard/types'; +import CrossFilter from './CrossFilter'; +import { CrossFilterIndicator } from '../../selectors'; + +const StyledCollapse = styled(Collapse)` + ${({ theme }) => ` + .ant-collapse-header { + margin-bottom: ${theme.gridUnit * 4}px; + } + .ant-collapse-item > .ant-collapse-header { + padding-bottom: 0; + } + .ant-collapse-item > .ant-collapse-header > .ant-collapse-arrow { + font-size: ${theme.typography.sizes.xs}px; + padding-top: ${theme.gridUnit * 3}px; + } + .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box { + padding-top: 0; + } + `} +`; + +const StyledCrossFiltersTitle = styled.span` + ${({ theme }) => ` + font-size: ${theme.typography.sizes.s}px; + `} +`; + +const CrossFiltersVerticalCollapse = (props: { + crossFilters: CrossFilterIndicator[]; +}) => { + const { crossFilters } = props; + const theme = useTheme(); + const crossFiltersIndicators = useMemo( + () => + crossFilters.map(filter => ( + + )), + [crossFilters], + ); + + if (!crossFilters.length) { + return null; + } + + return ( + + + {t('Cross-filters')} + + } + > + {crossFiltersIndicators} + + + + ); +}; + +export default CrossFiltersVerticalCollapse; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts new file mode 100644 index 0000000000000..c0f45af5b3e6c --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts @@ -0,0 +1,53 @@ +/** + * 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 { DataMaskStateWithId } from '@superset-ui/core'; +import { DashboardInfo, DashboardLayout } from 'src/dashboard/types'; +import { CrossFilterIndicator, selectChartCrossFilters } from '../../selectors'; + +export const crossFiltersSelector = (props: { + dataMask: DataMaskStateWithId; + dashboardInfo: DashboardInfo; + dashboardLayout: DashboardLayout; +}): CrossFilterIndicator[] => { + const { dataMask, dashboardInfo, dashboardLayout } = props; + const chartConfiguration = dashboardInfo.metadata?.chart_configuration; + const chartsIds = Object.keys(chartConfiguration); + const shouldFilterEmitters = true; + + let selectedCrossFilters: CrossFilterIndicator[] = []; + + for (let i = 0; i < chartsIds.length; i += 1) { + const chartId = Number(chartsIds[i]); + const crossFilters = selectChartCrossFilters( + dataMask, + chartId, + dashboardLayout, + chartConfiguration, + shouldFilterEmitters, + ); + selectedCrossFilters = [ + ...selectedCrossFilters, + ...(crossFilters as CrossFilterIndicator[]), + ]; + } + return selectedCrossFilters; +}; + +export default crossFiltersSelector; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/styles.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/styles.ts new file mode 100644 index 0000000000000..0eee141c1000b --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/styles.ts @@ -0,0 +1,28 @@ +/** + * 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 { css } from '@superset-ui/core'; + +export const ellipsisCss = css` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: middle; +`; 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 fa0bfeac6f22a..d4ec83c1e2abd 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -46,7 +46,12 @@ import { useDashboardHasTabs, useSelectFiltersInScope, } from 'src/dashboard/components/nativeFilters/state'; -import { FilterBarOrientation, RootState } from 'src/dashboard/types'; +import { + DashboardInfo, + DashboardLayout, + FilterBarOrientation, + RootState, +} from 'src/dashboard/types'; import DropdownContainer, { Ref as DropdownContainerRef, } from 'src/components/DropdownContainer'; @@ -54,6 +59,8 @@ import Icons from 'src/components/Icons'; import { FiltersOutOfScopeCollapsible } from '../FiltersOutOfScopeCollapsible'; import { useFilterControlFactory } from '../useFilterControlFactory'; import { FiltersDropdownContent } from '../FiltersDropdownContent'; +import crossFiltersSelector from '../CrossFilters/selectors'; +import CrossFilter from '../CrossFilters/CrossFilter'; import { useFilterOutlined } from '../useFilterOutlined'; type FilterControlsProps = { @@ -77,6 +84,29 @@ const FilterControls: FC = ({ const [overflowedIds, setOverflowedIds] = useState([]); const popoverRef = useRef(null); + const dataMask = useSelector( + state => state.dataMask, + ); + const dashboardInfo = useSelector( + state => state.dashboardInfo, + ); + const dashboardLayout = useSelector( + state => state.dashboardLayout.present, + ); + const isCrossFiltersEnabled = isFeatureEnabled( + FeatureFlag.DASHBOARD_CROSS_FILTERS, + ); + const selectedCrossFilters = useMemo( + () => + isCrossFiltersEnabled + ? crossFiltersSelector({ + dataMask, + dashboardInfo, + dashboardLayout, + }) + : [], + [dashboardInfo, dashboardLayout, dataMask, isCrossFiltersEnabled], + ); const { filterControlFactory, filtersWithValues } = useFilterControlFactory( dataMaskSelected, onFilterSelectionChange, @@ -126,37 +156,68 @@ const FilterControls: FC = ({ ); - const items = useMemo( - () => - filtersInScope.map((filter, index) => ({ - id: filter.id, - element: ( -
- {renderer(filter, index)} -
- ), - })), - [filtersInScope, renderer], - ); - const overflowedFiltersInScope = useMemo( () => filtersInScope.filter(({ id }) => overflowedIds?.includes(id)), [filtersInScope, overflowedIds], ); - const activeOverflowedFiltersInScope = useMemo( + const overflowedCrossFilters = useMemo( () => - overflowedFiltersInScope.filter(filter => - isNativeFilterWithDataMask(filter), + selectedCrossFilters.filter(({ emitterId, name }) => + overflowedIds?.includes(`${name}${emitterId}`), ), - [overflowedFiltersInScope], + [overflowedIds, selectedCrossFilters], + ); + + const activeOverflowedFiltersInScope = useMemo(() => { + const activeOverflowedFilters = overflowedFiltersInScope.filter(filter => + isNativeFilterWithDataMask(filter), + ); + return [...activeOverflowedFilters, ...overflowedCrossFilters]; + }, [overflowedCrossFilters, overflowedFiltersInScope]); + + const rendererCrossFilter = useCallback( + (crossFilter, orientation, last) => ( + 0 && + `${last.name}${last.emitterId}` === + `${crossFilter.name}${crossFilter.emitterId}` + } + /> + ), + [filtersInScope.length], ); + const items = useMemo(() => { + const crossFilters = selectedCrossFilters.map(c => ({ + // a combination of filter name and chart id to account + // for multiple cross filters from the same chart in the future + id: `${c.name}${c.emitterId}`, + element: rendererCrossFilter( + c, + FilterBarOrientation.HORIZONTAL, + selectedCrossFilters.at(-1), + ), + })); + const nativeFiltersInScope = filtersInScope.map((filter, index) => ({ + id: filter.id, + element: ( +
+ {renderer(filter, index)} +
+ ), + })); + return [...crossFilters, ...nativeFiltersInScope]; + }, [filtersInScope, renderer, rendererCrossFilter, selectedCrossFilters]); + const renderHorizontalContent = () => (
@@ -193,12 +254,15 @@ const FilterControls: FC = ({ } dropdownContent={ overflowedFiltersInScope.length || + overflowedCrossFilters.length || (filtersOutOfScope.length && showCollapsePanel) ? () => ( ) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx index 84710c94b5f09..c4c720710bbd8 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersDropdownContent/index.tsx @@ -19,19 +19,29 @@ import React, { ReactNode } from 'react'; import { css, Divider, Filter, SupersetTheme } from '@superset-ui/core'; +import { FilterBarOrientation } from 'src/dashboard/types'; import { FiltersOutOfScopeCollapsible } from '../FiltersOutOfScopeCollapsible'; +import { CrossFilterIndicator } from '../../selectors'; export interface FiltersDropdownContentProps { + overflowedCrossFilters: CrossFilterIndicator[]; filtersInScope: (Filter | Divider)[]; filtersOutOfScope: (Filter | Divider)[]; renderer: (filter: Filter | Divider, index: number) => ReactNode; + rendererCrossFilter: ( + crossFilter: CrossFilterIndicator, + orientation: FilterBarOrientation.VERTICAL, + last: CrossFilterIndicator, + ) => ReactNode; showCollapsePanel?: boolean; } export const FiltersDropdownContent = ({ + overflowedCrossFilters, filtersInScope, filtersOutOfScope, renderer, + rendererCrossFilter, showCollapsePanel, }: FiltersDropdownContentProps) => (
+ {overflowedCrossFilters.map(crossFilter => + rendererCrossFilter( + crossFilter, + FilterBarOrientation.VERTICAL, + overflowedCrossFilters.at(-1) as CrossFilterIndicator, + ), + )} {filtersInScope.map(renderer)} {showCollapsePanel && ( css` + font-size: ${theme.typography.sizes.s}px; + `} + > + {t('Filters out of scope (%d)', filtersOutOfScope.length)} + + } key="1" > {filtersOutOfScope.map(renderer)} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx index 9dc2d62d44f0f..badff642955a4 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx @@ -18,14 +18,23 @@ */ import React from 'react'; -import { styled, t } from '@superset-ui/core'; +import { + DataMaskStateWithId, + FeatureFlag, + isFeatureEnabled, + styled, + t, +} from '@superset-ui/core'; import Icons from 'src/components/Icons'; import Loading from 'src/components/Loading'; +import { DashboardInfo, DashboardLayout, RootState } from 'src/dashboard/types'; +import { useSelector } from 'react-redux'; import FilterControls from './FilterControls/FilterControls'; import { getFilterBarTestId } from './utils'; import { HorizontalBarProps } from './types'; import FilterBarSettings from './FilterBarSettings'; import FilterConfigurationLink from './FilterConfigurationLink'; +import crossFiltersSelector from './CrossFilters/selectors'; const HorizontalBar = styled.div` ${({ theme }) => ` @@ -95,7 +104,26 @@ const HorizontalFilterBar: React.FC = ({ isInitialized, onSelectionChange, }) => { - const hasFilters = filterValues.length > 0; + const dataMask = useSelector( + state => state.dataMask, + ); + const dashboardInfo = useSelector( + state => state.dashboardInfo, + ); + const dashboardLayout = useSelector( + state => state.dashboardLayout.present, + ); + const isCrossFiltersEnabled = isFeatureEnabled( + FeatureFlag.DASHBOARD_CROSS_FILTERS, + ); + const selectedCrossFilters = isCrossFiltersEnabled + ? crossFiltersSelector({ + dataMask, + dashboardInfo, + dashboardLayout, + }) + : []; + const hasFilters = filterValues.length > 0 || selectedCrossFilters.length > 0; return ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index 3150ed5e90652..265c5932ac880 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -41,6 +41,7 @@ import { useFilterSets } from './state'; import EditSection from './FilterSets/EditSection'; import Header from './Header'; import FilterControls from './FilterControls/FilterControls'; +import CrossFiltersVertical from './CrossFilters/Vertical'; const BarWrapper = styled.div<{ width: number }>` width: ${({ theme }) => theme.gridUnit * 8}px; @@ -190,6 +191,96 @@ const VerticalFilterBar: React.FC = ({ const numberOfFilters = nativeFilterValues.length; + const filterControls = useMemo( + () => + filterValues.length === 0 ? ( + + + + ) : ( + + + + ), + [canEdit, dataMaskSelected, filterValues.length, onSelectionChange], + ); + + const filterSetsTabs = useMemo( + () => ( + + + {editFilterSetId && ( + setEditFilterSetId(null)} + filterSetId={editFilterSetId} + /> + )} + {filterControls} + + + + + + ), + [ + dataMaskSelected, + editFilterSetId, + filterControls, + filterSetFilterValues.length, + isDisabled, + numberOfFilters, + onSelectionChange, + tab, + tabPaneStyle, + ], + ); + + const crossFilters = useMemo( + () => + isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ? ( + + ) : null, + [], + ); + return ( = ({
) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? ( - - - {editFilterSetId && ( - setEditFilterSetId(null)} - filterSetId={editFilterSetId} - /> - )} - {filterValues.length === 0 ? ( - - - - ) : ( - - - - )} - - - - - + <> + {crossFilters} + {filterSetsTabs} + ) : (
- {filterValues.length === 0 ? ( - - - - ) : ( - - - - )} + <> + {crossFilters} + {filterControls} +
)} {actions} diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts similarity index 80% rename from superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts rename to superset-frontend/src/dashboard/components/nativeFilters/selectors.ts index c0916b99607b0..871a8d402ef82 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts @@ -60,7 +60,7 @@ type Filter = { }; const extractLabel = (filter?: FilterState): string | null => { - if (filter?.label) { + if (filter?.label && !filter?.label?.includes(undefined)) { return filter.label; } if (filter?.value) { @@ -158,6 +158,8 @@ export type Indicator = { path?: string[]; }; +export type CrossFilterIndicator = Indicator & { emitterId: number }; + const cachedIndicatorsForChart = {}; const cachedDashboardFilterDataForChart = {}; // inspects redux state to find what the filter indicators should be shown for a given chart @@ -214,9 +216,97 @@ export const selectIndicatorsForChart = ( return indicators; }; +const getStatus = ({ + label, + column, + type = DataMaskType.NativeFilters, + rejectedColumns, + appliedColumns, +}: { + label: string | null; + column?: string; + type?: DataMaskType; + rejectedColumns?: Set; + appliedColumns?: Set; +}): IndicatorStatus => { + // a filter is only considered unset if it's value is null + const hasValue = label !== null; + if (type === DataMaskType.CrossFilters && hasValue) { + return IndicatorStatus.CrossFilterApplied; + } + if (!column && hasValue) { + // Filter without datasource + return IndicatorStatus.Applied; + } + if (column && rejectedColumns?.has(column)) + return IndicatorStatus.Incompatible; + if (column && appliedColumns?.has(column) && hasValue) { + return IndicatorStatus.Applied; + } + return IndicatorStatus.Unset; +}; + +const defaultChartConfig = {}; +export const selectChartCrossFilters = ( + dataMask: DataMaskStateWithId, + chartId: number, + dashboardLayout: Layout, + chartConfiguration: ChartConfiguration = defaultChartConfig, + filterEmitter = false, +): Indicator[] | CrossFilterIndicator[] => { + let crossFilterIndicators: any = []; + if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) { + const dashboardLayoutValues = Object.values(dashboardLayout); + crossFilterIndicators = Object.values(chartConfiguration) + .filter(chartConfig => { + const inScope = + chartConfig.crossFilters?.chartsInScope?.includes(chartId); + if (!filterEmitter && inScope) { + return true; + } + if (filterEmitter && !inScope) { + return true; + } + return false; + }) + .map(chartConfig => { + const filterState = dataMask[chartConfig.id]?.filterState; + const extraFormData = dataMask[chartConfig.id]?.extraFormData; + const label = extractLabel(filterState); + const filtersState = filterState?.filters; + const column = + extraFormData?.filters?.[0]?.col || + (filtersState && Object.keys(filtersState)[0]); + + const dashboardLayoutItem = dashboardLayoutValues.find( + layoutItem => layoutItem?.meta?.chartId === chartConfig.id, + ); + const filterObject: Indicator = { + column, + name: dashboardLayoutItem?.meta?.sliceName as string, + path: [ + ...(dashboardLayoutItem?.parents ?? []), + dashboardLayoutItem?.id || '', + ], + status: getStatus({ + label, + type: DataMaskType.CrossFilters, + }), + value: label, + }; + if (filterEmitter) { + (filterObject as CrossFilterIndicator).emitterId = chartId; + } + return filterObject; + }) + .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied); + } + + return crossFilterIndicators; +}; + const cachedNativeIndicatorsForChart = {}; const cachedNativeFilterDataForChart: any = {}; -const defaultChartConfig = {}; export const selectNativeIndicatorsForChart = ( nativeFilters: Filters, dataMask: DataMaskStateWithId, @@ -240,31 +330,6 @@ export const selectNativeIndicatorsForChart = ( ) { return cachedNativeIndicatorsForChart[chartId]; } - const getStatus = ({ - label, - column, - type = DataMaskType.NativeFilters, - }: { - label: string | null; - column?: string; - type?: DataMaskType; - }): IndicatorStatus => { - // a filter is only considered unset if it's value is null - const hasValue = label !== null; - if (type === DataMaskType.CrossFilters && hasValue) { - return IndicatorStatus.CrossFilterApplied; - } - if (!column && hasValue) { - // Filter without datasource - return IndicatorStatus.Applied; - } - if (column && rejectedColumns.has(column)) - return IndicatorStatus.Incompatible; - if (column && appliedColumns.has(column) && hasValue) { - return IndicatorStatus.Applied; - } - return IndicatorStatus.Unset; - }; let nativeFilterIndicators: any = []; if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) { @@ -284,7 +349,12 @@ export const selectNativeIndicatorsForChart = ( column, name: nativeFilter.name, path: [nativeFilter.id], - status: getStatus({ label, column }), + status: getStatus({ + label, + column, + rejectedColumns, + appliedColumns, + }), value: label, }; }); @@ -292,35 +362,12 @@ export const selectNativeIndicatorsForChart = ( let crossFilterIndicators: any = []; if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) { - const dashboardLayoutValues = Object.values(dashboardLayout); - crossFilterIndicators = Object.values(chartConfiguration) - .filter(chartConfig => - chartConfig.crossFilters?.chartsInScope?.includes(chartId), - ) - .map(chartConfig => { - const filterState = dataMask[chartConfig.id]?.filterState; - const label = extractLabel(filterState); - const filtersState = filterState?.filters; - const column = filtersState && Object.keys(filtersState)[0]; - - const dashboardLayoutItem = dashboardLayoutValues.find( - layoutItem => layoutItem?.meta?.chartId === chartConfig.id, - ); - return { - column, - name: dashboardLayoutItem?.meta?.sliceName as string, - path: [ - ...(dashboardLayoutItem?.parents ?? []), - dashboardLayoutItem?.id, - ], - status: getStatus({ - label, - type: DataMaskType.CrossFilters, - }), - value: label, - }; - }) - .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied); + crossFilterIndicators = selectChartCrossFilters( + dataMask, + chartId, + dashboardLayout, + chartConfiguration, + ); } const indicators = crossFilterIndicators.concat(nativeFilterIndicators); cachedNativeIndicatorsForChart[chartId] = indicators;