This is an automated email from the ASF dual-hosted git repository. elizabeth pushed a commit to branch elizabeth/fix-resize-bug in repository https://gitbox.apache.org/repos/asf/superset.git
commit cb94c87751bc972906d01fd5bcdff3663473493b Author: Elizabeth Thompson <[email protected]> AuthorDate: Tue Jan 20 11:03:06 2026 -0800 feat(dashboard): show applied filters in chart title tooltip When a chart title is truncated (overflows), the tooltip now displays filter information including: - Count of filters applied to the chart - List of each filter with name, column, and value This helps users understand what filters are affecting a chart when the title is truncated and they hover over it. Changes: - Add useAppliedFilterIndicators hook for reusable filter indicator logic - Update SliceHeader to include filter info in the title tooltip 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --- .../src/dashboard/components/SliceHeader/index.tsx | 80 +++++++- .../dashboard/hooks/useAppliedFilterIndicators.ts | 220 +++++++++++++++++++++ 2 files changed, 292 insertions(+), 8 deletions(-) diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx index c72da050ee..e70f9159c2 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx @@ -21,6 +21,7 @@ import { ReactNode, useContext, useEffect, + useMemo, useRef, useState, } from 'react'; @@ -45,6 +46,8 @@ import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip' import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage'; import RowCountLabel from 'src/components/RowCountLabel'; import { Link } from 'react-router-dom'; +import { useAppliedFilterIndicators } from 'src/dashboard/hooks/useAppliedFilterIndicators'; +import { getFilterValueForDisplay } from 'src/dashboard/components/nativeFilters/utils'; const extensionsRegistry = getExtensionsRegistry(); @@ -215,20 +218,81 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>( const canExplore = !editMode && supersetCanExplore; + // Get applied filter indicators for this chart + const { appliedIndicators, appliedCrossFilterIndicators, filterCount } = + useAppliedFilterIndicators(slice.slice_id); + + // Build the filter list for the tooltip + const filterListContent = useMemo(() => { + const allFilters = [ + ...appliedCrossFilterIndicators, + ...appliedIndicators, + ]; + if (allFilters.length === 0) return null; + + return allFilters.map(indicator => { + const filterValue = getFilterValueForDisplay(indicator.value); + const columnLabel = indicator.customColumnLabel || indicator.column; + return `• ${indicator.name}${columnLabel ? ` (${columnLabel})` : ''}${filterValue ? `: ${filterValue}` : ''}`; + }); + }, [appliedIndicators, appliedCrossFilterIndicators]); + useEffect(() => { const headerElement = headerRef.current; - if (canExplore) { - setHeaderTooltip(getSliceHeaderTooltip(sliceName)); - } else if ( + const isTruncated = headerElement && (headerElement.scrollWidth > headerElement.offsetWidth || - headerElement.scrollHeight > headerElement.offsetHeight) - ) { - setHeaderTooltip(sliceName ?? null); + headerElement.scrollHeight > headerElement.offsetHeight); + + // Build the tooltip content + let tooltipContent: ReactNode = null; + + if (canExplore) { + tooltipContent = getSliceHeaderTooltip(sliceName); + } else if (isTruncated) { + tooltipContent = sliceName ?? null; + } + + // Add filter information to tooltip when title is truncated and filters are applied + if (isTruncated && filterCount > 0 && filterListContent) { + const filterInfo = ( + <div> + {tooltipContent && <div>{tooltipContent}</div>} + <div + css={css` + margin-top: ${tooltipContent ? '8px' : '0'}; + border-top: ${tooltipContent + ? '1px solid rgba(255,255,255,0.2)' + : 'none'}; + padding-top: ${tooltipContent ? '8px' : '0'}; + `} + > + <div + css={css` + font-weight: 600; + margin-bottom: 4px; + `} + > + {t('%s filter(s) applied to this chart', filterCount)} + </div> + <div + css={css` + font-size: 12px; + opacity: 0.9; + `} + > + {filterListContent.map((filter, index) => ( + <div key={index}>{filter}</div> + ))} + </div> + </div> + </div> + ); + setHeaderTooltip(filterInfo); } else { - setHeaderTooltip(null); + setHeaderTooltip(tooltipContent); } - }, [sliceName, width, height, canExplore]); + }, [sliceName, width, height, canExplore, filterCount, filterListContent]); const exploreUrl = `/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`; diff --git a/superset-frontend/src/dashboard/hooks/useAppliedFilterIndicators.ts b/superset-frontend/src/dashboard/hooks/useAppliedFilterIndicators.ts new file mode 100644 index 0000000000..e65b513922 --- /dev/null +++ b/superset-frontend/src/dashboard/hooks/useAppliedFilterIndicators.ts @@ -0,0 +1,220 @@ +/** + * 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 { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { uniqWith } from 'lodash'; +import { + DataMaskStateWithId, + Filters, + JsonObject, + usePrevious, +} from '@superset-ui/core'; +import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems'; +import { + Indicator, + IndicatorStatus, + selectIndicatorsForChart, + selectNativeIndicatorsForChart, +} from 'src/dashboard/components/nativeFilters/selectors'; +import { Chart, RootState } from 'src/dashboard/types'; + +const sortByStatus = (indicators: Indicator[]): Indicator[] => { + const statuses = [ + IndicatorStatus.Applied, + IndicatorStatus.Unset, + IndicatorStatus.Incompatible, + ]; + return indicators.sort( + (a, b) => + statuses.indexOf(a.status as IndicatorStatus) - + statuses.indexOf(b.status as IndicatorStatus), + ); +}; + +const indicatorsInitialState: Indicator[] = []; + +export interface AppliedFilterIndicators { + appliedIndicators: Indicator[]; + appliedCrossFilterIndicators: Indicator[]; + filterCount: number; +} + +/** + * Hook to get applied filter indicators for a specific chart. + * Extracts the filter indicator logic from FiltersBadge for reuse. + */ +export const useAppliedFilterIndicators = ( + chartId: number, +): AppliedFilterIndicators => { + // Using 'any' type for these selectors to match FiltersBadge implementation + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const datasources = useSelector<RootState, any>(state => state.datasources); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dashboardFilters = useSelector<RootState, any>( + state => state.dashboardFilters, + ); + const nativeFilters = useSelector<RootState, Filters>( + state => state.nativeFilters?.filters, + ); + const chartConfiguration = useSelector<RootState, JsonObject>( + state => state.dashboardInfo.metadata?.chart_configuration, + ); + const chart = useSelector<RootState, Chart>(state => state.charts[chartId]); + const chartLayoutItems = useChartLayoutItems(); + const dataMask = useSelector<RootState, DataMaskStateWithId>( + state => state.dataMask, + ); + + const [nativeIndicators, setNativeIndicators] = useState<Indicator[]>( + indicatorsInitialState, + ); + const [dashboardIndicators, setDashboardIndicators] = useState<Indicator[]>( + indicatorsInitialState, + ); + + const prevChart = usePrevious(chart); + const prevChartStatus = prevChart?.chartStatus; + const prevDashboardFilters = usePrevious(dashboardFilters); + const prevDatasources = usePrevious(datasources); + const showIndicators = + chart?.chartStatus && ['rendered', 'success'].includes(chart.chartStatus); + + useEffect(() => { + if (!showIndicators && dashboardIndicators.length > 0) { + setDashboardIndicators(indicatorsInitialState); + } else if (prevChartStatus !== 'success') { + if ( + chart?.queriesResponse?.[0]?.rejected_filters !== + prevChart?.queriesResponse?.[0]?.rejected_filters || + chart?.queriesResponse?.[0]?.applied_filters !== + prevChart?.queriesResponse?.[0]?.applied_filters || + dashboardFilters !== prevDashboardFilters || + datasources !== prevDatasources + ) { + setDashboardIndicators( + selectIndicatorsForChart( + chartId, + dashboardFilters, + datasources, + chart, + ), + ); + } + } + }, [ + chart, + chartId, + dashboardFilters, + dashboardIndicators.length, + datasources, + prevChart?.queriesResponse, + prevChartStatus, + prevDashboardFilters, + prevDatasources, + showIndicators, + ]); + + const prevNativeFilters = usePrevious(nativeFilters); + const prevChartLayoutItems = usePrevious(chartLayoutItems); + const prevDataMask = usePrevious(dataMask); + const prevChartConfig = usePrevious(chartConfiguration); + + useEffect(() => { + if (!showIndicators && nativeIndicators.length > 0) { + setNativeIndicators(indicatorsInitialState); + } else if (prevChartStatus !== 'success') { + if ( + chart?.queriesResponse?.[0]?.rejected_filters !== + prevChart?.queriesResponse?.[0]?.rejected_filters || + chart?.queriesResponse?.[0]?.applied_filters !== + prevChart?.queriesResponse?.[0]?.applied_filters || + nativeFilters !== prevNativeFilters || + chartLayoutItems !== prevChartLayoutItems || + dataMask !== prevDataMask || + prevChartConfig !== chartConfiguration + ) { + setNativeIndicators( + selectNativeIndicatorsForChart( + nativeFilters, + dataMask, + chartId, + chart, + chartLayoutItems, + chartConfiguration, + ), + ); + } + } + }, [ + chart, + chartId, + chartConfiguration, + dataMask, + nativeFilters, + nativeIndicators.length, + prevChart?.queriesResponse, + prevChartConfig, + prevChartStatus, + prevDataMask, + prevNativeFilters, + showIndicators, + chartLayoutItems, + prevChartLayoutItems, + ]); + + const indicators = useMemo( + () => + uniqWith( + sortByStatus([...dashboardIndicators, ...nativeIndicators]), + (ind1, ind2) => + ind1.column === ind2.column && + ind1.name === ind2.name && + (ind1.status !== IndicatorStatus.Applied || + ind2.status !== IndicatorStatus.Applied), + ), + [dashboardIndicators, nativeIndicators], + ); + + const appliedCrossFilterIndicators = useMemo( + () => + indicators.filter( + indicator => indicator.status === IndicatorStatus.CrossFilterApplied, + ), + [indicators], + ); + + const appliedIndicators = useMemo( + () => + indicators.filter( + indicator => indicator.status === IndicatorStatus.Applied, + ), + [indicators], + ); + + const filterCount = + appliedIndicators.length + appliedCrossFilterIndicators.length; + + return { + appliedIndicators, + appliedCrossFilterIndicators, + filterCount, + }; +}; + +export default useAppliedFilterIndicators;
