You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2026/01/20 19:04:27 UTC
(superset) 37/37: feat(dashboard): show applied filters in chart title tooltip
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 <es...@gmail.com>
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 <no...@anthropic.com>
---
.../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;