From 6f8418b9922f32fecff039e146e81c0e83ba8265 Mon Sep 17 00:00:00 2001 From: geido Date: Thu, 2 Feb 2023 17:04:17 +0100 Subject: [PATCH 1/7] Initial implementation --- .../DetailsPanel/DetailsPanel.test.tsx | 2 +- .../FiltersBadge/DetailsPanel/index.tsx | 2 +- .../FilterIndicator/FilterIndicator.test.tsx | 2 +- .../FiltersBadge/FilterIndicator/index.tsx | 2 +- .../components/FiltersBadge/index.tsx | 2 +- .../FilterBarCrossFilters/Vertical.tsx | 155 +++++++++++++++ .../FilterBar/FilterBarCrossFilters/index.tsx | 72 +++++++ .../nativeFilters/FilterBar/Vertical.tsx | 188 ++++++++++-------- .../selectors.ts | 146 ++++++++------ 9 files changed, 426 insertions(+), 145 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/index.tsx rename superset-frontend/src/dashboard/components/{FiltersBadge => nativeFilters}/selectors.ts (85%) 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 8d2b121ff6b0..3c3e25ee56a8 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 3531ab1be9b2..022dab796875 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 fdded804b8ba..de49516a64e0 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 e7675a3ef0e3..2954474e64ea 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 fd21d07fc2b9..0532822530e6 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx @@ -31,7 +31,7 @@ import { IndicatorStatus, selectIndicatorsForChart, selectNativeIndicatorsForChart, -} from './selectors'; +} from '../nativeFilters/selectors'; import { ChartsState, DashboardInfo, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx new file mode 100644 index 000000000000..484404580ad3 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx @@ -0,0 +1,155 @@ +/** + * 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 Collapse from 'src/components/Collapse'; +import { styled, t, css } from '@superset-ui/core'; +import { Indicator } from 'src/dashboard/components/nativeFilters/selectors'; +import Icons from 'src/components/Icons'; +import { useDispatch } from 'react-redux'; +import { setFocusedNativeFilter } from 'src/dashboard/actions/nativeFilters'; +import { Tag } from 'src/components'; + +const StyledCollapse = styled(Collapse)` + ${({ theme }) => ` + .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.5}px; + } + .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box { + padding-top: 0; + } + `} +`; + +const StyledCrossFilter = styled.div` + ${({ theme }) => ` + margin-top: ${theme.gridUnit * 4}px; + `} +`; + +const StyledCrossFiltersTitle = styled.span` + font-size: ${({ theme }) => `${theme.typography.sizes.s}px;`}; +`; + +const StyledCrossFilterTitle = styled.div` + ${({ theme }) => ` + display: flex; + font-size: ${theme.typography.sizes.s}px; + color: ${theme.colors.grayscale.base}; + vertical-align: middle; + cursor: pointer; + align-items: center; + `} +`; +const StyledIconSearch = styled(Icons.SearchOutlined)` + ${({ theme }) => ` + color: ${theme.colors.grayscale.light1}; + margin-left: ${theme.gridUnit * 2}px; + &:hover { + color: ${theme.colors.grayscale.base}; + } + `} +`; + +const StyledCrossFilterTag = styled(Tag)` + ${({ theme }) => ` + margin-top: ${theme.gridUnit * 2}px; + `} +`; + +const ellipsisCss = css` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: middle; +`; + +const StyledCrossFilterValue = styled('b')` + ${({ theme }) => ` + max-width: ${theme.gridUnit * 30}px; + ${ellipsisCss} + `} +`; + +const StyledCrossFilterColumn = styled('span')` + ${({ theme }) => ` + max-width: ${theme.gridUnit * 20}px; + ${ellipsisCss} + `} +`; + +const FilterBarCrossFiltersVertical = (props: { + crossFilters: Indicator[]; +}) => { + const { crossFilters } = props; + const dispatch = useDispatch(); + const onHighlightFilterSource = useCallback( + (path?: string[]) => { + if (path) { + dispatch(setFocusedNativeFilter(path[0])); + } + }, + [dispatch], + ); + + const crossFiltersIndicators = crossFilters.map(filter => ( + + onHighlightFilterSource(filter.path)} + role="button" + tabIndex={0} + > + {filter.name} + + {(filter.column || filter.value) && ( + + {filter.column} + {filter.value} + + )} + + )); + + return ( + + + {t('Cross-filters')} + + } + > + {crossFiltersIndicators} + + + ); +}; + +export default FilterBarCrossFiltersVertical; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/index.tsx new file mode 100644 index 000000000000..8fc639aad882 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/index.tsx @@ -0,0 +1,72 @@ +/** + * 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 { + FilterBarOrientation, + DashboardInfo, + DashboardLayout, + RootState, +} from 'src/dashboard/types'; +import { DataMaskStateWithId } from '@superset-ui/core'; +import { + selectChartCrossFilters, + Indicator, +} from 'src/dashboard/components/nativeFilters/selectors'; +import { useSelector } from 'react-redux'; +import FilterBarCrossFiltersVertical from './Vertical'; + +const FilterBarCrossFilters = (props: { + orientation: FilterBarOrientation; +}) => { + const { orientation } = props; + const dataMask = useSelector( + state => state.dataMask, + ); + const dashboardInfo = useSelector( + state => state.dashboardInfo, + ); + const dashboardLayout = useSelector( + state => state.dashboardLayout.present, + ); + const chartConfiguration = dashboardInfo.metadata?.chart_configuration; + const chartsIds = Object.keys(chartConfiguration); + let selectedCrossFilters: Indicator[] = []; + + chartsIds.forEach(id => { + const chartId = Number(id); + const crossFilters = selectChartCrossFilters( + dataMask, + chartId, + dashboardLayout, + chartConfiguration, + ); + selectedCrossFilters = [...selectedCrossFilters, ...crossFilters]; + }); + + if (!selectedCrossFilters.length) return null; + + return orientation === FilterBarOrientation.VERTICAL ? ( + + ) : ( + Horizontal + ); +}; + +export default FilterBarCrossFilters; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index 2a6b7178362d..168994db0c46 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -34,6 +34,7 @@ import { AntdTabs } from 'src/components'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import Loading from 'src/components/Loading'; import { EmptyStateSmall } from 'src/components/EmptyState'; +import { FilterBarOrientation } from 'src/dashboard/types'; import { getFilterBarTestId } from './utils'; import { TabIds, VerticalBarProps } from './types'; import FilterSets from './FilterSets'; @@ -41,6 +42,7 @@ import { useFilterSets } from './state'; import EditSection from './FilterSets/EditSection'; import Header from './Header'; import FilterControls from './FilterControls/FilterControls'; +import FilterBarCrossFilters from './FilterBarCrossFilters'; const BarWrapper = styled.div<{ width: number }>` width: ${({ theme }) => theme.gridUnit * 8}px; @@ -191,6 +193,101 @@ const VerticalFilterBar: React.FC = ({ const numberOfFilters = nativeFilterValues.length; + const filterControls = useMemo( + () => + filterValues.length === 0 ? ( + + + + ) : ( + + + + ), + [ + canEdit, + dataMaskSelected, + filterValues.length, + focusedFilterId, + 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 85% rename from superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts rename to superset-frontend/src/dashboard/components/nativeFilters/selectors.ts index c0916b99607b..d84b755a11d7 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) { @@ -214,9 +214,84 @@ 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, +): Indicator[] => { + 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 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, + ); + 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); + } + + return crossFilterIndicators; +}; + const cachedNativeIndicatorsForChart = {}; const cachedNativeFilterDataForChart: any = {}; -const defaultChartConfig = {}; export const selectNativeIndicatorsForChart = ( nativeFilters: Filters, dataMask: DataMaskStateWithId, @@ -240,31 +315,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 +334,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 +347,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; From 4349dfd6a48021e119b6e43ffda579e3c407261d Mon Sep 17 00:00:00 2001 From: geido Date: Tue, 21 Feb 2023 12:39:59 +0100 Subject: [PATCH 2/7] Add vertical truncation --- .../FilterBarCrossFilters/Vertical.tsx | 122 ++++++++++++++---- .../FilterBar/FilterBarCrossFilters/index.tsx | 48 ++++--- .../FilterBarCrossFilters/selectors.ts | 56 ++++++++ .../nativeFilters/FilterBar/Horizontal.tsx | 40 +++++- .../components/nativeFilters/selectors.ts | 27 +++- 5 files changed, 236 insertions(+), 57 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx index 484404580ad3..82d48e0057c0 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx @@ -17,14 +17,17 @@ * under the License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import Collapse from 'src/components/Collapse'; -import { styled, t, css } from '@superset-ui/core'; -import { Indicator } from 'src/dashboard/components/nativeFilters/selectors'; +import { styled, t, css, useTheme } from '@superset-ui/core'; +import { IndicatorWithEmitterType } from 'src/dashboard/components/nativeFilters/selectors'; import Icons from 'src/components/Icons'; import { useDispatch } from 'react-redux'; import { setFocusedNativeFilter } from 'src/dashboard/actions/nativeFilters'; import { Tag } from 'src/components'; +import { Tooltip } from 'src/components/Tooltip'; +import useCSSTextTruncation from 'src/hooks/useTruncation/useCSSTextTruncation'; +import { FilterBarOrientation } from 'src/dashboard/types'; const StyledCollapse = styled(Collapse)` ${({ theme }) => ` @@ -87,23 +90,79 @@ const ellipsisCss = css` const StyledCrossFilterValue = styled('b')` ${({ theme }) => ` - max-width: ${theme.gridUnit * 30}px; - ${ellipsisCss} + max-width: ${theme.gridUnit * 25}px; `} + ${ellipsisCss} `; const StyledCrossFilterColumn = styled('span')` ${({ theme }) => ` - max-width: ${theme.gridUnit * 20}px; - ${ellipsisCss} + max-width: ${theme.gridUnit * 25}px; + padding-right: ${theme.gridUnit}px; `} + ${ellipsisCss} `; +const CrossFilterTag = (props: { + filter: IndicatorWithEmitterType; + removeCrossFilter: (filterId: number) => void; +}) => { + const { filter, removeCrossFilter } = props; + const [columnRef, columnIsTruncated] = + useCSSTextTruncation(); + const [valueRef, valueIsTruncated] = useCSSTextTruncation(); + + return ( + removeCrossFilter(filter.emitterId)} + > + + + {filter.column} + + + + + {filter.value} + + + + ); +}; + +const CrossFilterChartTitle = (props: { + title: string; + orientation: FilterBarOrientation; +}) => { + const { title, orientation } = props; + const [titleRef, titleIsTruncated] = useCSSTextTruncation(); + const theme = useTheme(); + return ( + + + {title} + + + ); +}; + const FilterBarCrossFiltersVertical = (props: { - crossFilters: Indicator[]; + crossFilters: IndicatorWithEmitterType[]; + orientation: FilterBarOrientation; + removeCrossFilter: (chartId: number) => void; }) => { - const { crossFilters } = props; + const { crossFilters, orientation, removeCrossFilter } = props; const dispatch = useDispatch(); + const onHighlightFilterSource = useCallback( (path?: string[]) => { if (path) { @@ -113,24 +172,33 @@ const FilterBarCrossFiltersVertical = (props: { [dispatch], ); - const crossFiltersIndicators = crossFilters.map(filter => ( - - onHighlightFilterSource(filter.path)} - role="button" - tabIndex={0} - > - {filter.name} - - {(filter.column || filter.value) && ( - - {filter.column} - {filter.value} - - )} - - )); + const crossFiltersIndicators = useMemo( + () => + crossFilters.map(filter => ( + + onHighlightFilterSource(filter.path)} + role="button" + tabIndex={0} + > + + + + + + {(filter.column || filter.value) && ( + + )} + + )), + [crossFilters, onHighlightFilterSource, orientation, removeCrossFilter], + ); return ( { const { orientation } = props; + const dispatch = useDispatch(); const dataMask = useSelector( state => state.dataMask, ); @@ -45,27 +44,34 @@ const FilterBarCrossFilters = (props: { const dashboardLayout = useSelector( state => state.dashboardLayout.present, ); - const chartConfiguration = dashboardInfo.metadata?.chart_configuration; - const chartsIds = Object.keys(chartConfiguration); - let selectedCrossFilters: Indicator[] = []; + const selectedCrossFilters = crossFiltersSelector({ + dataMask, + dashboardInfo, + dashboardLayout, + }); - chartsIds.forEach(id => { - const chartId = Number(id); - const crossFilters = selectChartCrossFilters( - dataMask, - chartId, - dashboardLayout, - chartConfiguration, + const handleRemoveCrossFilter = (chartId: number) => { + dispatch( + updateDataMask(chartId, { + extraFormData: { + filters: [], + }, + filterState: { + value: null, + selectedValues: null, + }, + }), ); - selectedCrossFilters = [...selectedCrossFilters, ...crossFilters]; - }); + }; if (!selectedCrossFilters.length) return null; - return orientation === FilterBarOrientation.VERTICAL ? ( - - ) : ( - Horizontal + return ( + ); }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts new file mode 100644 index 000000000000..353350841ad6 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts @@ -0,0 +1,56 @@ +/** + * 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 { + IndicatorWithEmitterType, + selectChartCrossFilters, +} from '../../selectors'; + +export const crossFiltersSelector = (props: { + dataMask: DataMaskStateWithId; + dashboardInfo: DashboardInfo; + dashboardLayout: DashboardLayout; +}): IndicatorWithEmitterType[] => { + const { dataMask, dashboardInfo, dashboardLayout } = props; + const chartConfiguration = dashboardInfo.metadata?.chart_configuration; + const chartsIds = Object.keys(chartConfiguration); + const shouldFilterEmitters = true; + + let selectedCrossFilters: IndicatorWithEmitterType[] = []; + + 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 IndicatorWithEmitterType[]), + ]; + } + return selectedCrossFilters; +}; + +export default crossFiltersSelector; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx index 7e786985e890..2cda17698e5a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx @@ -17,15 +17,30 @@ * under the License. */ -import React from 'react'; -import { styled, t } from '@superset-ui/core'; +import React, { useMemo } from 'react'; +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, + FilterBarOrientation, + 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 FilterBarCrossFilters from './FilterBarCrossFilters'; +import crossFiltersSelector from './FilterBarCrossFilters/selectors'; const HorizontalBar = styled.div` ${({ theme }) => ` @@ -96,7 +111,26 @@ const HorizontalFilterBar: React.FC = ({ focusedFilterId, 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/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts index d84b755a11d7..137154be2ae7 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts @@ -158,6 +158,8 @@ export type Indicator = { path?: string[]; }; +export type IndicatorWithEmitterType = Indicator & { emitterId: number }; + const cachedIndicatorsForChart = {}; const cachedDashboardFilterDataForChart = {}; // inspects redux state to find what the filter indicators should be shown for a given chart @@ -250,14 +252,23 @@ export const selectChartCrossFilters = ( chartId: number, dashboardLayout: Layout, chartConfiguration: ChartConfiguration = defaultChartConfig, -): Indicator[] => { + filterEmitter = false, +): Indicator[] | IndicatorWithEmitterType[] => { 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), - ) + .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; @@ -270,12 +281,12 @@ export const selectChartCrossFilters = ( const dashboardLayoutItem = dashboardLayoutValues.find( layoutItem => layoutItem?.meta?.chartId === chartConfig.id, ); - return { + const filterObject: Indicator = { column, name: dashboardLayoutItem?.meta?.sliceName as string, path: [ ...(dashboardLayoutItem?.parents ?? []), - dashboardLayoutItem?.id, + dashboardLayoutItem?.id || '', ], status: getStatus({ label, @@ -283,6 +294,10 @@ export const selectChartCrossFilters = ( }), value: label, }; + if (filterEmitter) { + (filterObject as IndicatorWithEmitterType).emitterId = chartId; + } + return filterObject; }) .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied); } From e936866486ff670b30cff0dfa659be5eed745124 Mon Sep 17 00:00:00 2001 From: geido Date: Tue, 21 Feb 2023 16:15:54 +0100 Subject: [PATCH 3/7] Add horizontal mode --- .../FilterBarCrossFilters/CrossFilter.tsx | 229 ++++++++++++++++++ .../FilterBarCrossFilters/Vertical.tsx | 187 +++----------- .../FilterBar/FilterBarCrossFilters/index.tsx | 78 ------ .../FilterBarCrossFilters/selectors.ts | 11 +- .../FilterControls/FilterControls.tsx | 106 ++++++-- .../FiltersDropdownContent/index.tsx | 17 ++ .../nativeFilters/FilterBar/Horizontal.tsx | 10 +- .../nativeFilters/FilterBar/Vertical.tsx | 5 +- .../components/nativeFilters/selectors.ts | 6 +- 9 files changed, 375 insertions(+), 274 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx delete mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/index.tsx diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx new file mode 100644 index 000000000000..8dc6bf9b1e85 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx @@ -0,0 +1,229 @@ +/** + * 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 { styled, t, css, useTheme } from '@superset-ui/core'; +import { CrossFilterIndicator } from 'src/dashboard/components/nativeFilters/selectors'; +import Icons from 'src/components/Icons'; +import { useDispatch } from 'react-redux'; +import { setFocusedNativeFilter } from 'src/dashboard/actions/nativeFilters'; +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 { updateDataMask } from 'src/dataMask/actions'; + +const StyledCrossFilterTitle = styled.div` + ${({ theme }) => ` + display: flex; + font-size: ${theme.typography.sizes.s}px; + color: ${theme.colors.grayscale.base}; + vertical-align: middle; + cursor: pointer; + align-items: center; + `} +`; +const StyledIconSearch = styled(Icons.SearchOutlined)` + ${({ theme }) => ` + color: ${theme.colors.grayscale.light1}; + margin-left: ${theme.gridUnit * 2}px; + &:hover { + color: ${theme.colors.grayscale.base}; + } + `} +`; + +const ellipsisCss = css` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: middle; +`; + +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 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} + + + + ); +}; + +const CrossFilterChartTitle = (props: { + title: string; + orientation: FilterBarOrientation; +}) => { + const { title, orientation } = props; + const [titleRef, titleIsTruncated] = useCSSTextTruncation(); + const theme = useTheme(); + return ( + + + {title} + + + ); +}; + +const CrossFilter = (props: { + filter: CrossFilterIndicator; + orientation: FilterBarOrientation; + last?: boolean; +}) => { + const { filter, orientation, last } = props; + const theme = useTheme(); + const dispatch = useDispatch(); + + const onHighlightFilterSource = 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 ( +
+ onHighlightFilterSource(filter.path)} + role="button" + tabIndex={0} + > + + + + + + {(filter.column || filter.value) && ( + + )} + {last && ( + + )} +
+ ); +}; + +export default CrossFilter; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx index 82d48e0057c0..0ac5f42e7730 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx @@ -17,17 +17,18 @@ * under the License. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import Collapse from 'src/components/Collapse'; -import { styled, t, css, useTheme } from '@superset-ui/core'; -import { IndicatorWithEmitterType } from 'src/dashboard/components/nativeFilters/selectors'; -import Icons from 'src/components/Icons'; -import { useDispatch } from 'react-redux'; -import { setFocusedNativeFilter } from 'src/dashboard/actions/nativeFilters'; -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 { styled, t, DataMaskStateWithId } from '@superset-ui/core'; +import { useSelector } from 'react-redux'; +import { + DashboardInfo, + DashboardLayout, + FilterBarOrientation, + RootState, +} from 'src/dashboard/types'; +import CrossFilter from './CrossFilter'; +import crossFiltersSelector from './selectors'; const StyledCollapse = styled(Collapse)` ${({ theme }) => ` @@ -44,162 +45,42 @@ const StyledCollapse = styled(Collapse)` `} `; -const StyledCrossFilter = styled.div` - ${({ theme }) => ` - margin-top: ${theme.gridUnit * 4}px; - `} -`; - const StyledCrossFiltersTitle = styled.span` font-size: ${({ theme }) => `${theme.typography.sizes.s}px;`}; `; -const StyledCrossFilterTitle = styled.div` - ${({ theme }) => ` - display: flex; - font-size: ${theme.typography.sizes.s}px; - color: ${theme.colors.grayscale.base}; - vertical-align: middle; - cursor: pointer; - align-items: center; - `} -`; -const StyledIconSearch = styled(Icons.SearchOutlined)` - ${({ theme }) => ` - color: ${theme.colors.grayscale.light1}; - margin-left: ${theme.gridUnit * 2}px; - &:hover { - color: ${theme.colors.grayscale.base}; - } - `} -`; - -const StyledCrossFilterTag = styled(Tag)` - ${({ theme }) => ` - margin-top: ${theme.gridUnit * 2}px; - `} -`; - -const ellipsisCss = css` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: middle; -`; - -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 CrossFilterTag = (props: { - filter: IndicatorWithEmitterType; - removeCrossFilter: (filterId: number) => void; -}) => { - const { filter, removeCrossFilter } = props; - const [columnRef, columnIsTruncated] = - useCSSTextTruncation(); - const [valueRef, valueIsTruncated] = useCSSTextTruncation(); - - return ( - removeCrossFilter(filter.emitterId)} - > - - - {filter.column} - - - - - {filter.value} - - - +const FilterBarCrossFiltersVertical = () => { + const dataMask = useSelector( + state => state.dataMask, ); -}; - -const CrossFilterChartTitle = (props: { - title: string; - orientation: FilterBarOrientation; -}) => { - const { title, orientation } = props; - const [titleRef, titleIsTruncated] = useCSSTextTruncation(); - const theme = useTheme(); - return ( - - - {title} - - + const dashboardInfo = useSelector( + state => state.dashboardInfo, ); -}; - -const FilterBarCrossFiltersVertical = (props: { - crossFilters: IndicatorWithEmitterType[]; - orientation: FilterBarOrientation; - removeCrossFilter: (chartId: number) => void; -}) => { - const { crossFilters, orientation, removeCrossFilter } = props; - const dispatch = useDispatch(); - - const onHighlightFilterSource = useCallback( - (path?: string[]) => { - if (path) { - dispatch(setFocusedNativeFilter(path[0])); - } - }, - [dispatch], + const dashboardLayout = useSelector( + state => state.dashboardLayout.present, ); + const selectedCrossFilters = crossFiltersSelector({ + dataMask, + dashboardInfo, + dashboardLayout, + }); const crossFiltersIndicators = useMemo( () => - crossFilters.map(filter => ( - - onHighlightFilterSource(filter.path)} - role="button" - tabIndex={0} - > - - - - - - {(filter.column || filter.value) && ( - - )} - + selectedCrossFilters.map(filter => ( + )), - [crossFilters, onHighlightFilterSource, orientation, removeCrossFilter], + [selectedCrossFilters], ); + if (!selectedCrossFilters.length) { + return null; + } + return ( { - const { orientation } = props; - const dispatch = useDispatch(); - 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, - }); - - const handleRemoveCrossFilter = (chartId: number) => { - dispatch( - updateDataMask(chartId, { - extraFormData: { - filters: [], - }, - filterState: { - value: null, - selectedValues: null, - }, - }), - ); - }; - - if (!selectedCrossFilters.length) return null; - - return ( - - ); -}; - -export default FilterBarCrossFilters; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts index 353350841ad6..c0f45af5b3e6 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts @@ -19,22 +19,19 @@ import { DataMaskStateWithId } from '@superset-ui/core'; import { DashboardInfo, DashboardLayout } from 'src/dashboard/types'; -import { - IndicatorWithEmitterType, - selectChartCrossFilters, -} from '../../selectors'; +import { CrossFilterIndicator, selectChartCrossFilters } from '../../selectors'; export const crossFiltersSelector = (props: { dataMask: DataMaskStateWithId; dashboardInfo: DashboardInfo; dashboardLayout: DashboardLayout; -}): IndicatorWithEmitterType[] => { +}): CrossFilterIndicator[] => { const { dataMask, dashboardInfo, dashboardLayout } = props; const chartConfiguration = dashboardInfo.metadata?.chart_configuration; const chartsIds = Object.keys(chartConfiguration); const shouldFilterEmitters = true; - let selectedCrossFilters: IndicatorWithEmitterType[] = []; + let selectedCrossFilters: CrossFilterIndicator[] = []; for (let i = 0; i < chartsIds.length; i += 1) { const chartId = Number(chartsIds[i]); @@ -47,7 +44,7 @@ export const crossFiltersSelector = (props: { ); selectedCrossFilters = [ ...selectedCrossFilters, - ...(crossFilters as IndicatorWithEmitterType[]), + ...(crossFilters as CrossFilterIndicator[]), ]; } return selectedCrossFilters; 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 a621052d5143..4f04c699d0e2 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 '../FilterBarCrossFilters/selectors'; +import CrossFilter from '../FilterBarCrossFilters/CrossFilter'; type FilterControlsProps = { focusedFilterId?: string; @@ -76,6 +83,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, focusedFilterId, @@ -113,6 +143,20 @@ const FilterControls: FC = ({ [filtersWithValues, portalNodes], ); + const rendererCrossFilter = useCallback( + (crossFilter, orientation, last) => ( + + ), + [], + ); + const renderVerticalContent = () => ( <> {filtersInScope.map(renderer)} @@ -126,37 +170,53 @@ const FilterControls: FC = ({ ); - const items = useMemo( - () => - filtersInScope.map((filter, index) => ({ - id: filter.id, - element: ( -
- {renderer(filter, index)} -
- ), - })), - [filtersInScope, renderer], - ); + 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 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 activerOverflowedFilters = overflowedFiltersInScope.filter(filter => + isNativeFilterWithDataMask(filter), + ); + return [...activerOverflowedFilters, ...overflowedCrossFilters]; + }, [overflowedCrossFilters, overflowedFiltersInScope]); + const renderHorizontalContent = () => (
@@ -196,9 +256,11 @@ const FilterControls: FC = ({ (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 84710c94b5f0..c4c720710bbd 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 && ( ` width: ${({ theme }) => theme.gridUnit * 8}px; @@ -283,7 +282,7 @@ const VerticalFilterBar: React.FC = ({ const crossFilters = useMemo( () => isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ? ( - + ) : null, [], ); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts index 137154be2ae7..871a8d402ef8 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts @@ -158,7 +158,7 @@ export type Indicator = { path?: string[]; }; -export type IndicatorWithEmitterType = Indicator & { emitterId: number }; +export type CrossFilterIndicator = Indicator & { emitterId: number }; const cachedIndicatorsForChart = {}; const cachedDashboardFilterDataForChart = {}; @@ -253,7 +253,7 @@ export const selectChartCrossFilters = ( dashboardLayout: Layout, chartConfiguration: ChartConfiguration = defaultChartConfig, filterEmitter = false, -): Indicator[] | IndicatorWithEmitterType[] => { +): Indicator[] | CrossFilterIndicator[] => { let crossFilterIndicators: any = []; if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) { const dashboardLayoutValues = Object.values(dashboardLayout); @@ -295,7 +295,7 @@ export const selectChartCrossFilters = ( value: label, }; if (filterEmitter) { - (filterObject as IndicatorWithEmitterType).emitterId = chartId; + (filterObject as CrossFilterIndicator).emitterId = chartId; } return filterObject; }) From 335fa413c494172acce3866c2335257ad9eb83f5 Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Wed, 22 Feb 2023 11:45:16 +0100 Subject: [PATCH 4/7] Update superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx Co-authored-by: Kamil Gabryjelski --- .../FilterBar/FilterBarCrossFilters/CrossFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx index 8dc6bf9b1e85..dd1790552d57 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx @@ -57,7 +57,7 @@ const ellipsisCss = css` vertical-align: middle; `; -const StyledCrossFilterValue = styled('b')` +const StyledCrossFilterValue = styled.b` ${({ theme }) => ` max-width: ${theme.gridUnit * 25}px; `} From fa029d551aa67cabfd8ce4096a24c1a5626b23be Mon Sep 17 00:00:00 2001 From: geido Date: Thu, 23 Feb 2023 11:55:15 +0100 Subject: [PATCH 5/7] Split and add tests --- .../DashboardBuilder/DashboardBuilder.tsx | 6 +- .../CrossFilters/CrossFilter.test.tsx | 82 +++++++ .../FilterBar/CrossFilters/CrossFilter.tsx | 114 +++++++++ .../CrossFilters/CrossFilterTag.test.tsx | 61 +++++ .../FilterBar/CrossFilters/CrossFilterTag.tsx | 93 +++++++ .../CrossFilters/CrossFilterTitle.test.tsx | 52 ++++ .../CrossFilters/CrossFilterTitle.tsx | 87 +++++++ .../FilterBar/CrossFilters/Vertical.tsx | 46 ++++ .../CrossFilters/VerticalCollapse.test.tsx | 107 ++++++++ .../VerticalCollapse.tsx} | 60 +++-- .../selectors.ts | 0 .../FilterBar/CrossFilters/styles.ts | 28 +++ .../FilterBarCrossFilters/CrossFilter.tsx | 229 ------------------ .../FilterControls/FilterControls.tsx | 8 +- .../FiltersOutOfScopeCollapsible/index.tsx | 10 +- .../nativeFilters/FilterBar/Horizontal.tsx | 2 +- .../nativeFilters/FilterBar/Vertical.tsx | 4 +- 17 files changed, 720 insertions(+), 269 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.test.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTag.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.test.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/Vertical.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.test.tsx rename superset-frontend/src/dashboard/components/nativeFilters/FilterBar/{FilterBarCrossFilters/Vertical.tsx => CrossFilters/VerticalCollapse.tsx} (66%) rename superset-frontend/src/dashboard/components/nativeFilters/FilterBar/{FilterBarCrossFilters => CrossFilters}/selectors.ts (100%) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/styles.ts delete mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index f07fdb9c066c..3e55b9b1b2e8 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -447,6 +447,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) @@ -525,7 +528,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/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilter.test.tsx new file mode 100644 index 000000000000..85dc3bf70547 --- /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 000000000000..f03e777b2504 --- /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 000000000000..72f831dcfa39 --- /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 000000000000..e59070df7aff --- /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 000000000000..16987440a247 --- /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 000000000000..a5a6ed109688 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx @@ -0,0 +1,87 @@ +/** + * 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 }) => ` + color: ${theme.colors.grayscale.light1}; + margin-left: ${theme.gridUnit}px; + transition: 0.3s; + vertical-align: middle; + &: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 000000000000..93fb649d686f --- /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 000000000000..92aaf8cfc64a --- /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/FilterBarCrossFilters/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx similarity index 66% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx index 0ac5f42e7730..a748f9ed9977 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/VerticalCollapse.tsx @@ -19,25 +19,22 @@ import React, { useMemo } from 'react'; import Collapse from 'src/components/Collapse'; -import { styled, t, DataMaskStateWithId } from '@superset-ui/core'; -import { useSelector } from 'react-redux'; -import { - DashboardInfo, - DashboardLayout, - FilterBarOrientation, - RootState, -} from 'src/dashboard/types'; +import { styled, t, useTheme, css } from '@superset-ui/core'; +import { FilterBarOrientation } from 'src/dashboard/types'; import CrossFilter from './CrossFilter'; -import crossFiltersSelector from './selectors'; +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.5}px; + padding-top: ${theme.gridUnit * 3}px; } .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box { padding-top: 0; @@ -46,38 +43,29 @@ const StyledCollapse = styled(Collapse)` `; const StyledCrossFiltersTitle = styled.span` - font-size: ${({ theme }) => `${theme.typography.sizes.s}px;`}; + ${({ theme }) => ` + font-size: ${theme.typography.sizes.s}px; + `} `; -const FilterBarCrossFiltersVertical = () => { - 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, - }); - +const CrossFiltersVerticalCollapse = (props: { + crossFilters: CrossFilterIndicator[]; +}) => { + const { crossFilters } = props; + const theme = useTheme(); const crossFiltersIndicators = useMemo( () => - selectedCrossFilters.map(filter => ( + crossFilters.map(filter => ( )), - [selectedCrossFilters], + [crossFilters], ); - if (!selectedCrossFilters.length) { + if (!crossFilters.length) { return null; } @@ -96,9 +84,19 @@ const FilterBarCrossFiltersVertical = () => { } > {crossFiltersIndicators} + ); }; -export default FilterBarCrossFiltersVertical; +export default CrossFiltersVerticalCollapse; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts similarity index 100% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/selectors.ts rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/selectors.ts 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 000000000000..0eee141c1000 --- /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/FilterBarCrossFilters/CrossFilter.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx deleted file mode 100644 index dd1790552d57..000000000000 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarCrossFilters/CrossFilter.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/** - * 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 { styled, t, css, useTheme } from '@superset-ui/core'; -import { CrossFilterIndicator } from 'src/dashboard/components/nativeFilters/selectors'; -import Icons from 'src/components/Icons'; -import { useDispatch } from 'react-redux'; -import { setFocusedNativeFilter } from 'src/dashboard/actions/nativeFilters'; -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 { updateDataMask } from 'src/dataMask/actions'; - -const StyledCrossFilterTitle = styled.div` - ${({ theme }) => ` - display: flex; - font-size: ${theme.typography.sizes.s}px; - color: ${theme.colors.grayscale.base}; - vertical-align: middle; - cursor: pointer; - align-items: center; - `} -`; -const StyledIconSearch = styled(Icons.SearchOutlined)` - ${({ theme }) => ` - color: ${theme.colors.grayscale.light1}; - margin-left: ${theme.gridUnit * 2}px; - &:hover { - color: ${theme.colors.grayscale.base}; - } - `} -`; - -const ellipsisCss = css` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: middle; -`; - -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 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} - - - - ); -}; - -const CrossFilterChartTitle = (props: { - title: string; - orientation: FilterBarOrientation; -}) => { - const { title, orientation } = props; - const [titleRef, titleIsTruncated] = useCSSTextTruncation(); - const theme = useTheme(); - return ( - - - {title} - - - ); -}; - -const CrossFilter = (props: { - filter: CrossFilterIndicator; - orientation: FilterBarOrientation; - last?: boolean; -}) => { - const { filter, orientation, last } = props; - const theme = useTheme(); - const dispatch = useDispatch(); - - const onHighlightFilterSource = 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 ( -
- onHighlightFilterSource(filter.path)} - role="button" - tabIndex={0} - > - - - - - - {(filter.column || filter.value) && ( - - )} - {last && ( - - )} -
- ); -}; - -export default CrossFilter; 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 4f04c699d0e2..9a0e3f647614 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -59,8 +59,8 @@ import Icons from 'src/components/Icons'; import { FiltersOutOfScopeCollapsible } from '../FiltersOutOfScopeCollapsible'; import { useFilterControlFactory } from '../useFilterControlFactory'; import { FiltersDropdownContent } from '../FiltersDropdownContent'; -import crossFiltersSelector from '../FilterBarCrossFilters/selectors'; -import CrossFilter from '../FilterBarCrossFilters/CrossFilter'; +import crossFiltersSelector from '../CrossFilters/selectors'; +import CrossFilter from '../CrossFilters/CrossFilter'; type FilterControlsProps = { focusedFilterId?: string; @@ -211,10 +211,10 @@ const FilterControls: FC = ({ ); const activeOverflowedFiltersInScope = useMemo(() => { - const activerOverflowedFilters = overflowedFiltersInScope.filter(filter => + const activeOverflowedFilters = overflowedFiltersInScope.filter(filter => isNativeFilterWithDataMask(filter), ); - return [...activerOverflowedFilters, ...overflowedCrossFilters]; + return [...activeOverflowedFilters, ...overflowedCrossFilters]; }, [overflowedCrossFilters, overflowedFiltersInScope]); const renderHorizontalContent = () => ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx index a85f1907322c..898ca2768e25 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FiltersOutOfScopeCollapsible/index.tsx @@ -80,7 +80,15 @@ export const FiltersOutOfScopeCollapsible = ({ } > 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 ca16f98d20c2..1adc2100b515 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx @@ -34,7 +34,7 @@ import { getFilterBarTestId } from './utils'; import { HorizontalBarProps } from './types'; import FilterBarSettings from './FilterBarSettings'; import FilterConfigurationLink from './FilterConfigurationLink'; -import crossFiltersSelector from './FilterBarCrossFilters/selectors'; +import crossFiltersSelector from './CrossFilters/selectors'; const HorizontalBar = styled.div` ${({ theme }) => ` diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index f7096dfe492d..63285cacef05 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -41,7 +41,7 @@ import { useFilterSets } from './state'; import EditSection from './FilterSets/EditSection'; import Header from './Header'; import FilterControls from './FilterControls/FilterControls'; -import FilterBarCrossFiltersVertical from './FilterBarCrossFilters/Vertical'; +import CrossFiltersVertical from './CrossFilters/Vertical'; const BarWrapper = styled.div<{ width: number }>` width: ${({ theme }) => theme.gridUnit * 8}px; @@ -282,7 +282,7 @@ const VerticalFilterBar: React.FC = ({ const crossFilters = useMemo( () => isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ? ( - + ) : null, [], ); From 78f97711c4a4e602f12b055e57fd735fe1c01a72 Mon Sep 17 00:00:00 2001 From: geido Date: Thu, 23 Feb 2023 14:35:38 +0100 Subject: [PATCH 6/7] Fix vertical alignment --- .../CrossFilters/CrossFilterTitle.tsx | 15 ++-- .../FilterControls/FilterControls.tsx | 70 ++++++++++--------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx index a5a6ed109688..70f0ad2cd91b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/CrossFilterTitle.tsx @@ -37,12 +37,15 @@ const StyledCrossFilterTitle = styled.div` const StyledIconSearch = styled(Icons.SearchOutlined)` ${({ theme }) => ` - color: ${theme.colors.grayscale.light1}; - margin-left: ${theme.gridUnit}px; - transition: 0.3s; - vertical-align: middle; - &:hover { - color: ${theme.colors.grayscale.base}; + & > 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}; + } } `} `; 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 9a0e3f647614..1cbf13d79f1d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -143,20 +143,6 @@ const FilterControls: FC = ({ [filtersWithValues, portalNodes], ); - const rendererCrossFilter = useCallback( - (crossFilter, orientation, last) => ( - - ), - [], - ); - const renderVerticalContent = () => ( <> {filtersInScope.map(renderer)} @@ -170,6 +156,41 @@ const FilterControls: FC = ({ ); + const overflowedFiltersInScope = useMemo( + () => filtersInScope.filter(({ id }) => overflowedIds?.includes(id)), + [filtersInScope, overflowedIds], + ); + + const overflowedCrossFilters = useMemo( + () => + selectedCrossFilters.filter(({ emitterId, name }) => + overflowedIds?.includes(`${name}${emitterId}`), + ), + [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 @@ -197,26 +218,6 @@ const FilterControls: FC = ({ return [...crossFilters, ...nativeFiltersInScope]; }, [filtersInScope, renderer, rendererCrossFilter, selectedCrossFilters]); - const overflowedFiltersInScope = useMemo( - () => filtersInScope.filter(({ id }) => overflowedIds?.includes(id)), - [filtersInScope, overflowedIds], - ); - - const overflowedCrossFilters = useMemo( - () => - selectedCrossFilters.filter(({ emitterId, name }) => - overflowedIds?.includes(`${name}${emitterId}`), - ), - [overflowedIds, selectedCrossFilters], - ); - - const activeOverflowedFiltersInScope = useMemo(() => { - const activeOverflowedFilters = overflowedFiltersInScope.filter(filter => - isNativeFilterWithDataMask(filter), - ); - return [...activeOverflowedFilters, ...overflowedCrossFilters]; - }, [overflowedCrossFilters, overflowedFiltersInScope]); - const renderHorizontalContent = () => (
@@ -253,6 +254,7 @@ const FilterControls: FC = ({ } dropdownContent={ overflowedFiltersInScope.length || + overflowedCrossFilters.length || (filtersOutOfScope.length && showCollapsePanel) ? () => ( Date: Fri, 24 Feb 2023 16:10:10 +0100 Subject: [PATCH 7/7] Change empty copy --- .../components/nativeFilters/FilterBar/Vertical.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index 63285cacef05..3edf95e25850 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -197,11 +197,13 @@ const VerticalFilterBar: React.FC = ({ filterValues.length === 0 ? (