Skip to content

Commit e2fc25d

Browse files
authored
feat: Add custom table onClick behavior (#2140)
## Summary This is the **first of a few PRs** adding customizable on-click / linking behaviors to dashboard tables. This feature is behind the `NEXT_PUBLIC_IS_DASHBOARD_LINKING_ENABLED`, which has been enabled in the preview environment and for local development. Tables without a custom on click behavior defined will fall back to the existing on click behavior (which is linking to the search page, with group by columns passed as filters). ### Scope In this PR: - Support for defining a custom on click behavior linking a dashboard row to the search page. - The source and where condition passed to the search page is customizable via handlebars templates, which can reference the column values in the clicked row. ### Not included yet Future PRs will add: - Linking to specific sources, without requiring a template, with source mapping during dashboard import - Linking to dashboards, by templated dashboard name or id (with mapping during dashboard import) - Passing templated filter values to the destination dashboard or search - Updates to the external API to support the new onClick fields - Updates to the MCP prompts to support generating dashboards with custom links - Updates to the import flow to support importing bundles of linked dashboards at once - Support for linking on other visualization types ## Screenshots or video https://github.com/user-attachments/assets/afdd59cf-53bd-4ae5-b4fa-acc565409eb6 ## How to test locally or on Vercel This can be tested in the preview environment ## References - Linear Issue: Closes HDX-4063 - Related PRs:
1 parent 11e1301 commit e2fc25d

25 files changed

Lines changed: 1303 additions & 60 deletions

‎.changeset/sharp-weeks-impress.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
feat: Add custom table onClick behavior

‎packages/app/.env.development‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ OTEL_SERVICE_NAME="hdx-oss-dev-app"
77
PORT=${HYPERDX_APP_PORT}
88
NODE_OPTIONS="--max-http-header-size=131072"
99
NEXT_PUBLIC_HYPERDX_BASE_PATH=
10-
NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${HDX_DEV_OTEL_HTTP_PORT:-4318}"
10+
NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${HDX_DEV_OTEL_HTTP_PORT:-4318}"
11+
NEXT_PUBLIC_IS_DASHBOARD_LINKING_ENABLED=true

‎packages/app/scripts/run-e2e.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ const env = {
5858
...process.env,
5959
...(!useLocal && { E2E_FULLSTACK: 'true' }),
6060
...(useDev && { E2E_USE_DEV: 'true' }),
61+
// Feature flags that should always be enabled during E2E runs. NEXT_PUBLIC_*
62+
// vars are baked into the build, so they must be present in env at `next
63+
// build` time — not just in .env.development (which isn't loaded in prod
64+
// builds).
65+
NEXT_PUBLIC_IS_DASHBOARD_LINKING_ENABLED: 'true',
6166
};
6267

6368
// Port configuration from HDX_E2E_* env vars (set by scripts/test-e2e.sh)

‎packages/app/src/HDXMultiSeriesTableChart.tsx‎

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useCallback, useMemo, useRef, useState } from 'react';
2-
import Link from 'next/link';
32
import cx from 'classnames';
43
import { UnstyledButton } from '@mantine/core';
54
import { IconDownload, IconTextWrap } from '@tabler/icons-react';
@@ -35,7 +34,7 @@ export const Table = ({
3534
data,
3635
groupColumnName,
3736
columns,
38-
getRowSearchLink,
37+
onRowClick,
3938
tableBottom,
4039
enableClientSideSorting = false,
4140
sorting,
@@ -54,7 +53,7 @@ export const Table = ({
5453
sortingFn?: SortingFnOption<any>;
5554
}[];
5655
groupColumnName?: string;
57-
getRowSearchLink?: (row: any) => string | null;
56+
onRowClick?: (row: any, e?: React.MouseEvent) => void;
5857
tableBottom?: React.ReactNode;
5958
enableClientSideSorting?: boolean;
6059
sorting: SortingState;
@@ -142,40 +141,46 @@ export const Table = ({
142141
formattedValue = formatNumber(value, numberFormat);
143142
}
144143

145-
if (getRowSearchLink == null) {
146-
return formattedValue;
147-
}
148-
const link = getRowSearchLink(row.original);
144+
const className = cx('align-top overflow-hidden py-1 pe-3', {
145+
'text-break': wrapLinesEnabled,
146+
'text-truncate': !wrapLinesEnabled,
147+
});
149148

150-
if (!link) {
149+
if (onRowClick) {
151150
return (
152151
<div
153-
className={cx('align-top overflow-hidden py-1 pe-3', {
154-
'text-break': wrapLinesEnabled,
155-
'text-truncate': !wrapLinesEnabled,
156-
})}
152+
role="link"
153+
tabIndex={0}
154+
className={className}
155+
style={{ cursor: 'pointer' }}
156+
// Left-click: fires onClick with button === 0. The parent
157+
// handler detects meta/ctrl for cmd/ctrl-click → new tab.
158+
onClick={e => onRowClick(row.original, e)}
159+
// Middle-click (button === 1) fires onAuxClick but NOT
160+
// onClick on non-anchor elements.
161+
onAuxClick={e => {
162+
if (e.button === 1) {
163+
e.preventDefault();
164+
onRowClick(row.original, e);
165+
}
166+
}}
167+
// Suppress the browser's middle-click autoscroll cursor on non-anchor elements.
168+
onMouseDown={e => {
169+
if (e.button === 1) e.preventDefault();
170+
}}
171+
onKeyDown={e => {
172+
if (e.key === 'Enter' || e.key === ' ') {
173+
e.preventDefault();
174+
onRowClick(row.original);
175+
}
176+
}}
157177
>
158178
{formattedValue}
159179
</div>
160180
);
161181
}
162182

163-
return (
164-
<Link
165-
href={link}
166-
className={cx('align-top overflow-hidden py-1 pe-3', {
167-
'text-break': wrapLinesEnabled,
168-
'text-truncate': !wrapLinesEnabled,
169-
})}
170-
style={{
171-
display: 'block',
172-
color: 'inherit',
173-
textDecoration: 'none',
174-
}}
175-
>
176-
{formattedValue}
177-
</Link>
178-
);
183+
return <div className={className}>{formattedValue}</div>;
179184
},
180185
size:
181186
i === numColumns - 2

‎packages/app/src/components/ChartEditor/RawSqlChartEditor.tsx‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { getAllMetricTables, usePrevious } from '@/utils';
3030
import { DEFAULT_TILE_ALERT } from '@/utils/alerts';
3131

3232
import { ConnectionSelectControlled } from '../ConnectionSelect';
33+
import { OnClickFormButton } from '../DBEditTimeChartForm/OnClickForm/OnClickFormButton';
3334
import SourceSchemaPreview from '../SourceSchemaPreview';
3435
import { SourceSelectControlled } from '../SourceSelect';
3536

@@ -43,13 +44,15 @@ export default function RawSqlChartEditor({
4344
control,
4445
setValue,
4546
onOpenDisplaySettings,
47+
onSubmit,
4648
isDashboardForm,
4749
alert,
4850
dashboardId,
4951
}: {
5052
control: Control<ChartEditorFormState>;
5153
setValue: UseFormSetValue<ChartEditorFormState>;
5254
onOpenDisplaySettings: () => void;
55+
onSubmit: (suppressErrorNotification?: boolean) => void;
5356
isDashboardForm: boolean;
5457
alert: ChartEditorFormState['alert'];
5558
dashboardId?: string;
@@ -222,13 +225,23 @@ export default function RawSqlChartEditor({
222225
Add Alert
223226
</Button>
224227
)}
225-
<Button
226-
onClick={onOpenDisplaySettings}
227-
size="compact-sm"
228-
variant="secondary"
229-
>
230-
Display Settings
231-
</Button>
228+
229+
<Group>
230+
{displayType === DisplayType.Table && (
231+
<OnClickFormButton
232+
control={control}
233+
setValue={setValue}
234+
onSubmit={onSubmit}
235+
/>
236+
)}
237+
<Button
238+
onClick={onOpenDisplaySettings}
239+
size="compact-sm"
240+
variant="secondary"
241+
>
242+
Display Settings
243+
</Button>
244+
</Group>
232245
</Group>
233246
</Group>
234247
<RawSqlChartInstructions displayType={displayType ?? DisplayType.Table} />

‎packages/app/src/components/ChartEditor/utils.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { ChartEditorFormState } from './types';
2828
function normalizeChartConfig<
2929
C extends Pick<
3030
BuilderSavedChartConfig,
31-
'select' | 'having' | 'orderBy' | 'displayType' | 'metricTables'
31+
'select' | 'having' | 'orderBy' | 'displayType' | 'metricTables' | 'onClick'
3232
>,
3333
>(config: C, source: TSource): C {
3434
const isMetricSource = source.kind === SourceKind.Metric;
@@ -45,6 +45,10 @@ function normalizeChartConfig<
4545
config.displayType === DisplayType.Table ? config.having : undefined,
4646
orderBy:
4747
config.displayType === DisplayType.Table ? config.orderBy : undefined,
48+
onClick:
49+
config.onClick && config.displayType === DisplayType.Table
50+
? config.onClick
51+
: undefined,
4852
};
4953
}
5054

@@ -78,6 +82,7 @@ export function convertFormStateToSavedChartConfig(
7882
'fillNulls',
7983
'alignDateRangeToGranularity',
8084
'alert',
85+
'onClick',
8186
]),
8287
sqlTemplate: form.sqlTemplate ?? '',
8388
connection: form.connection ?? '',
@@ -121,6 +126,7 @@ export function convertFormStateToChartConfig(
121126
'compareToPreviousPeriod',
122127
'fillNulls',
123128
'alignDateRangeToGranularity',
129+
'onClick',
124130
]),
125131
sqlTemplate: form.sqlTemplate ?? '',
126132
connection: form.connection ?? '',

‎packages/app/src/components/DBEditTimeChartForm/ChartEditorControls.tsx‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEdito
2828
import { IS_LOCAL_MODE } from '@/config';
2929
import { DEFAULT_TILE_ALERT } from '@/utils/alerts';
3030

31+
import { OnClickFormButton } from './OnClickForm/OnClickFormButton';
3132
import { ChartSeriesEditor } from './ChartSeriesEditor';
3233
import { TileAlertEditor } from './TileAlertEditor';
3334

@@ -259,13 +260,22 @@ export function ChartEditorControls({
259260
</Button>
260261
)}
261262
</Group>
262-
<Button
263-
onClick={openDisplaySettings}
264-
size="compact-sm"
265-
variant="secondary"
266-
>
267-
Display Settings
268-
</Button>
263+
<Group>
264+
{displayType === DisplayType.Table && (
265+
<OnClickFormButton
266+
control={control}
267+
setValue={setValue}
268+
onSubmit={onSubmit}
269+
/>
270+
)}
271+
<Button
272+
onClick={openDisplaySettings}
273+
size="compact-sm"
274+
variant="secondary"
275+
>
276+
Display Settings
277+
</Button>
278+
</Group>
269279
</Flex>
270280
</>
271281
) : (

‎packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ export default function EditTimeChartForm({
589589
control={control}
590590
setValue={setValue}
591591
onOpenDisplaySettings={openDisplaySettings}
592+
onSubmit={onSubmit}
592593
isDashboardForm={isDashboardForm}
593594
alert={alert}
594595
dashboardId={dashboardId}

0 commit comments

Comments
 (0)