Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/sql-to-builder-conversion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperdx/common-utils': minor
'@hyperdx/app': minor
---

feat: Support conversion from Raw SQL to Builder charts
28 changes: 28 additions & 0 deletions packages/app/src/components/ChartEditor/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FieldPath, FieldPathValue, SetValueConfig } from 'react-hook-form';
import {
BuilderSavedChartConfig,
PromqlSavedChartConfig,
Expand Down Expand Up @@ -35,3 +36,30 @@ export type ChartEditorFormState = Partial<BuilderSavedChartConfig> &
series: SavedChartConfigWithSelectArray['select'];
configType?: 'sql' | 'builder' | 'promql';
};

/**
* Options accepted by {@link ChartFormSetValue}: react-hook-form's standard
* `setValue` config plus `isUserChange`, which records that the write came from
* a genuine user edit (see below).
*/
export type ChartFormSetValueOptions = SetValueConfig & {
isUserChange?: boolean;
};

/**
* A `setValue` wrapper for the chart editor form that also records whether the
* write came from a genuine user edit. Builder ⇄ SQL conversion uses this to
* tell user edits apart from the many programmatic `setValue` writes (defaults,
* resets, its own generated output). Pass `{ isUserChange: true }` only when the
* user directly changed the field via a control that isn't a registered
* react-hook-form input (toggles, pickers, table sorts, helper buttons, etc.).
*
* The flag lives in the options object (rather than a positional argument) so
* this stays assignable to `UseFormSetValue` and can be threaded through
* components that only forward `setValue` without any changes.
*/
export type ChartFormSetValue = <TName extends FieldPath<ChartEditorFormState>>(
name: TName,
value: FieldPathValue<ChartEditorFormState, TName>,
options?: ChartFormSetValueOptions,
) => void;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
FieldArrayWithId,
FieldErrors,
UseFormClearErrors,
UseFormSetValue,
useWatch,
} from 'react-hook-form';
import { TableConnection } from '@hyperdx/common-utils/dist/core/metadata';
Expand All @@ -23,6 +22,7 @@ import { IconBell, IconCirclePlus } from '@tabler/icons-react';

import {
ChartEditorFormState,
ChartFormSetValue,
SavedChartConfigWithSelectArray,
} from '@/components/ChartEditor/types';
import MVOptimizationIndicator from '@/components/MaterializedViews/MVOptimizationIndicator';
Expand All @@ -44,7 +44,7 @@ import { buildGroupByConnectionProps } from './utils';

type ChartEditorControlsProps = {
control: Control<ChartEditorFormState>;
setValue: UseFormSetValue<ChartEditorFormState>;
setValue: ChartFormSetValue;
clearErrors: UseFormClearErrors<ChartEditorFormState>;
errors: FieldErrors<ChartEditorFormState>;
fields: FieldArrayWithId<ChartEditorFormState, 'series', 'id'>[];
Expand Down Expand Up @@ -203,7 +203,7 @@ export function ChartEditorControls({
name="where"
onSubmit={onSubmit}
onLanguageChange={(lang: 'sql' | 'lucene') =>
setValue('whereLanguage', lang)
setValue('whereLanguage', lang, { isUserChange: true })
}
showLabel={false}
/>
Expand Down Expand Up @@ -335,6 +335,7 @@ export function ChartEditorControls({
setValue(
'seriesReturnType',
seriesReturnType === 'ratio' ? 'column' : 'ratio',
{ isUserChange: true },
);
onSubmit();
}}
Expand Down Expand Up @@ -433,7 +434,7 @@ export function ChartEditorControls({
name="where"
onSubmit={onSubmit}
onLanguageChange={(lang: 'sql' | 'lucene') =>
setValue('whereLanguage', lang)
setValue('whereLanguage', lang, { isUserChange: true })
}
showLabel={false}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import { SortingState } from '@tanstack/react-table';

import { buildTableRowSearchUrl } from '@/ChartUtils';
import { getAlertReferenceLines } from '@/components/Alerts';
import { ChartEditorFormState } from '@/components/ChartEditor/types';
import {
ChartEditorFormState,
ChartFormSetValueOptions,
} from '@/components/ChartEditor/types';
import ChartSQLPreview from '@/components/ChartSQLPreview';
import { DBBarChart } from '@/components/DBBarChart';
import DBHeatmapChart, {
Expand Down Expand Up @@ -128,7 +131,11 @@ type ChartPreviewPanelProps = {
showGeneratedSql: boolean;
showSampleEvents: boolean;
dbTimeChartConfig?: ChartConfigWithDateRange;
setValue: (name: 'orderBy', value: string) => void;
setValue: (
name: 'orderBy',
value: string,
options?: ChartFormSetValueOptions,
) => void;
onSubmit: () => void;
};

Expand All @@ -153,7 +160,9 @@ export function ChartPreviewPanel({

const onTableSortingChange = useCallback(
(sortState: SortingState | null) => {
setValue('orderBy', sortingStateToOrderByString(sortState) ?? '');
setValue('orderBy', sortingStateToOrderByString(sortState) ?? '', {
isUserChange: true,
});
onSubmit();
},
[setValue, onSubmit],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Control,
FieldErrors,
UseFormClearErrors,
UseFormSetValue,
useWatch,
} from 'react-hook-form';
import {
Expand Down Expand Up @@ -33,6 +32,7 @@ import { AGG_FNS } from '@/ChartUtils';
import { AggFnSelectControlled } from '@/components/AggFnSelect';
import {
ChartEditorFormState,
ChartFormSetValue,
SavedChartConfigWithSelectArray,
} from '@/components/ChartEditor/types';
import {
Expand Down Expand Up @@ -68,7 +68,7 @@ type ChartSeriesEditorProps = {
onSwapSeries: (from: number, to: number) => void;
onDuplicateSeries: (index: number) => void;
onSubmit: () => void;
setValue: UseFormSetValue<ChartEditorFormState>;
setValue: ChartFormSetValue;
showGroupBy: boolean;
showHaving: boolean;
showDuplicate: boolean;
Expand Down Expand Up @@ -175,7 +175,7 @@ export function ChartSeriesEditor({
const currentValue = aggCondition || '';

const newValue = currentValue ? `${currentValue} AND ${clause}` : clause;
setValue(`${namePrefix}aggCondition`, newValue);
setValue(`${namePrefix}aggCondition`, newValue, { isUserChange: true });
onSubmit();
},
[aggCondition, namePrefix, setValue, onSubmit],
Expand All @@ -185,7 +185,7 @@ export function ChartSeriesEditor({
(clause: string) => {
const currentValue = groupBy || '';
const newValue = currentValue ? `${currentValue}, ${clause}` : clause;
setValue('groupBy', newValue);
setValue('groupBy', newValue, { isUserChange: true });
onSubmit();
},
[groupBy, setValue, onSubmit],
Expand Down Expand Up @@ -316,11 +316,17 @@ export function ChartSeriesEditor({
metricName={metricName}
metricType={metricType}
setMetricName={value => {
setValue(`${namePrefix}metricName`, value);
setValue(`${namePrefix}valueExpression`, 'Value');
setValue(`${namePrefix}metricName`, value, {
isUserChange: true,
});
setValue(`${namePrefix}valueExpression`, 'Value', {
isUserChange: true,
});
}}
setMetricType={value =>
setValue(`${namePrefix}metricType`, value)
setValue(`${namePrefix}metricType`, value, {
isUserChange: true,
})
}
metricSource={tableSource}
data-testid="metric-name-selector"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import { ChartActionBar } from './ChartActionBar';
import { ChartEditorControls } from './ChartEditorControls';
import { ChartPreviewPanel } from './ChartPreviewPanel';
import { ErrorNotificationMessage } from './ErrorNotificationMessage';
import { useBuilderToSqlConversion } from './useBuilderToSqlConversion';
import { useBuilderSqlConversion } from './useBuilderSqlConversion';
import {
buildChartConfigForExplanations,
computeDbTimeChartConfig,
Expand Down Expand Up @@ -158,7 +158,7 @@ export default function EditTimeChartForm({
setValue,
getValues,
// The callback form of `watch` is used to subscribe to field changes
// (without re-rendering) in useBuilderToSqlConversion; useWatch can't do this.
// (without re-rendering) in useBuilderSqlConversion; useWatch can't do this.
// eslint-disable-next-line react-hook-form/no-use-watch
watch,
handleSubmit,
Expand Down Expand Up @@ -234,8 +234,13 @@ export default function EditTimeChartForm({
const databaseName = tableSource?.from.databaseName;
const tableName = tableSource?.from.tableName;

// Carry the builder config over as a SQL template when switching to SQL mode
useBuilderToSqlConversion({
// Carry the query over between builder and SQL modes when the config type is
// toggled (builder → SQL template, and SQL template → builder fields).
// `setValueWithEditTracking` wraps `setValue` and additionally records genuine
// user edits (passed with `isUserChange: true`) so builder ⇄ SQL conversion
// knows which representation is freshest. Hand it to the form controls in
// place of the raw `setValue`.
const { setValue: setValueWithEditTracking } = useBuilderSqlConversion({
control,
getValues,
setValue,
Expand Down Expand Up @@ -637,15 +642,26 @@ export default function EditTimeChartForm({
if (numberFormat !== undefined) {
setValue('numberFormat', numberFormat);
}
setValue('alignDateRangeToGranularity', alignDateRangeToGranularity);
setValue('fillNulls', fillNulls);
setValue('compareToPreviousPeriod', compareToPreviousPeriod);
// These affect the generated query, so mark them as user edits.
setValueWithEditTracking(
'alignDateRangeToGranularity',
alignDateRangeToGranularity,
{ isUserChange: true },
);
setValueWithEditTracking('fillNulls', fillNulls, { isUserChange: true });
setValueWithEditTracking(
'compareToPreviousPeriod',
compareToPreviousPeriod,
{ isUserChange: true },
);
setValue('fitYAxisToData', fitYAxisToData);
setValue('groupByColumnsOnLeft', groupByColumnsOnLeft);
// Persist `null` (not undefined) when cleared so the disabled state
// survives JSON round-tripping through the URL query state; otherwise
// the dropped key lets RHF's `values` sync restore the stale value.
setValue('seriesLimit', seriesLimit ?? null);
setValueWithEditTracking('seriesLimit', seriesLimit ?? null, {
isUserChange: true,
});
setValue('color', color);
setValue('colorRules', colorRules);
setValue('backgroundChart', backgroundChart);
Expand All @@ -657,21 +673,29 @@ export default function EditTimeChartForm({
}
onSubmit();
},
[setValue, onDirtyChange, onSubmit],
[setValue, setValueWithEditTracking, onDirtyChange, onSubmit],
);

const handleUpdateHeatmapSettings = useCallback(
(data: HeatmapSettingsValues) => {
setValue('series.0.valueExpression', data.value);
setValue('series.0.countExpression', data.count || 'count()');
setValue('series.0.heatmapScaleType', data.scaleType);
setValueWithEditTracking('series.0.valueExpression', data.value, {
isUserChange: true,
});
setValueWithEditTracking(
'series.0.countExpression',
data.count || 'count()',
{ isUserChange: true },
);
setValueWithEditTracking('series.0.heatmapScaleType', data.scaleType, {
isUserChange: true,
});
// Heatmap settings are applied outside RHF's change tracking.
subFormDirty.current = true;
onDirtyChange?.(true);
onSubmit();
closeHeatmapSettings();
},
[setValue, onDirtyChange, onSubmit, closeHeatmapSettings],
[setValueWithEditTracking, onDirtyChange, onSubmit, closeHeatmapSettings],
);

const heatmapValueExpression = useWatch({
Expand Down Expand Up @@ -838,7 +862,7 @@ export default function EditTimeChartForm({
) : isRawSqlInput ? (
<RawSqlChartEditor
control={control}
setValue={setValue}
setValue={setValueWithEditTracking}
onOpenDisplaySettings={openDisplaySettings}
onSubmit={onSubmit}
isDashboardForm={isDashboardForm}
Expand All @@ -848,7 +872,7 @@ export default function EditTimeChartForm({
) : (
<ChartEditorControls
control={control}
setValue={setValue}
setValue={setValueWithEditTracking}
clearErrors={clearErrors}
errors={errors}
fields={fields}
Expand Down Expand Up @@ -908,7 +932,7 @@ export default function EditTimeChartForm({
showGeneratedSql={showGeneratedSql}
showSampleEvents={showSampleEvents}
dbTimeChartConfig={dbTimeChartConfig}
setValue={(name, value) => setValue(name, value)}
setValue={setValueWithEditTracking}
onSubmit={onSubmit}
/>
<SaveToDashboardModal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useMemo } from 'react';
import { Control, UseFormSetValue } from 'react-hook-form';
import { Control } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { Button, Divider, Flex } from '@mantine/core';

import { ChartEditorFormState } from '@/components/ChartEditor/types';
import {
ChartEditorFormState,
ChartFormSetValue,
} from '@/components/ChartEditor/types';
import SearchWhereInput from '@/components/SearchInput/SearchWhereInput';

type HeatmapSeriesEditorProps = {
control: Control<ChartEditorFormState>;
setValue: UseFormSetValue<ChartEditorFormState>;
setValue: ChartFormSetValue;
tableSource?: TSource;
onSubmit: () => void;
onOpenDisplaySettings: () => void;
Expand All @@ -32,7 +35,7 @@ export function HeatmapSeriesEditor({
name="where"
onSubmit={onSubmit}
onLanguageChange={(lang: 'sql' | 'lucene') =>
setValue('whereLanguage', lang)
setValue('whereLanguage', lang, { isUserChange: true })
}
showLabel={false}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { classifyFormEdit } from '@/components/DBEditTimeChartForm/useBuilderToSqlConversion';
import { classifyFormEdit } from '@/components/DBEditTimeChartForm/useBuilderSqlConversion';

describe('classifyFormEdit', () => {
it('classifies a SQL editor change in SQL mode as a SQL edit', () => {
Expand Down
Loading
Loading