Skip to content

Commit cab98c7

Browse files
authored
feat: Substitute variables in Raw SQL charts (#2873)
1 parent d201b71 commit cab98c7

21 files changed

Lines changed: 1982 additions & 59 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@hyperdx/app': patch
3+
'@hyperdx/common-utils': patch
4+
---
5+
6+
feat: Substitute dashboard variables in raw SQL tiles

‎packages/app/src/DBDashboardPage.tsx‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
AlertState,
4848
BuilderChartConfigWithDateRange,
4949
ChartConfigWithDateRange,
50+
ChartVariable,
5051
DashboardContainer as DashboardContainerSchema,
5152
DashboardFilter,
5253
DisplayType,
@@ -60,6 +61,7 @@ import {
6061
SQLInterval,
6162
TSource,
6263
} from '@hyperdx/common-utils/dist/types';
64+
import { filterReferencedVariables } from '@hyperdx/common-utils/dist/variables';
6365
import {
6466
ActionIcon,
6567
Alert,
@@ -386,6 +388,7 @@ const Tile = forwardRef(
386388
granularity,
387389
onTimeRangeSelect,
388390
filters,
391+
variables,
389392
showAlertAnnotations,
390393
isLive,
391394
readOnly,
@@ -414,6 +417,7 @@ const Tile = forwardRef(
414417
granularity: SQLInterval | undefined;
415418
onTimeRangeSelect: (start: Date, end: Date) => void;
416419
filters?: Filter[];
420+
variables?: ChartVariable[];
417421
// When true, draw alert firing/recovery annotations on this tile's chart.
418422
showAlertAnnotations?: boolean;
419423
isLive?: boolean;
@@ -530,6 +534,25 @@ const Tile = forwardRef(
530534
displayTypeRequiresSource(chart.config.displayType) &&
531535
!chart.config.source;
532536

537+
// `variables` is a new reference every time the dashboard's filter change. To ensure
538+
// `tileVariables` is stable unless the tile's *referenced variables* actually change,
539+
// we serialize the referenced subset and use changes in the serialized value to drive
540+
// changes to `tileVariables`.
541+
const serializedTileVariables = useMemo(
542+
() =>
543+
!!variables && isRawSqlSavedChartConfig(chart.config)
544+
? JSON.stringify(filterReferencedVariables(chart.config, variables))
545+
: undefined,
546+
[chart.config, variables],
547+
);
548+
const tileVariables = useMemo<ChartVariable[] | undefined>(
549+
() =>
550+
serializedTileVariables
551+
? JSON.parse(serializedTileVariables)
552+
: undefined,
553+
[serializedTileVariables],
554+
);
555+
533556
useEffect(() => {
534557
if (isPromqlSavedChartConfig(chart.config)) {
535558
if (source != null) {
@@ -552,6 +575,7 @@ const Tile = forwardRef(
552575
dateRange,
553576
granularity,
554577
filters,
578+
variables: tileVariables,
555579
});
556580
} else if (source != null) {
557581
setQueriedConfig({
@@ -570,6 +594,7 @@ const Tile = forwardRef(
570594
dateRange,
571595
granularity,
572596
filters,
597+
variables: tileVariables,
573598
});
574599
}
575600

@@ -614,7 +639,7 @@ const Tile = forwardRef(
614639
});
615640
}
616641
}
617-
}, [source, chart, dateRange, granularity, filters]);
642+
}, [source, chart, dateRange, granularity, filters, tileVariables]);
618643

619644
const [hovered, setHovered] = useState(false);
620645

@@ -1472,13 +1497,15 @@ const EditTileModal = ({
14721497
onSave,
14731498
isSaving,
14741499
dateRange,
1500+
variables,
14751501
}: {
14761502
dashboardId?: string;
14771503
chart: Tile | undefined;
14781504
onClose: () => void;
14791505
dateRange: [Date, Date];
14801506
isSaving?: boolean;
14811507
onSave: (chart: Tile) => void;
1508+
variables?: ChartVariable[];
14821509
}) => {
14831510
const contextZIndex = useZIndex();
14841511
const modalZIndex = contextZIndex + 10;
@@ -1533,6 +1560,7 @@ const EditTileModal = ({
15331560
<EditTimeChartForm
15341561
dashboardId={dashboardId}
15351562
chartConfig={chart.config}
1563+
variables={variables}
15361564
dateRange={dateRange}
15371565
isSaving={isSaving}
15381566
onSave={config => {
@@ -1783,6 +1811,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
17831811
setFilterQueries,
17841812
ignoredFilterExpressions,
17851813
getFilterQueriesForSource,
1814+
variables,
17861815
} = useDashboardFilters(filters);
17871816

17881817
const { isLoading: isVariablesFlagLoading, isVariablesEnabled } =
@@ -2234,6 +2263,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
22342263
},
22352264
...getFilterQueriesForSource(tileSourceId),
22362265
]}
2266+
variables={showFilterVariableOptions ? variables : undefined}
22372267
onTimeRangeSelect={onTimeRangeSelect}
22382268
showAlertAnnotations={showAlertAnnotations}
22392269
isHighlighted={highlightedTileId === chart.id}
@@ -2334,6 +2364,8 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
23342364
onTimeRangeSelect,
23352365
showAlertAnnotations,
23362366
getFilterQueriesForSource,
2367+
showFilterVariableOptions,
2368+
variables,
23372369
moveTargetContainers,
23382370
handleMoveTileToGroup,
23392371
selectedTileIds,
@@ -3012,6 +3044,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
30123044
if (!isSaving) setEditedTile(undefined);
30133045
}}
30143046
dateRange={searchedTimeRange}
3047+
variables={showFilterVariableOptions ? variables : undefined}
30153048
isSaving={isSaving}
30163049
onSave={newChart => {
30173050
if (dashboard == null) {

‎packages/app/src/components/ChartSQLPreview.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export default function ChartSQLPreview({
112112
radius="sm"
113113
style={{ overflow: 'hidden' }}
114114
p="xs"
115+
data-testid="chart-sql-preview"
115116
>
116117
{data ? (
117118
// Prefer showing the (possibly placeholder) SQL over the loading state

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import {
1212
displayTypeSupportsBuilderAlerts,
1313
displayTypeSupportsRawSqlAlerts,
1414
} from '@hyperdx/common-utils/dist/core/utils';
15-
import { isRawSqlSavedChartConfig } from '@hyperdx/common-utils/dist/guards';
15+
import {
16+
isRawSqlChartConfig,
17+
isRawSqlSavedChartConfig,
18+
} from '@hyperdx/common-utils/dist/guards';
1619
import {
1720
ChartConfigWithDateRange,
21+
ChartVariable,
1822
DisplayType,
1923
SavedChartConfig,
2024
SourceKind,
@@ -88,13 +92,16 @@ import {
8892
buildChartConfigForExplanations,
8993
computeDbTimeChartConfig,
9094
displayTypeToActiveTab,
95+
resolvePreviewVariables,
9196
TABS_WITH_GENERATED_SQL,
9297
zSavedChartConfig,
9398
} from './utils';
9499

95100
type EditTimeChartFormProps = {
96101
dashboardId?: string;
97102
chartConfig: SavedChartConfig;
103+
/** Variables and their selected values, from the parent dashboard (if one exists). */
104+
variables?: ChartVariable[];
98105
displayedTimeInputValue?: string;
99106
dateRange: [Date, Date];
100107
isSaving?: boolean;
@@ -133,6 +140,7 @@ function applyHeatmapDefaults(
133140
export default function EditTimeChartForm({
134141
dashboardId,
135142
chartConfig,
143+
variables,
136144
displayedTimeInputValue,
137145
dateRange,
138146
isSaving,
@@ -364,9 +372,24 @@ export default function EditTimeChartForm({
364372
[],
365373
);
366374

375+
// Attach variables so that variable references can be validated and expanded in the preview
376+
const previewConfig = useMemo(() => {
377+
if (queriedConfig == null || !isRawSqlChartConfig(queriedConfig)) {
378+
return queriedConfig;
379+
}
380+
return {
381+
...queriedConfig,
382+
variables: resolvePreviewVariables({
383+
config: queriedConfig,
384+
variables,
385+
hasAlert: alert != null,
386+
}),
387+
};
388+
}, [queriedConfig, variables, alert]);
389+
367390
const dbTimeChartConfig = useMemo(
368-
() => computeDbTimeChartConfig(queriedConfig, alert),
369-
[queriedConfig, alert],
391+
() => computeDbTimeChartConfig(previewConfig, alert),
392+
[previewConfig, alert],
370393
);
371394

372395
const [saveToDashboardModalOpen, setSaveToDashboardModalOpen] =
@@ -595,7 +618,7 @@ export default function EditTimeChartForm({
595618
const chartConfigForExplanations = useMemo(
596619
() =>
597620
buildChartConfigForExplanations({
598-
queriedConfig,
621+
queriedConfig: previewConfig,
599622
queriedSourceId: queriedSource?.id,
600623
tableSource,
601624
chartConfig,
@@ -604,7 +627,7 @@ export default function EditTimeChartForm({
604627
dbTimeChartConfig,
605628
}),
606629
[
607-
queriedConfig,
630+
previewConfig,
608631
queriedSource?.id,
609632
tableSource,
610633
chartConfig,
@@ -903,7 +926,7 @@ export default function EditTimeChartForm({
903926
/>
904927
</ErrorBoundary>
905928
<ChartPreviewPanel
906-
queriedConfig={queriedConfig}
929+
queriedConfig={previewConfig}
907930
tableSource={tableSource}
908931
dateRange={dateRange}
909932
activeTab={activeTab}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ChartAlertBaseSchema,
1515
ChartConfigWithDateRange,
1616
ChartConfigWithOptTimestamp,
17+
ChartVariable,
1718
DisplayType,
1819
Filter,
1920
SavedChartConfig,
@@ -22,6 +23,7 @@ import {
2223
TSource,
2324
validateAlertScheduleOffsetMinutes,
2425
} from '@hyperdx/common-utils/dist/types';
26+
import { filterReferencedVariables } from '@hyperdx/common-utils/dist/variables';
2527

2628
import {
2729
convertToCategoricalChartConfig,
@@ -143,6 +145,28 @@ export function computeDbTimeChartConfig(
143145
};
144146
}
145147

148+
/**
149+
* Returns the dashboard variables a chart preview should use.
150+
* - Builder and PromQL configs don't yet support variables, so they resolve to an empty set
151+
* - Alerts always run with empty variable selections, so they resolve to each referenced variable with an empty `values` array.
152+
* - Otherwise, variables are filtered to only those referenced by the chart config.
153+
*/
154+
export function resolvePreviewVariables({
155+
config,
156+
variables,
157+
hasAlert,
158+
}: {
159+
config: ChartConfigWithDateRange;
160+
variables: ChartVariable[] | undefined;
161+
hasAlert: boolean;
162+
}): ChartVariable[] | undefined {
163+
if (!variables) return undefined;
164+
const referenced = filterReferencedVariables(config, variables);
165+
return hasAlert
166+
? referenced.map(variable => ({ ...variable, values: [] }))
167+
: referenced;
168+
}
169+
146170
export function buildSampleEventsConfig(
147171
queriedConfig: ChartConfigWithDateRange | undefined,
148172
tableSource: TSource | undefined,

0 commit comments

Comments
 (0)