Skip to content

Commit 32f1189

Browse files
authored
feat: Add RawSqlChartConfig types for SQL-based Table (#1846)
## Summary This PR is the first step towards raw SQL-driven charts. - It introduces updated ChartConfig types, which are now unions of `BuilderChartConfig` (which is unchanged from the current `ChartConfig` types` and `RawSqlChartConfig` types which represent sql-driven charts. - It adds _very basic_ support for SQL-driven tables in the Chart Explorer and Dashboard pages. This is currently behind a feature toggle and enabled only in preview environments and for local development. The changes in most of the files in this PR are either type updates or the addition of type guards to handle the new ChartConfig union type. The DBEditTimeChartForm has been updated significantly to (a) add the Raw SQL option to the table chart editor and (b) handle conversion from internal form state (which can now include properties from either branch of the ChartConfig union) to valid SavedChartConfigs (which may only include properties from one branch). Significant changes are in: - packages/app/src/components/ChartEditor/types.ts - packages/app/src/components/ChartEditor/RawSqlChartEditor.tsx - packages/app/src/components/ChartEditor/utils.ts - packages/app/src/components/DBEditTimeChartForm.tsx - packages/app/src/components/DBTableChart.tsx - packages/app/src/components/SQLEditor.tsx - packages/app/src/hooks/useOffsetPaginatedQuery.tsx Future PRs will add templating to the Raw SQL driven charts for date range and granularity injection; support for other chart types driven by SQL; improved placeholder, validation, and error states; and improved support in the external API and import/export. ### Screenshots or video https://github.com/user-attachments/assets/008579cc-ef3c-496e-9899-88bbb21eaa5e ### How to test locally or on Vercel The SQL-driven table can be tested in the preview environment or locally. ### References - Linear Issue: HDX-3580 - Related PRs:
1 parent 32d45f7 commit 32f1189

69 files changed

Lines changed: 1690 additions & 798 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/fast-swans-brake.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
"@hyperdx/api": patch
4+
"@hyperdx/app": patch
5+
---
6+
7+
feat: Add RawSqlChartConfig types for SQL-based Table

‎packages/api/src/controllers/dashboard.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { isBuilderSavedChartConfig } from '@hyperdx/common-utils/dist/guards';
12
import {
3+
BuilderSavedChartConfig,
24
DashboardWithoutIdSchema,
35
Tile,
46
} from '@hyperdx/common-utils/dist/types';
@@ -17,15 +19,17 @@ import Dashboard from '@/models/dashboard';
1719

1820
function pickAlertsByTile(tiles: Tile[]) {
1921
return tiles.reduce((acc, tile) => {
20-
if (tile.config.alert) {
22+
if (isBuilderSavedChartConfig(tile.config) && tile.config.alert) {
2123
acc[tile.id] = tile.config.alert;
2224
}
2325
return acc;
2426
}, {});
2527
}
2628

2729
type TileForAlertSync = Pick<Tile, 'id'> & {
28-
config?: Pick<Tile['config'], 'alert'> | { alert?: IAlert | AlertDocument };
30+
config?:
31+
| Pick<BuilderSavedChartConfig, 'alert'>
32+
| { alert?: IAlert | AlertDocument };
2933
};
3034

3135
function extractTileAlertData(tiles: TileForAlertSync[]): {
@@ -48,8 +52,15 @@ async function syncDashboardAlerts(
4852
): Promise<void> {
4953
const { tileIds: oldTileIds, tileIdsWithAlerts: oldTileIdsWithAlerts } =
5054
extractTileAlertData(oldTiles);
55+
56+
const newTilesForAlertSync: TileForAlertSync[] = newTiles.map(t => ({
57+
id: t.id,
58+
config: isBuilderSavedChartConfig(t.config)
59+
? { alert: t.config.alert }
60+
: {},
61+
}));
5162
const { tileIds: newTileIds, tileIdsWithAlerts: newTileIdsWithAlerts } =
52-
extractTileAlertData(newTiles);
63+
extractTileAlertData(newTilesForAlertSync);
5364

5465
// 1. Create/update alerts for tiles that have alerts
5566
const alertsByTile = pickAlertsByTile(newTiles);

‎packages/api/src/fixtures.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createNativeClient } from '@hyperdx/common-utils/dist/clickhouse/node';
22
import {
3+
BuilderSavedChartConfig,
34
DisplayType,
45
SavedChartConfig,
56
Tile,
@@ -421,7 +422,8 @@ export const randomMongoId = () =>
421422

422423
export const makeTile = (opts?: {
423424
id?: string;
424-
alert?: SavedChartConfig['alert'];
425+
alert?: BuilderSavedChartConfig['alert'];
426+
sourceId?: string;
425427
}): Tile => ({
426428
id: opts?.id ?? randomMongoId(),
427429
x: 1,
@@ -433,10 +435,11 @@ export const makeTile = (opts?: {
433435

434436
export const makeChartConfig = (opts?: {
435437
id?: string;
436-
alert?: SavedChartConfig['alert'];
438+
alert?: BuilderSavedChartConfig['alert'];
439+
sourceId?: string;
437440
}): SavedChartConfig => ({
438441
name: 'Test Chart',
439-
source: 'test-source',
442+
source: opts?.sourceId ?? 'test-source',
440443
displayType: DisplayType.Line,
441444
select: [
442445
{

‎packages/api/src/routers/external-api/v2/utils/dashboards.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { isRawSqlSavedChartConfig } from '@hyperdx/common-utils/dist/guards';
12
import {
23
AggregateFunctionSchema,
4+
BuilderSavedChartConfig,
35
DisplayType,
46
SavedChartConfig,
57
} from '@hyperdx/common-utils/dist/types';
@@ -62,7 +64,7 @@ const DEFAULT_SELECT_ITEM: ExternalDashboardSelectItem = {
6264
};
6365

6466
const convertToExternalSelectItem = (
65-
item: Exclude<SavedChartConfig['select'][number], string>,
67+
item: Exclude<BuilderSavedChartConfig['select'][number], string>,
6668
): ExternalDashboardSelectItem => {
6769
const parsedAggFn = AggregateFunctionSchema.safeParse(item.aggFn);
6870
const aggFn = parsedAggFn.success ? parsedAggFn.data : 'none';
@@ -84,6 +86,9 @@ const convertToExternalSelectItem = (
8486
const convertToExternalTileChartConfig = (
8587
config: SavedChartConfig,
8688
): ExternalDashboardTileConfig | undefined => {
89+
// HDX-3582: Implement this for Raw SQL charts
90+
if (isRawSqlSavedChartConfig(config)) return undefined;
91+
8792
const sourceId = config.source?.toString() ?? '';
8893

8994
const stringValueOrDefault = <D>(
@@ -175,7 +180,10 @@ const convertToExternalTileChartConfig = (
175180

176181
function convertTileToExternalChart(
177182
tile: DashboardDocument['tiles'][number],
178-
): ExternalDashboardTileWithId {
183+
): ExternalDashboardTileWithId | undefined {
184+
// HDX-3582: Implement this for Raw SQL charts
185+
if (isRawSqlSavedChartConfig(tile.config)) return undefined;
186+
179187
// Returned in case of a failure converting the saved chart config
180188
const defaultTileConfig: ExternalDashboardTileConfig = {
181189
displayType: 'line',
@@ -196,7 +204,9 @@ export function convertToExternalDashboard(
196204
return {
197205
id: dashboard._id.toString(),
198206
name: dashboard.name,
199-
tiles: dashboard.tiles.map(convertTileToExternalChart),
207+
tiles: dashboard.tiles
208+
.map(convertTileToExternalChart)
209+
.filter(t => t !== undefined),
200210
tags: dashboard.tags || [],
201211
filters: dashboard.filters?.map(translateFilterToExternalFilter) || [],
202212
savedQuery: dashboard.savedQuery ?? null,
@@ -211,7 +221,7 @@ export function convertToExternalDashboard(
211221

212222
const convertToInternalSelectItem = (
213223
item: ExternalDashboardSelectItem,
214-
): Exclude<SavedChartConfig['select'][number], string> => {
224+
): Exclude<BuilderSavedChartConfig['select'][number], string> => {
215225
return {
216226
...pick(item, ['alias', 'metricType', 'metricName', 'aggFn', 'level']),
217227
aggCondition: item.where,

‎packages/api/src/tasks/checkAlerts/index.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { renderChartConfig } from '@hyperdx/common-utils/dist/core/renderChartCo
1717
import { aliasMapToWithClauses } from '@hyperdx/common-utils/dist/core/utils';
1818
import { timeBucketByGranularity } from '@hyperdx/common-utils/dist/core/utils';
1919
import {
20-
ChartConfigWithOptDateRange,
20+
isBuilderSavedChartConfig,
21+
isRawSqlSavedChartConfig,
22+
} from '@hyperdx/common-utils/dist/guards';
23+
import {
24+
BuilderChartConfigWithOptDateRange,
2125
DisplayType,
2226
} from '@hyperdx/common-utils/dist/types';
2327
import * as fns from 'date-fns';
@@ -67,6 +71,7 @@ export const alertHasGroupBy = (details: AlertDetails): boolean => {
6771
}
6872
if (
6973
details.taskType === AlertTaskType.TILE &&
74+
isBuilderSavedChartConfig(details.tile.config) &&
7075
details.tile.config.groupBy &&
7176
details.tile.config.groupBy.length > 0
7277
) {
@@ -84,10 +89,10 @@ export async function computeAliasWithClauses(
8489
savedSearch: Pick<ISavedSearch, 'select' | 'where' | 'whereLanguage'>,
8590
source: ISource,
8691
metadata: Metadata,
87-
): Promise<ChartConfigWithOptDateRange['with']> {
92+
): Promise<BuilderChartConfigWithOptDateRange['with']> {
8893
const resolvedSelect =
8994
savedSearch.select || source.defaultTableSelectExpression || '';
90-
const config: ChartConfigWithOptDateRange = {
95+
const config: BuilderChartConfigWithOptDateRange = {
9196
connection: '',
9297
displayType: DisplayType.Search,
9398
from: source.from,
@@ -317,7 +322,7 @@ const getChartConfigFromAlert = (
317322
connection: string,
318323
dateRange: [Date, Date],
319324
windowSizeInMins: number,
320-
): ChartConfigWithOptDateRange | undefined => {
325+
): BuilderChartConfigWithOptDateRange | undefined => {
321326
const { alert, source } = details;
322327
if (details.taskType === AlertTaskType.SAVED_SEARCH) {
323328
const savedSearch = details.savedSearch;
@@ -344,6 +349,10 @@ const getChartConfigFromAlert = (
344349
};
345350
} else if (details.taskType === AlertTaskType.TILE) {
346351
const tile = details.tile;
352+
353+
// Alerts are not supported for raw sql based charts
354+
if (isRawSqlSavedChartConfig(tile.config)) return undefined;
355+
347356
// Doesn't work for metric alerts yet
348357
if (
349358
tile.config.displayType === DisplayType.Line ||

‎packages/api/src/tasks/checkAlerts/providers/__tests__/default.test.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,10 @@ describe('DefaultAlertProvider', () => {
148148
});
149149

150150
// Create tile with source
151-
const tile = makeTile({ id: 'test-tile-123' });
152-
tile.config.source = source._id.toString();
151+
const tile = makeTile({
152+
id: 'test-tile-123',
153+
sourceId: source._id.toString(),
154+
});
153155

154156
// Create dashboard
155157
const dashboard = await Dashboard.create({
@@ -301,8 +303,10 @@ describe('DefaultAlertProvider', () => {
301303
);
302304

303305
// Create tile and alert
304-
const tile = makeTile({ id: 'test-tile-123' });
305-
tile.config.source = source._id.toString();
306+
const tile = makeTile({
307+
id: 'test-tile-123',
308+
sourceId: source._id.toString(),
309+
});
306310

307311
const dashboard = await Dashboard.create({
308312
team: team._id,
@@ -503,8 +507,10 @@ describe('DefaultAlertProvider', () => {
503507
it('should skip alerts with missing source', async () => {
504508
const team = await createTeam({ name: 'Test Team' });
505509

506-
const tile = makeTile({ id: 'test-tile' });
507-
tile.config.source = new mongoose.Types.ObjectId().toString(); // Non-existent source
510+
const tile = makeTile({
511+
id: 'test-tile',
512+
sourceId: new mongoose.Types.ObjectId().toString(), // Non-existent source
513+
});
508514

509515
const dashboard = await Dashboard.create({
510516
team: team._id,
@@ -684,8 +690,10 @@ describe('DefaultAlertProvider', () => {
684690
});
685691

686692
// Create tile with source
687-
const tile = makeTile({ id: 'test-tile-123' });
688-
tile.config.source = source._id.toString();
693+
const tile = makeTile({
694+
id: 'test-tile-123',
695+
sourceId: source._id.toString(),
696+
});
689697

690698
// Create dashboard
691699
const dashboard = await Dashboard.create({

‎packages/api/src/tasks/checkAlerts/providers/default.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node';
2+
import { isRawSqlSavedChartConfig } from '@hyperdx/common-utils/dist/guards';
23
import { Tile } from '@hyperdx/common-utils/dist/types';
34
import mongoose from 'mongoose';
45
import ms from 'ms';
@@ -106,6 +107,16 @@ async function getTileDetails(
106107
return [];
107108
}
108109

110+
if (isRawSqlSavedChartConfig(tile.config)) {
111+
logger.warn({
112+
tileId,
113+
dashboardId: dashboard._id,
114+
alertId: alert.id,
115+
message: 'skipping alert with raw sql chart config, not supported',
116+
});
117+
return [];
118+
}
119+
109120
const source = await Source.findOne({
110121
_id: tile.config.source,
111122
team: alert.team,

0 commit comments

Comments
 (0)