Skip to content
Merged
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/builder-to-sql-carryover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperdx/common-utils': minor
'@hyperdx/app': minor
---

feat: Convert current builder config to SQL during editor switch
40 changes: 7 additions & 33 deletions packages/app/src/ChartUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { add, differenceInSeconds } from 'date-fns';
import { omit } from 'lodash';
import SqlString from 'sqlstring';
import { z } from 'zod';
import {
Expand All @@ -15,14 +14,15 @@ import {
convertDateRangeToGranularityString,
convertGranularityToSeconds,
convertToCategoricalChartConfig,
convertToNumberChartConfig,
convertToTableChartConfig,
getAlignedDateRange,
Granularity,
} from '@hyperdx/common-utils/dist/core/utils';
import { isBuilderChartConfig } from '@hyperdx/common-utils/dist/guards';
import {
AggregateFunction as AggFnV2,
BuilderChartConfigWithDateRange,
BuilderChartConfigWithOptTimestamp,
BuilderSavedChartConfig,
ChartConfigWithDateRange,
ChartConfigWithOptDateRange,
Expand Down Expand Up @@ -1140,37 +1140,11 @@ export function buildTableRowSearchUrl({
});
}

export function convertToNumberChartConfig(
config: BuilderChartConfigWithDateRange,
): BuilderChartConfigWithOptTimestamp {
return omit(config, ['granularity', 'groupBy']);
}

// The pie/bar seriesLimit→LIMIT translation lives in common-utils
export { convertToCategoricalChartConfig };

export function convertToTableChartConfig(
config: BuilderChartConfigWithOptTimestamp,
): BuilderChartConfigWithOptTimestamp {
const convertedConfig = structuredClone(omit(config, ['granularity']));

// Set a default limit if not already set
if (!convertedConfig.limit) {
convertedConfig.limit = { limit: 200 };
}

// Set a default orderBy if groupBy is set but orderBy is not,
// so that the set of rows within the limit is stable.
if (
convertedConfig.groupBy &&
typeof convertedConfig.groupBy === 'string' &&
!convertedConfig.orderBy
) {
convertedConfig.orderBy = convertedConfig.groupBy;
}

return convertedConfig;
}
export {
convertToCategoricalChartConfig,
convertToNumberChartConfig,
convertToTableChartConfig,
};

export function buildMVDateRangeIndicator({
mvOptimizationData,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/ClickhousePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ function ClickhousePage() {
<ReactCodeMirror
extensions={[clickhouseSql()]}
editable={false}
value={formatSql(row.query)}
value={formatSql(String(row.query ?? ''))}
theme={colorScheme === 'dark' ? 'dark' : 'light'}
lang="sql"
maxHeight="200px"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { ChartActionBar } from './ChartActionBar';
import { ChartEditorControls } from './ChartEditorControls';
import { ChartPreviewPanel } from './ChartPreviewPanel';
import { ErrorNotificationMessage } from './ErrorNotificationMessage';
import { useBuilderToSqlConversion } from './useBuilderToSqlConversion';
import {
buildChartConfigForExplanations,
computeDbTimeChartConfig,
Expand Down Expand Up @@ -156,6 +157,10 @@ export default function EditTimeChartForm({
control,
setValue,
getValues,
// The callback form of `watch` is used to subscribe to field changes
// (without re-rendering) in useBuilderToSqlConversion; useWatch can't do this.
// eslint-disable-next-line react-hook-form/no-use-watch
watch,
handleSubmit,
register,
setError,
Expand Down Expand Up @@ -229,6 +234,15 @@ 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({
control,
getValues,
setValue,
watch,
tableSource,
});

const activeTab = displayTypeToActiveTab(displayType);

// When switching display types, remove the alert if the new display type doesn't support alerts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jest.mock('@/hooks/useFetchMetricMetadata', () => ({
}));

jest.mock('@/hooks/useMetadata', () => ({
useMetadataWithSettings: jest.fn().mockReturnValue({}),
useGetKeyValues: jest.fn().mockReturnValue({
data: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { classifyFormEdit } from '@/components/DBEditTimeChartForm/useBuilderToSqlConversion';

describe('classifyFormEdit', () => {
it('classifies a SQL editor change in SQL mode as a SQL edit', () => {
expect(
classifyFormEdit({
name: 'sqlTemplate',
type: 'change',
configType: 'sql',
}),
).toBe('sql');
});

it('classifies a builder query change in builder mode as a builder edit', () => {
expect(
classifyFormEdit({
name: 'series.0.aggFn',
type: 'change',
configType: 'builder',
}),
).toBe('builder');
expect(
classifyFormEdit({
name: 'where',
type: 'change',
configType: 'builder',
}),
).toBe('builder');
expect(
classifyFormEdit({
name: 'source',
type: 'change',
configType: 'builder',
}),
).toBe('builder');
});

it('ignores programmatic changes (undefined type)', () => {
// Mode-switch field resets and our own generated-SQL write arrive without a
// 'change' type, so they must never be counted as user edits.
expect(
classifyFormEdit({
name: 'sqlTemplate',
type: undefined,
configType: 'sql',
}),
).toBeNull();
expect(
classifyFormEdit({
name: 'select',
type: undefined,
configType: 'builder',
}),
).toBeNull();
});

it('ignores mode-agnostic fields (chart name, configType toggle)', () => {
expect(
classifyFormEdit({ name: 'name', type: 'change', configType: 'builder' }),
).toBeNull();
expect(
classifyFormEdit({ name: 'name', type: 'change', configType: 'sql' }),
).toBeNull();
expect(
classifyFormEdit({
name: 'configType',
type: 'change',
configType: 'sql',
}),
).toBeNull();
});

it('does not count a builder field change while in SQL mode', () => {
// e.g. the source select is present in both modes; changing it in SQL mode
// is not a builder edit.
expect(
classifyFormEdit({ name: 'source', type: 'change', configType: 'sql' }),
).toBeNull();
});

it('does not count a SQL editor change while in builder mode', () => {
expect(
classifyFormEdit({
name: 'sqlTemplate',
type: 'change',
configType: 'builder',
}),
).toBeNull();
});

it('ignores changes with no field name', () => {
expect(
classifyFormEdit({ name: undefined, type: 'change', configType: 'sql' }),
).toBeNull();
});
});
Loading
Loading