diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index 9e7286db4b7319..ceeb1daab9b590 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -50,7 +50,10 @@ import { import {widgetCanUseTimeSeriesVisualization} from 'sentry/views/dashboards/utils/widgetCanUseTimeSeriesVisualization'; import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer'; import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState'; -import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types'; +import type { + LegendSelection, + TabularColumn, +} from 'sentry/views/dashboards/widgets/common/types'; import {Widget} from 'sentry/views/dashboards/widgets/widget/widget'; import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext'; import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext'; @@ -356,6 +359,19 @@ function WidgetCard(props: Props) { const canUseTimeseriesVisualization = widgetCanUseTimeSeriesVisualization(widget); if (canUseTimeseriesVisualization) { + // Only pass legend selection when there's explicit URL state. + // getWidgetSelectionState returns a default that hides Releases for + // LINE/AREA widgets, which was appropriate for the old overlay-line + // rendering but not for the new bubble markers. + const legendSelectionForWidget = location.query.unselectedSeries + ? widgetLegendState.getWidgetSelectionState(widget) + : undefined; + + const handleLegendSelectionChange = (legendState: LegendSelection) => { + widgetLegendState.setWidgetSelectionState(legendState, widget); + onLegendSelectChanged?.(); + }; + return ( diff --git a/static/app/views/dashboards/widgetCard/visualizationWidget.tsx b/static/app/views/dashboards/widgetCard/visualizationWidget.tsx index 2f2bb523a60c2d..51b1d1758a060b 100644 --- a/static/app/views/dashboards/widgetCard/visualizationWidget.tsx +++ b/static/app/views/dashboards/widgetCard/visualizationWidget.tsx @@ -31,6 +31,7 @@ import { import {getChartType} from 'sentry/views/dashboards/utils/getWidgetExploreUrl'; import {matchTimeSeriesToTableRowValue} from 'sentry/views/dashboards/widgetCard/matchTimeSeriesToTableRowValue'; import {transformWidgetSeriesToTimeSeries} from 'sentry/views/dashboards/widgetCard/transformWidgetSeriesToTimeSeries'; +import {WidgetLegendNameEncoderDecoder} from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; import { MISSING_DATA_MESSAGE, NUMBER_MIN_VALUE, @@ -103,6 +104,20 @@ export function VisualizationWidget({ }: VisualizationWidgetProps) { const onWidgetError = useWidgetErrorCallback(); + // WidgetLegendSelectionState persists legend selection to the URL with + // keys in `seriesName|~|widgetId` format so each widget's selection is + // tracked independently. TimeSeriesWidgetVisualization uses plain + // series names. Decode on the way in and encode on the way out. + const decodedLegendSelection = legendSelection + ? decodeLegendSelection(legendSelection) + : undefined; + + const handleLegendSelectionChange = onLegendSelectionChange + ? (plain: LegendSelection) => { + onLegendSelectionChange(encodeLegendSelection(plain, widget.id)); + } + : undefined; + const {releases: releasesWithDate} = useReleaseStats(selection, { enabled: showReleaseAs !== 'none', }); @@ -157,8 +172,8 @@ export function VisualizationWidget({ isSampled={isSampled} sampleCount={sampleCount} onZoom={onZoom} - legendSelection={legendSelection} - onLegendSelectionChange={onLegendSelectionChange} + legendSelection={decodedLegendSelection} + onLegendSelectionChange={handleLegendSelectionChange} isFullScreen={isFullScreen} /> ); @@ -500,3 +515,32 @@ function renderBreakdownLabel( return fallbackLabel; } + +/** + * Decodes legend selection keys from `seriesName|~|widgetId` format to + * plain `seriesName` keys used by `TimeSeriesWidgetVisualization`. + */ +function decodeLegendSelection(encoded: LegendSelection): LegendSelection { + const decoded: LegendSelection = {}; + for (const key in encoded) { + decoded[WidgetLegendNameEncoderDecoder.decodeSeriesNameForLegend(key, true)] = + encoded[key]!; + } + return decoded; +} + +/** + * Encodes legend selection keys from plain `seriesName` format back to + * `seriesName|~|widgetId` format used by `WidgetLegendSelectionState`. + */ +function encodeLegendSelection( + plain: LegendSelection, + widgetId: string | undefined +): LegendSelection { + const encoded: LegendSelection = {}; + for (const key in plain) { + encoded[WidgetLegendNameEncoderDecoder.encodeSeriesNameForLegend(key, widgetId)] = + plain[key]!; + } + return encoded; +} diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx index 356eda1ece973a..cd8f14e756df1e 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx @@ -3,19 +3,6 @@ import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; import {formatTimeSeriesLabel} from './formatTimeSeriesLabel'; describe('formatSeriesName', () => { - describe('releases', () => { - it.each([ - ['p75(span.duration)|~|11762', 'p75(span.duration)'], - ['Releases|~|', 'Releases'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); - }); - }); - describe('aggregates', () => { it.each([ ['user_misery()', 'user_misery()'], @@ -70,26 +57,8 @@ describe('formatSeriesName', () => { }); }); - describe('combinations', () => { - it.each([ - ['equation|p75(measurements.cls) + 1|~|76123', 'p75(measurements.cls) + 1'], - ['equation|p75(measurements.cls)|~|76123', 'p75(measurements.cls)'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); - }); - }); - describe('groupBy', () => { it.each([ - [ - 'equation|p75(measurements.cls)|~|76123', - [{key: 'release', value: 'v0.0.2'}], - 'v0.0.2', - ], ['p95(span.duration)', [{key: 'release', value: 'v0.0.2'}], 'v0.0.2'], ['p95(span.duration)', [{key: 'gen_ai.request.model', value: null}], '(no value)'], [ diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx index 8897947291d8ed..fca3b9c7451d90 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx @@ -1,7 +1,6 @@ import {t} from 'sentry/locale'; import {maybeEquationAlias, stripEquationPrefix} from 'sentry/utils/discover/fields'; import {formatVersion} from 'sentry/utils/versions/formatVersion'; -import {WidgetLegendNameEncoderDecoder} from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types'; export function formatTimeSeriesLabel(timeSeries: TimeSeries): string { @@ -34,9 +33,6 @@ export function formatTimeSeriesLabel(timeSeries: TimeSeries): string { let {yAxis: seriesName} = timeSeries; - // Decode from series name disambiguation - seriesName = WidgetLegendNameEncoderDecoder.decodeSeriesNameForLegend(seriesName)!; - // Attempt to parse the `seriesName` as a version. A correct `TimeSeries` // would have a `yAxis` like `p50(span.duration)` with a `groupBy` like // `[{key: "release", value: "proj@1.2.3"}]`. `groupBy` was only introduced