From 0922c3ff2dd79e68e633ea5fe1c17fa16307fd8a Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Wed, 23 Feb 2022 16:59:40 +0100 Subject: [PATCH] feat(native-filters): Implement filter cards (#18874) * feat(native-filters): Filter cards first pass * Add dependencies row and lots of improvements * Display tooltip correctly * Styling fix * Remove accidentaly added changes * Address comments * Fix type * Fixes --- .../FilterControls/FilterControls.tsx | 4 - .../FilterCard/DependenciesRow.tsx | 113 ++++++++++++++ .../FilterCard/FilterCardContent.tsx | 34 +++++ .../nativeFilters/FilterCard/NameRow.tsx | 50 ++++++ .../nativeFilters/FilterCard/ScopeRow.tsx | 68 +++++++++ .../nativeFilters/FilterCard/Styles.ts | 90 +++++++++++ .../FilterCard/TooltipWithTruncation.tsx | 37 +++++ .../nativeFilters/FilterCard/TypeRow.tsx | 35 +++++ .../nativeFilters/FilterCard/index.tsx | 50 ++++++ .../nativeFilters/FilterCard/types.ts | 36 +++++ .../FilterCard/useFilterDependencies.ts | 34 +++++ .../FilterCard/useFilterScope.ts | 143 ++++++++++++++++++ .../nativeFilters/FilterCard/useTruncation.ts | 58 +++++++ .../dashboard/containers/DashboardPage.tsx | 6 +- superset-frontend/src/dashboard/styles.ts | 44 ++++++ 15 files changed, 797 insertions(+), 5 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/FilterCardContent.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/Styles.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TooltipWithTruncation.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/index.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterDependencies.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterScope.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useTruncation.ts create mode 100644 superset-frontend/src/dashboard/styles.ts 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 dd1bfd97110..d0591dfa72d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -44,10 +44,6 @@ const Wrapper = styled.div` padding: ${({ theme }) => theme.gridUnit * 4}px; // 108px padding to make room for buttons with position: absolute padding-bottom: ${({ theme }) => theme.gridUnit * 27}px; - - &:hover { - cursor: pointer; - } `; type FilterControlsProps = { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx new file mode 100644 index 00000000000..c585fc40e41 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx @@ -0,0 +1,113 @@ +/** + * 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, useMemo, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { css, t, useTheme } from '@superset-ui/core'; +import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState'; +import Icons from 'src/components/Icons'; +import { + DependencyItem, + Row, + RowLabel, + RowTruncationCount, + RowValue, + TooltipList, +} from './Styles'; +import { useFilterDependencies } from './useFilterDependencies'; +import { useTruncation } from './useTruncation'; +import { DependencyValueProps, FilterCardRowProps } from './types'; +import { TooltipWithTruncation } from './TooltipWithTruncation'; + +const DependencyValue = ({ dependency, label }: DependencyValueProps) => { + const dispatch = useDispatch(); + const handleClick = useCallback(() => { + dispatch(setDirectPathToChild([dependency.id])); + }, [dependency.id, dispatch]); + return ( + + {label} + + ); +}; + +export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => { + const dependencies = useFilterDependencies(filter); + const dependenciesRef = useRef(null); + const [elementsTruncated, hasHiddenElements] = useTruncation(dependenciesRef); + const theme = useTheme(); + + const tooltipText = useMemo( + () => + elementsTruncated > 0 && dependencies ? ( + + {dependencies.map(dependency => ( +
  • + +
  • + ))} +
    + ) : null, + [elementsTruncated, dependencies], + ); + + if (!Array.isArray(dependencies) || dependencies.length === 0) { + return null; + } + return ( + + + {t('Dependent on')}{' '} + + + + + + + {dependencies.map((dependency, index) => ( + + ))} + + {hasHiddenElements && ( + +{elementsTruncated} + )} + + + ); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/FilterCardContent.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/FilterCardContent.tsx new file mode 100644 index 00000000000..41696ed98f8 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/FilterCardContent.tsx @@ -0,0 +1,34 @@ +/** + * 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 { Filter } from '@superset-ui/core'; +import { ScopeRow } from './ScopeRow'; +import { DependenciesRow } from './DependenciesRow'; +import { NameRow } from './NameRow'; +import { TypeRow } from './TypeRow'; + +export const FilterCardContent = ({ filter }: { filter: Filter }) => ( +
    + + + + +
    +); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx new file mode 100644 index 00000000000..05cb8119487 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx @@ -0,0 +1,50 @@ +/** + * 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, { useRef } from 'react'; +import { css, SupersetTheme } from '@superset-ui/core'; +import Icons from 'src/components/Icons'; +import { Row, FilterName } from './Styles'; +import { FilterCardRowProps } from './types'; +import { useTruncation } from './useTruncation'; +import { TooltipWithTruncation } from './TooltipWithTruncation'; + +export const NameRow = ({ filter }: FilterCardRowProps) => { + const filterNameRef = useRef(null); + const [elementsTruncated] = useTruncation(filterNameRef); + return ( + + css` + margin-bottom: ${theme.gridUnit * 3}px; + ` + } + > + + css` + margin-right: ${theme.gridUnit}px; + ` + } + /> + + {filter.name} + + + ); +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx new file mode 100644 index 00000000000..8f005874d23 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx @@ -0,0 +1,68 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useMemo, useRef } from 'react'; +import { t } from '@superset-ui/core'; +import { useFilterScope } from './useFilterScope'; +import { + Row, + RowLabel, + RowTruncationCount, + RowValue, + TooltipList, +} from './Styles'; +import { useTruncation } from './useTruncation'; +import { FilterCardRowProps } from './types'; +import { TooltipWithTruncation } from './TooltipWithTruncation'; + +export const ScopeRow = React.memo(({ filter }: FilterCardRowProps) => { + const scope = useFilterScope(filter); + const scopeRef = useRef(null); + + const [elementsTruncated, hasHiddenElements] = useTruncation(scopeRef); + const tooltipText = useMemo( + () => + elementsTruncated > 0 && scope ? ( + + {scope.map(val => ( +
  • {val}
  • + ))} +
    + ) : null, + [elementsTruncated, scope], + ); + + if (!Array.isArray(scope) || scope.length === 0) { + return null; + } + return ( + + {t('Scope')} + + + {scope.map((element, index) => ( + {index === 0 ? element : `, ${element}`} + ))} + + {hasHiddenElements > 0 && ( + +{elementsTruncated} + )} + + + ); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/Styles.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/Styles.ts new file mode 100644 index 00000000000..9799a0d333b --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/Styles.ts @@ -0,0 +1,90 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { css, styled } from '@superset-ui/core'; + +export const Row = styled.div` + ${({ theme }) => css` + display: flex; + align-items: center; + margin: ${theme.gridUnit}px 0; + font-size: ${theme.typography.sizes.s}px; + + &:first-of-type { + margin-top: 0; + } + + &:last-of-type { + margin-bottom: 0; + } + + & .ant-tooltip-open { + display: inline-flex; + } + `}; +`; + +export const RowLabel = styled.span` + ${({ theme }) => css` + color: ${theme.colors.grayscale.base}; + padding-right: ${theme.gridUnit * 4}px; + margin-right: auto; + text-transform: uppercase; + white-space: nowrap; + `}; +`; + +export const RowValue = styled.div` + ${({ theme }) => css` + color: ${theme.colors.grayscale.dark1}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline; + `}; +`; + +export const FilterName = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const DependencyItem = styled.span` + text-decoration: underline; + cursor: pointer; +`; + +export const RowTruncationCount = styled.span` + ${({ theme }) => css` + color: ${theme.colors.primary.base}; + `} +`; + +export const TooltipList = styled.ul` + ${({ theme }) => css` + padding-left: ${theme.gridUnit * 3}px; + margin-bottom: 0; + `}; +`; + +export const TooltipTrigger = styled.div` + min-width: 0; + display: inline-flex; + white-space: nowrap; +`; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TooltipWithTruncation.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TooltipWithTruncation.tsx new file mode 100644 index 00000000000..cfb36013a74 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TooltipWithTruncation.tsx @@ -0,0 +1,37 @@ +/** + * 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 { TooltipProps } from 'antd/lib/tooltip'; +import { Tooltip } from 'src/components/Tooltip'; +import { TooltipTrigger } from './Styles'; + +export const TooltipWithTruncation = ({ + title, + children, + ...props +}: TooltipProps) => ( + + {children} + +); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx new file mode 100644 index 00000000000..f276c32f283 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useMemo } from 'react'; +import { getChartMetadataRegistry, t } from '@superset-ui/core'; +import { Row, RowLabel, RowValue } from './Styles'; +import { FilterCardRowProps } from './types'; + +export const TypeRow = ({ filter }: FilterCardRowProps) => { + const metadata = useMemo( + () => getChartMetadataRegistry().get(filter.filterType), + [filter.filterType], + ); + return ( + + {t('Filter type')} + {metadata?.name} + + ); +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/index.tsx new file mode 100644 index 00000000000..6f544fc95e8 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/index.tsx @@ -0,0 +1,50 @@ +/** + * 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, { useState } from 'react'; +import Popover from 'src/components/Popover'; +import { FilterCardContent } from './FilterCardContent'; +import { FilterCardProps } from './types'; + +export const FilterCard = ({ + children, + filter, + getPopupContainer, + isVisible: externalIsVisible = true, +}: FilterCardProps) => { + const [internalIsVisible, setInternalIsVisible] = useState(false); + + return ( + { + setInternalIsVisible(visible); + }} + visible={externalIsVisible && internalIsVisible} + content={} + getPopupContainer={getPopupContainer ?? (() => document.body)} + destroyTooltipOnHide + > + {children} + + ); +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts new file mode 100644 index 00000000000..dd5bdf477be --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts @@ -0,0 +1,36 @@ +/** + * 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 { ReactNode } from 'react'; +import { Filter } from '@superset-ui/core'; + +export interface FilterCardProps { + children: ReactNode; + filter: Filter; + getPopupContainer?: (node: HTMLElement) => HTMLElement; + isVisible?: boolean; +} + +export interface FilterCardRowProps { + filter: Filter; +} + +export interface DependencyValueProps { + dependency: Filter; + label: string; +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterDependencies.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterDependencies.ts new file mode 100644 index 00000000000..60966658d61 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterDependencies.ts @@ -0,0 +1,34 @@ +/** + * 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 { ensureIsArray, Filter } from '@superset-ui/core'; +import { useSelector } from 'react-redux'; +import { RootState } from 'src/dashboard/types'; + +export const useFilterDependencies = (filter: Filter) => { + const filterDependencyIds = ensureIsArray(filter.cascadeParentIds); + return useSelector(state => { + if (filterDependencyIds.length === 0) { + return []; + } + return filterDependencyIds.reduce((acc: Filter[], filterDependencyId) => { + acc.push(state.nativeFilters.filters[filterDependencyId]); + return acc; + }, []); + }); +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterScope.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterScope.ts new file mode 100644 index 00000000000..f713076168c --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useFilterScope.ts @@ -0,0 +1,143 @@ +/** + * 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 { Filter, t } from '@superset-ui/core'; +import { useSelector } from 'react-redux'; +import { + ChartsState, + Layout, + LayoutItem, + RootState, +} from 'src/dashboard/types'; +import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants'; +import { CHART_TYPE } from 'src/dashboard/util/componentTypes'; +import { useMemo } from 'react'; + +const extractTabLabel = (tab?: LayoutItem) => + tab?.meta?.text || tab?.meta?.defaultText; +const extractChartLabel = (chart?: LayoutItem) => + chart?.meta?.sliceNameOverride || chart?.meta?.sliceName || chart?.id; + +const useCharts = () => { + const charts = useSelector(state => state.charts); + return useMemo(() => Object.values(charts), [charts]); +}; + +export const useFilterScope = (filter: Filter) => { + const layout = useSelector( + state => state.dashboardLayout.present, + ); + const charts = useCharts(); + + return useMemo(() => { + let topLevelTabs: string[] | undefined; + const topElementId = layout[DASHBOARD_ROOT_ID].children[0]; + if (topElementId.startsWith('TABS-')) { + topLevelTabs = layout[topElementId].children; + } + + // no charts in scope + if (filter.scope.rootPath.length === 0) { + return undefined; + } + + // all charts in scope + // no charts excluded and no top level tabs + // OR no charts excluded and every top level tab is in rootPath + if ( + filter.scope.excluded.length === 0 && + (filter.scope.rootPath[0] === DASHBOARD_ROOT_ID || + (topLevelTabs && + topLevelTabs.every(topLevelTab => + filter.scope.rootPath.includes(topLevelTab), + ))) + ) { + return [t('All charts')]; + } + + // no charts excluded and not every top level tab in scope + // returns "TAB1, TAB2" + if (filter.scope.excluded.length === 0 && topLevelTabs) { + return filter.scope.rootPath.map(tabId => extractTabLabel(layout[tabId])); + } + + const layoutCharts = Object.values(layout).filter( + layoutElement => layoutElement.type === CHART_TYPE, + ); + + // no top level tabs, charts excluded + // returns "CHART1, CHART2" + if (filter.scope.rootPath[0] === DASHBOARD_ROOT_ID) { + return charts + .filter(chart => !filter.scope.excluded.includes(chart.id)) + .map(chart => { + const layoutElement = layoutCharts.find( + layoutChart => layoutChart.meta.chartId === chart.id, + ); + return extractChartLabel(layoutElement); + }) + .filter(Boolean); + } + + // top level tabs, charts excluded + // returns "TAB1, TAB2, CHART1" + if (topLevelTabs) { + // We start assuming that all charts are in scope for all tabs in the root path + const topLevelTabsInFullScope = [...filter.scope.rootPath]; + const layoutChartElementsInTabsInScope = layoutCharts.filter(element => + element.parents.some(parent => + topLevelTabsInFullScope.includes(parent), + ), + ); + // Exclude the tabs that contain excluded charts + filter.scope.excluded.forEach(chartId => { + const excludedIndex = topLevelTabsInFullScope.findIndex(tabId => + layoutChartElementsInTabsInScope + .find(chart => chart.meta.chartId === chartId) + ?.parents.includes(tabId), + ); + if (excludedIndex > -1) { + topLevelTabsInFullScope.splice(excludedIndex, 1); + } + }); + // Handle charts that are in scope but belong to excluded tabs. + const chartsInExcludedTabs = charts + .filter(chart => !filter.scope.excluded.includes(chart.id)) + .reduce((acc: LayoutItem[], chart) => { + const layoutChartElementInExcludedTab = + layoutChartElementsInTabsInScope.find( + element => + element.meta.chartId === chart.id && + element.parents.every( + parent => !topLevelTabsInFullScope.includes(parent), + ), + ); + if (layoutChartElementInExcludedTab) { + acc.push(layoutChartElementInExcludedTab); + } + return acc; + }, []); + // Join tab names and chart names + return topLevelTabsInFullScope + .map(tabId => extractTabLabel(layout[tabId])) + .concat(chartsInExcludedTabs.map(extractChartLabel)); + } + + return undefined; + }, [charts, filter.scope.excluded, filter.scope.rootPath, layout]); +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useTruncation.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useTruncation.ts new file mode 100644 index 00000000000..4f2e1723691 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useTruncation.ts @@ -0,0 +1,58 @@ +/** + * 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 { RefObject, useLayoutEffect, useState } from 'react'; + +export const useTruncation = (elementRef: RefObject) => { + const [elementsTruncated, setElementsTruncated] = useState(0); + const [hasHiddenElements, setHasHiddenElements] = useState(false); + + useLayoutEffect(() => { + const currentElement = elementRef.current; + if (!currentElement) { + return; + } + const { scrollWidth, clientWidth, childNodes } = currentElement; + if (scrollWidth > clientWidth) { + // "..." is around 6px wide + const maxWidth = clientWidth - 6; + const elementsCount = childNodes.length; + let width = 0; + let i = 0; + while (width < maxWidth) { + width += (childNodes[i] as HTMLElement).offsetWidth; + i += 1; + } + if (i === elementsCount) { + setElementsTruncated(1); + setHasHiddenElements(false); + } else { + setElementsTruncated(elementsCount - i); + setHasHiddenElements(true); + } + } else { + setElementsTruncated(0); + } + }, [ + elementRef.current?.offsetWidth, + elementRef.current?.clientWidth, + elementRef, + ]); + + return [elementsTruncated, hasHiddenElements]; +}; diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index c4638d8d345..878f3d68695 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -17,8 +17,9 @@ * under the License. */ import React, { FC, useRef, useEffect, useState } from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { FeatureFlag, isFeatureEnabled, t, useTheme } from '@superset-ui/core'; import { useDispatch, useSelector } from 'react-redux'; +import { Global } from '@emotion/react'; import { useParams } from 'react-router-dom'; import { useToasts } from 'src/components/MessageToasts/withToasts'; import Loading from 'src/components/Loading'; @@ -49,6 +50,7 @@ import { getUrlParam } from 'src/utils/urlUtils'; import { canUserEditDashboard } from 'src/dashboard/util/findPermission'; import { getFilterSets } from '../actions/nativeFilters'; import { getFilterValue } from '../components/nativeFilters/FilterBar/keyValue'; +import { filterCardPopoverStyle } from '../styles'; export const MigrationContext = React.createContext( FILTER_BOX_MIGRATION_STATES.NOOP, @@ -68,6 +70,7 @@ const originalDocumentTitle = document.title; const DashboardPage: FC = () => { const dispatch = useDispatch(); + const theme = useTheme(); const user = useSelector( state => state.user, ); @@ -226,6 +229,7 @@ const DashboardPage: FC = () => { return ( <> + css` + .filter-card-popover { + width: 240px; + padding: 0; + border-radius: 4px; + + .ant-popover-inner-content { + padding: ${theme.gridUnit * 4}px; + } + + .ant-popover-arrow { + display: none; + } + } + + .filter-card-tooltip { + &.ant-tooltip-placement-bottom { + padding-top: 0; + & .ant-tooltip-arrow { + top: -13px; + } + } + } +`;