Compare commits

...

1 Commits

Author SHA1 Message Date
Elizabeth Thompson
eeb9edd7e2 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 <noreply@anthropic.com>
2026-01-22 09:37:50 -08:00
2 changed files with 293 additions and 9 deletions

View File

@@ -21,6 +21,7 @@ import {
ReactNode,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
@@ -40,6 +41,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();
@@ -197,20 +200,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);
} else {
setHeaderTooltip(null);
headerElement.scrollHeight > headerElement.offsetHeight);
// Build the tooltip content
let tooltipContent: ReactNode = null;
if (canExplore) {
tooltipContent = getSliceHeaderTooltip(sliceName);
} else if (isTruncated) {
tooltipContent = sliceName ?? null;
}
}, [sliceName, width, height, canExplore]);
// 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(tooltipContent);
}
}, [sliceName, width, height, canExplore, filterCount, filterListContent]);
const exploreUrl = `/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;

View File

@@ -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;