-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(explore): Add to filter option in heatmaps tooltip #116690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
42f595d
5310605
b81dc9d
aa52596
9791ed2
2d69901
695b3f2
fe54c3c
5ffb2b1
3a1c140
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import 'echarts/lib/chart/heatmap'; | ||
|
|
||
| import {Fragment, useRef, type ReactNode} from 'react'; | ||
| import {Fragment, useEffect, useRef} from 'react'; | ||
| import {useTheme} from '@emotion/react'; | ||
| import type { | ||
| TooltipFormatterCallback, | ||
|
|
@@ -15,18 +15,18 @@ import {defaultFormatAxisLabel} from 'sentry/components/charts/components/toolti | |
| import {isChartHovered} from 'sentry/components/charts/utils'; | ||
| import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; | ||
| import {t} from 'sentry/locale'; | ||
| import type {PageFilters} from 'sentry/types/core'; | ||
| import type {ReactEchartsRef} from 'sentry/types/echarts'; | ||
| import {defined} from 'sentry/utils'; | ||
| import {formatAbbreviatedNumber} from 'sentry/utils/formatters'; | ||
| import {ECHARTS_MISSING_DATA_VALUE} from 'sentry/utils/timeSeries/timeSeriesItemToEChartsDataPoint'; | ||
| import {useOrganization} from 'sentry/utils/useOrganization'; | ||
| import {useNavigate} from 'sentry/utils/useNavigate'; | ||
| import {NO_PLOTTABLE_VALUES} from 'sentry/views/dashboards/widgets/common/settings'; | ||
| import {formatYAxisValue} from 'sentry/views/dashboards/widgets/heatMapWidget/formatters/formatYAxisValue'; | ||
| import {plottablesCanBeVisualized} from 'sentry/views/dashboards/widgets/plottablesCanBeVisualized'; | ||
| import {formatTooltipValue} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue'; | ||
| import {formatXAxisTimestamp} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatXAxisTimestamp'; | ||
| import {FALLBACK_TYPE} from 'sentry/views/dashboards/widgets/timeSeriesWidget/settings'; | ||
| import {getExploreUrl, type GetExploreUrlArgs} from 'sentry/views/explore/utils'; | ||
|
|
||
| import {HeatMap} from './plottables/heatMap'; | ||
| import type {HeatMapPlottable} from './plottables/heatMapPlottable'; | ||
|
|
@@ -42,25 +42,76 @@ interface HeatMapWidgetVisualizationProps { | |
| */ | ||
| plottables: [HeatMap, ...HeatMapPlottable[]]; | ||
| /** | ||
| * Experimental! Specify the Z-axis scale type. Logarithmic scales can be much more useful for values with a high range. | ||
| * Callback that returns an explore URL for a given query and filtered datetime selection | ||
| */ | ||
| scale?: 'linear' | 'log'; | ||
| makeExploreUrl?: (query: string, filteredSelection: PageFilters) => string; | ||
| /** | ||
| * getExploreUrl props that will be used to generate an explore link for the tooltip. Omitting this will not generate an explore link. | ||
| * Callback that returns an updated query string. | ||
| */ | ||
| makeLocalQueryUpdateUrl?: (query: string) => string; | ||
| /** | ||
| * Experimental! Specify the Z-axis scale type. Logarithmic scales can be much more useful for values with a high range. | ||
| */ | ||
| tooltipExploreUrlArgs?: Omit<GetExploreUrlArgs, 'organization'>; | ||
| scale?: 'linear' | 'log'; | ||
| } | ||
|
|
||
| export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProps) { | ||
| const {plottables} = props; | ||
| const {plottables, makeLocalQueryUpdateUrl, makeExploreUrl} = props; | ||
| const theme = useTheme(); | ||
| const organization = useOrganization(); | ||
| const renderToString = useRenderToString(); | ||
|
|
||
| const navigate = useNavigate(); | ||
| const pageFilters = usePageFilters(); | ||
| const {start, end, period, utc} = pageFilters.selection.datetime; | ||
| const chartRef = useRef<ReactEchartsRef | null>(null); | ||
|
|
||
| // yes i am aware that this is UGLY but it's a hack so that we can use proper react routing. | ||
| // Basically the way ECharts renders the tooltip is by creating a string out of the dom tree. | ||
| // This means that we can't use any of the normal linking/routing tools that we use in React trees | ||
| // because they require contexts that won't be available properly in this string tree. | ||
| // Using the `<a>` tag will make the page reload and navigate to the url because it doesn't have | ||
| // link history context. Doing the navigation here preserves the link history context and makes the | ||
| // page navigation smoother instead of reloading the page every time a link is clicked. | ||
| useEffect(() => { | ||
| const handleClick = (e: MouseEvent) => { | ||
| if (!chartRef.current?.ele.contains(e.target as Node)) { | ||
|
sentry[bot] marked this conversation as resolved.
Outdated
|
||
| return; | ||
| } | ||
| const localQueryUpdateTarget = (e.target as Element).closest( | ||
| '[data-local-query-update-url]' | ||
| ); | ||
| const tracesLinkTarget = (e.target as Element).closest('[data-traces-link]'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Text clicks break tooltip routingMedium Severity The document click handler calls Reviewed by Cursor Bugbot for commit b81dc9d. Configure here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idk what they're talking about the client side navigation works
nikkikapadia marked this conversation as resolved.
|
||
| if (!localQueryUpdateTarget && !tracesLinkTarget) { | ||
| return; | ||
| } | ||
| e.preventDefault(); | ||
| const openInNewTab = e.metaKey || e.ctrlKey; | ||
| if (localQueryUpdateTarget) { | ||
| const localUrl = localQueryUpdateTarget.getAttribute( | ||
| 'data-local-query-update-url' | ||
| ); | ||
| if (localUrl) { | ||
| if (openInNewTab) { | ||
| window.open(localUrl, '_blank'); | ||
| } else { | ||
| navigate(localUrl); | ||
| } | ||
| } | ||
| } | ||
| if (tracesLinkTarget) { | ||
| const tracesUrl = tracesLinkTarget.getAttribute('data-traces-link'); | ||
| if (tracesUrl) { | ||
| if (openInNewTab) { | ||
| window.open(tracesUrl, '_blank'); | ||
| } else { | ||
| navigate(tracesUrl); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| document.addEventListener('click', handleClick); | ||
| return () => document.removeEventListener('click', handleClick); | ||
| }, [navigate]); | ||
|
nikkikapadia marked this conversation as resolved.
Outdated
|
||
|
|
||
| if (!plottablesCanBeVisualized(plottables)) { | ||
| throw new Error(NO_PLOTTABLE_VALUES); | ||
| } | ||
|
|
@@ -126,7 +177,7 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp | |
|
|
||
| return renderToString( | ||
| <Fragment> | ||
| <div className="tooltip-series"> | ||
| <div className="tooltip-series" style={{cursor: 'default'}}> | ||
| {filteredParams.map(param => { | ||
| let rawXValue: number | undefined; | ||
| let rawYValue: number | undefined; | ||
|
|
@@ -175,42 +226,34 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp | |
| } | ||
| } | ||
|
|
||
| let exploreLink: ReactNode; | ||
| let tracesLink: string | undefined; | ||
| let localQueryUpdateUrl: string | undefined; | ||
| const metricsQuery = defined(rawYValue) | ||
| ? yAxisBucketSize === 0 | ||
| ? `value:<=${rawYValue}` | ||
| : `value:>=${rawYValue} value:<${rawYValue + yAxisBucketSize}` | ||
| : undefined; | ||
|
|
||
| if (defined(rawXValue) && defined(rawYValue) && props.tooltipExploreUrlArgs) { | ||
| if (defined(rawXValue) && defined(rawYValue)) { | ||
| const xAxisMaxValue = rawXValue + xAxisBucketSize * 1000; | ||
| const yAxisMaxValue = rawYValue + yAxisBucketSize; | ||
|
|
||
| const exploreUrlProps: GetExploreUrlArgs = { | ||
| organization, | ||
| ...props.tooltipExploreUrlArgs, | ||
| selection: { | ||
| ...pageFilters.selection, | ||
| datetime: { | ||
| ...pageFilters.selection.datetime, | ||
| start: new Date(rawXValue), | ||
| end: new Date(xAxisMaxValue), | ||
| period: null, | ||
| }, | ||
|
|
||
| const filteredSelection = { | ||
| ...pageFilters.selection, | ||
| datetime: { | ||
| ...pageFilters.selection.datetime, | ||
| start: new Date(rawXValue), | ||
| end: new Date(xAxisMaxValue), | ||
| period: null, | ||
| }, | ||
| // TODO(nikki): we're only handling metrics for now but if we're looking to support other explore | ||
| // surfaces then we'll need to add more logic here | ||
| crossEvents: props.tooltipExploreUrlArgs?.crossEvents?.map(crossEvent => { | ||
| if (crossEvent.type === 'metrics') { | ||
| return { | ||
| ...crossEvent, | ||
| query: | ||
| yAxisBucketSize === 0 | ||
| ? `value:<=${rawYValue}` | ||
| : `value:>=${rawYValue} value:<${yAxisMaxValue}`, | ||
| }; | ||
| } | ||
| return crossEvent; | ||
| }), | ||
| }; | ||
|
|
||
| const tracesLink = getExploreUrl(exploreUrlProps); | ||
| exploreLink = <a href={tracesLink}>{t('View related traces')}</a>; | ||
| if (makeExploreUrl && metricsQuery) { | ||
| tracesLink = makeExploreUrl(metricsQuery, filteredSelection); | ||
| } | ||
| } | ||
|
|
||
| if (makeLocalQueryUpdateUrl && metricsQuery) { | ||
| localQueryUpdateUrl = makeLocalQueryUpdateUrl(metricsQuery); | ||
| } | ||
|
|
||
| return ( | ||
|
|
@@ -221,18 +264,37 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp | |
| </span>{' '} | ||
| {formattedZValue} | ||
| </div> | ||
| {exploreLink && ( | ||
| {defined(tracesLink) && ( | ||
| <div> | ||
| <span className="tooltip-label tooltip-label-centered"> | ||
| {exploreLink} | ||
| <a data-traces-link={tracesLink} href={tracesLink}> | ||
| {t('View connected spans')} | ||
| </a> | ||
| </span> | ||
| </div> | ||
| )} | ||
| {defined(localQueryUpdateUrl) && ( | ||
| <div> | ||
| <span className="tooltip-label tooltip-label-centered"> | ||
| <a | ||
| data-local-query-update-url={localQueryUpdateUrl} | ||
| href={localQueryUpdateUrl} | ||
| > | ||
| {t('Add to filter')} | ||
| </a> | ||
| </span> | ||
| </div> | ||
| )} | ||
| </Fragment> | ||
| ); | ||
| })} | ||
| </div> | ||
| <div className="tooltip-footer tooltip-footer-centered">{formattedXValue}</div> | ||
| <div | ||
| className="tooltip-footer tooltip-footer-centered" | ||
| style={{cursor: 'default'}} | ||
| > | ||
| {formattedXValue} | ||
| </div> | ||
| <div className="tooltip-arrow" /> | ||
| </Fragment> | ||
| ); | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😭