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
7 changes: 7 additions & 0 deletions .changeset/many-mails-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/api": patch
"@hyperdx/app": patch
---

feat: Support raw sql number charts and pie charts
17 changes: 13 additions & 4 deletions packages/app/src/ChartUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,13 +540,22 @@ export function formatResponseForPieChart(
data: ResponseJSON<Record<string, unknown>>,
getColor: (index: number, label: string) => string,
): Array<{ label: string; value: number; color: string }> {
if (!data.meta || data.data.length === 0) return [];
if (data.meta == null) {
throw new Error('No meta data found in response');
}

const valueColumns = inferValueColumns(data.meta, new Set());
const groupByColumns = inferGroupColumns(data.meta);
if (!valueColumns?.length) return [];
if (data.data.length === 0) return [];

const valueColumns = inferValueColumns(data.meta, new Set()) ?? [];
if (valueColumns.length === 0) {
throw new Error(
`No value columns found in result column metadata. Make sure a numeric column exists in the result set.\n\nResult column metadata: ${JSON.stringify(data.meta)}`,
);
}
const valueColumn = valueColumns[0].name;

const groupByColumns = inferGroupColumns(data.meta);

return (
data.data
.map(row => {
Expand Down
34 changes: 16 additions & 18 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,24 +420,22 @@ const Tile = forwardRef(
/>
</Box>
)}
{queriedConfig?.displayType === DisplayType.Number &&
isBuilderChartConfig(queriedConfig) && (
<DBNumberChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={toolbar}
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Pie &&
isBuilderChartConfig(queriedConfig) && (
<DBPieChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={toolbar}
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Number && (
<DBNumberChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={toolbar}
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Pie && (
<DBPieChart
key={`${keyPrefix}-${chart.id}`}
title={title}
toolbarPrefix={toolbar}
config={queriedConfig}
/>
)}
{effectiveMarkdownConfig?.displayType === DisplayType.Markdown &&
'markdown' in effectiveMarkdownConfig && (
<HDXMarkdownChart
Expand Down
28 changes: 20 additions & 8 deletions packages/app/src/__tests__/ChartUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,15 +862,27 @@ describe('ChartUtils', () => {
expect(result).toEqual([]);
});

it('returns empty array when there are no numeric value columns', () => {
const result = formatResponseForPieChart(
{
data: [{ ServiceName: 'checkout' }],
meta: [{ name: 'ServiceName', type: 'LowCardinality(String)' }],
},
getColor,
it('throws when meta is missing', () => {
expect(() =>
formatResponseForPieChart(
{ data: [{ 'count()': 10 }] } as any,
getColor,
),
).toThrow('No meta data found in response');
});

it('throws when there are no numeric value columns', () => {
expect(() =>
formatResponseForPieChart(
{
data: [{ ServiceName: 'checkout' }],
meta: [{ name: 'ServiceName', type: 'LowCardinality(String)' }],
},
getColor,
),
).toThrow(
'No value columns found in result column metadata. Make sure a numeric column exists in the result set.\n\nResult column metadata: [{"name":"ServiceName","type":"LowCardinality(String)"}]',
);
expect(result).toEqual([]);
});

it('uses the value column name as label when there are no group-by columns', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
IconCopy,
} from '@tabler/icons-react';

import { DISPLAY_TYPE_INSTRUCTIONS } from './constants';

const helpOpenedAtom = atom(true);

function ParamSnippet({
Expand Down Expand Up @@ -89,49 +91,7 @@ export function RawSqlChartInstructions({
</Group>
<Collapse in={helpOpened}>
<Stack gap={6} pl="xs" pt="md">
{(displayType === DisplayType.Line ||
displayType === DisplayType.StackedBar) && (
<>
<Text size="xs" fw="bold">
Result columns are plotted as follows:
</Text>
<List size="xs" withPadding spacing={3} mb="xs">
<List.Item>
<Text span size="xs" fw={600}>
Timestamp
</Text>
<Text span size="xs">
{' '}
— The first <Code fz="xs">Date</Code> or{' '}
<Code fz="xs">DateTime</Code> column.
</Text>
</List.Item>
<List.Item>
<Text span size="xs" fw={600}>
Series Value
</Text>
<Text span size="xs">
{' '}
— Each numeric column will be plotted as a separate
series. These columns are generally aggregate function
values.
</Text>
</List.Item>
<List.Item>
<Text span size="xs" fw={600}>
Group Names
</Text>
<Text span size="xs">
{' '}
(optional) — Any string, map, or array type result column
will be treated as a group column. Result rows with
different group column values will be plotted as separate
series.
</Text>
</List.Item>
</List>
</>
)}
{DISPLAY_TYPE_INSTRUCTIONS[displayType]}

<Text size="xs" fw="bold">
The following parameters can be referenced in this chart's SQL:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('convertFormStateToSavedChartConfig', () => {
it('returns undefined for sql config with an unsupported displayType', () => {
const form: ChartEditorFormState = {
configType: 'sql',
displayType: DisplayType.Pie,
displayType: DisplayType.Markdown,
sqlTemplate: 'SELECT 1',
connection: 'conn-1',
series: [],
Expand Down
31 changes: 0 additions & 31 deletions packages/app/src/components/ChartEditor/constants.ts

This file was deleted.

140 changes: 140 additions & 0 deletions packages/app/src/components/ChartEditor/constants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ReactNode } from 'react';
import { DisplayType } from '@hyperdx/common-utils/dist/types';
import { Code, List, Text } from '@mantine/core';

const TIMESERIES_PLACEHOLDER_SQL = `SELECT
toStartOfInterval(TimestampTime, INTERVAL {intervalSeconds:Int64} SECOND) AS ts,
SeverityText,
count() AS count
FROM
default.otel_logs
WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64})
AND TimestampTime < fromUnixTimestamp64Milli({endDateMilliseconds:Int64})
GROUP BY ts, SeverityText
ORDER BY ts ASC;`;

export const SQL_PLACEHOLDERS: Record<DisplayType, string> = {
[DisplayType.Line]: TIMESERIES_PLACEHOLDER_SQL,
[DisplayType.StackedBar]: TIMESERIES_PLACEHOLDER_SQL,
[DisplayType.Table]: `SELECT
count()
FROM
default.otel_logs
WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64})
AND TimestampTime <= fromUnixTimestamp64Milli({endDateMilliseconds:Int64})
LIMIT
200
`,
[DisplayType.Pie]: `SELECT
ServiceName,
count()
FROM
default.otel_logs
WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64})
AND TimestampTime < fromUnixTimestamp64Milli({endDateMilliseconds:Int64})
GROUP BY ServiceName;`,
[DisplayType.Number]: `SELECT
count()
FROM
default.otel_logs
WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64})
AND TimestampTime < fromUnixTimestamp64Milli({endDateMilliseconds:Int64});`,
[DisplayType.Search]: '',
[DisplayType.Heatmap]: '',
[DisplayType.Markdown]: '',
};

const TIMESERIES_INSTRUCTIONS = (
<>
<Text size="xs" fw="bold">
Result columns are plotted as follows:
</Text>
<List size="xs" withPadding spacing={3} mb="xs">
<List.Item>
<Text span size="xs" fw={600}>
Timestamp
</Text>
<Text span size="xs">
{' '}
— The first <Code fz="xs">Date</Code> or <Code fz="xs">DateTime</Code>{' '}
column.
</Text>
</List.Item>
<List.Item>
<Text span size="xs" fw={600}>
Series Value
</Text>
<Text span size="xs">
{' '}
— Each numeric column will be plotted as a separate series. These
columns are generally aggregate function values.
</Text>
</List.Item>
<List.Item>
<Text span size="xs" fw={600}>
Group Names
</Text>
<Text span size="xs">
{' '}
(optional) — Any string, map, or array type result column will be
treated as a group column. Result rows with different group column
values will be plotted as separate series.
</Text>
</List.Item>
</List>
</>
);

export const DISPLAY_TYPE_INSTRUCTIONS: Partial<
Record<DisplayType, ReactNode>
> = {
[DisplayType.Line]: TIMESERIES_INSTRUCTIONS,
[DisplayType.StackedBar]: TIMESERIES_INSTRUCTIONS,
[DisplayType.Pie]: (
<>
<Text size="xs" fw="bold">
Result columns are plotted as follows:
</Text>
<List size="xs" withPadding spacing={3} mb="xs">
<List.Item>
<Text span size="xs" fw={600}>
Slice Value
</Text>
<Text span size="xs">
{' '}
— The first numeric column determines each slice&apos;s size.
</Text>
</List.Item>
<List.Item>
<Text span size="xs" fw={600}>
Slice Label
</Text>
<Text span size="xs">
{' '}
(optional) — Each unique value of each string, map, and array type
columns will be used as a slice label.
</Text>
</List.Item>
</List>
</>
),
[DisplayType.Number]: (
<>
<Text size="xs" fw="bold">
Result columns are displayed as follows:
</Text>
<List size="xs" withPadding spacing={3} mb="xs">
<List.Item>
<Text span size="xs" fw={600}>
Number
</Text>
<Text span size="xs">
{' '}
— The value of the first numeric column in the first result row is
displayed as the number.
</Text>
</List.Item>
</List>
</>
),
};
8 changes: 6 additions & 2 deletions packages/app/src/components/ChartEditor/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ export const isRawSqlDisplayType = (
): displayType is
| DisplayType.Table
| DisplayType.Line
| DisplayType.StackedBar =>
| DisplayType.StackedBar
| DisplayType.Pie
| DisplayType.Number =>
displayType === DisplayType.Table ||
displayType === DisplayType.Line ||
displayType === DisplayType.StackedBar;
displayType === DisplayType.StackedBar ||
displayType === DisplayType.Pie ||
displayType === DisplayType.Number;

export function convertFormStateToSavedChartConfig(
form: ChartEditorFormState,
Expand Down
Loading
Loading