From eff16152d3d4e7ea6c2512467b23b85bd1e9a549 Mon Sep 17 00:00:00 2001
From: xob0t <5348245@gmail.com>
Date: Wed, 15 Jul 2026 15:53:40 +0300
Subject: [PATCH 1/2] feat(dashboards): add replay search action
---
.changeset/dashboard-replay-search.md | 5 +++
packages/app/src/DBDashboardPage.tsx | 57 +++++++++++++++++++++++++++
2 files changed, 62 insertions(+)
create mode 100644 .changeset/dashboard-replay-search.md
diff --git a/.changeset/dashboard-replay-search.md b/.changeset/dashboard-replay-search.md
new file mode 100644
index 0000000000..039bbbaab5
--- /dev/null
+++ b/.changeset/dashboard-replay-search.md
@@ -0,0 +1,5 @@
+---
+"@hyperdx/app": patch
+---
+
+feat(dashboards): add a Replay search action to dashboard tiles backed by event-search-compatible chart configurations. The action opens a new Search tab with the tile's source, query, filters, and dashboard time range preserved.
diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx
index bf5ba80740..6519634493 100644
--- a/packages/app/src/DBDashboardPage.tsx
+++ b/packages/app/src/DBDashboardPage.tsx
@@ -24,6 +24,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import RGL, { WidthProvider } from 'react-grid-layout';
import { useForm, useWatch } from 'react-hook-form';
import { TableConnection } from '@hyperdx/common-utils/dist/core/metadata';
+import { isMetricChartConfig } from '@hyperdx/common-utils/dist/core/renderChartConfig';
import {
convertToDashboardTemplate,
displayTypeSupportsBuilderAlerts,
@@ -684,6 +685,30 @@ const Tile = forwardRef(
);
}, [filters, queriedConfig, source]);
+ const replaySearchUrl = useMemo(() => {
+ if (!queriedConfig || !source || !isBuilderChartConfig(queriedConfig)) {
+ return null;
+ }
+
+ // Metric charts drill into their associated log source. Do not call the
+ // URL builder when that association is missing because it would surface
+ // a notification while rendering the dashboard.
+ if (
+ isMetricChartConfig(queriedConfig) &&
+ ((source.kind !== SourceKind.Metric &&
+ source.kind !== SourceKind.Trace) ||
+ !source.logSourceId)
+ ) {
+ return null;
+ }
+
+ return buildEventsSearchUrl({
+ source,
+ config: queriedConfig,
+ dateRange,
+ });
+ }, [dateRange, queriedConfig, source]);
+
const hoverToolbar = useMemo(() => {
const isRawSql = isRawSqlSavedChartConfig(chart.config);
const isPromQL = isPromqlSavedChartConfig(chart.config);
@@ -702,6 +727,25 @@ const Tile = forwardRef(
key="hover-toolbar"
my={2} // Margin to ensure that the Alert Indicator doesn't clip on non-Line/Bar display types
>
+ {replaySearchUrl && (
+
+
+
+
+
+ )}
+
{displayTypeSupportsAlerts &&
(alert ? (
// Existing alert: bell with a colored status dot indicator.
@@ -851,6 +895,7 @@ const Tile = forwardRef(
alertIndicatorColor,
alertTooltip,
moveTargets,
+ replaySearchUrl,
chart.config,
chart.id,
chart.containerId,
@@ -877,6 +922,17 @@ const Tile = forwardRef(
onMoveToGroup && moveTargets && moveTargets.length > 0;
return (
<>
+ {replaySearchUrl && (
+
}
+ >
+ Replay search
+
+ )}
{showAlerts && (
<>
Date: Wed, 15 Jul 2026 16:29:19 +0300
Subject: [PATCH 2/2] fix(dashboards): restrict replay search eligibility
---
.changeset/dashboard-replay-search.md | 2 +-
packages/app/src/DBDashboardPage.tsx | 30 ++++++++++++++++++---------
2 files changed, 21 insertions(+), 11 deletions(-)
diff --git a/.changeset/dashboard-replay-search.md b/.changeset/dashboard-replay-search.md
index 039bbbaab5..5b6324f2b1 100644
--- a/.changeset/dashboard-replay-search.md
+++ b/.changeset/dashboard-replay-search.md
@@ -2,4 +2,4 @@
"@hyperdx/app": patch
---
-feat(dashboards): add a Replay search action to dashboard tiles backed by event-search-compatible chart configurations. The action opens a new Search tab with the tile's source, query, filters, and dashboard time range preserved.
+feat(dashboards): add a Replay search action to log and trace dashboard tiles whose event query can be faithfully reconstructed. The action opens a new Search tab with the tile's source, query, filters, and dashboard time range preserved.
diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx
index 6519634493..994301e75a 100644
--- a/packages/app/src/DBDashboardPage.tsx
+++ b/packages/app/src/DBDashboardPage.tsx
@@ -24,7 +24,6 @@ import { ErrorBoundary } from 'react-error-boundary';
import RGL, { WidthProvider } from 'react-grid-layout';
import { useForm, useWatch } from 'react-hook-form';
import { TableConnection } from '@hyperdx/common-utils/dist/core/metadata';
-import { isMetricChartConfig } from '@hyperdx/common-utils/dist/core/renderChartConfig';
import {
convertToDashboardTemplate,
displayTypeSupportsBuilderAlerts,
@@ -50,6 +49,7 @@ import {
Filter,
getSampleWeightExpression,
isLogSource,
+ isSearchableSource,
isTraceSource,
SearchCondition,
SearchConditionLanguage,
@@ -690,18 +690,28 @@ const Tile = forwardRef(
return null;
}
- // Metric charts drill into their associated log source. Do not call the
- // URL builder when that association is missing because it would surface
- // a notification while rendering the dashboard.
- if (
- isMetricChartConfig(queriedConfig) &&
- ((source.kind !== SourceKind.Metric &&
- source.kind !== SourceKind.Trace) ||
- !source.logSourceId)
- ) {
+ if (queriedConfig.metricTables != null || !isSearchableSource(source)) {
return null;
}
+ if (Array.isArray(queriedConfig.select)) {
+ const hasPerSeriesCondition = queriedConfig.select.some(
+ select =>
+ typeof select !== 'string' &&
+ select.aggCondition != null &&
+ select.aggCondition.trim().length > 0,
+ );
+ const canPromoteSingleSeriesCondition =
+ queriedConfig.select.length === 1 && queriedConfig.where.length === 0;
+
+ // buildEventsSearchUrl can promote one per-series condition into the
+ // event query, but cannot faithfully replay multiple conditions or
+ // combine a series condition with a global where clause.
+ if (hasPerSeriesCondition && !canPromoteSingleSeriesCondition) {
+ return null;
+ }
+ }
+
return buildEventsSearchUrl({
source,
config: queriedConfig,