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;

Reply via email to