diff --git a/.changeset/many-mails-shop.md b/.changeset/many-mails-shop.md new file mode 100644 index 0000000000..af6d74c75a --- /dev/null +++ b/.changeset/many-mails-shop.md @@ -0,0 +1,7 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/api": patch +"@hyperdx/app": patch +--- + +feat: Support raw sql number charts and pie charts diff --git a/packages/app/src/ChartUtils.tsx b/packages/app/src/ChartUtils.tsx index 1cfd6f0326..39220a93e9 100644 --- a/packages/app/src/ChartUtils.tsx +++ b/packages/app/src/ChartUtils.tsx @@ -540,13 +540,22 @@ export function formatResponseForPieChart( data: ResponseJSON>, 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 => { diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx index cc13344cd2..e0291dd2d4 100644 --- a/packages/app/src/DBDashboardPage.tsx +++ b/packages/app/src/DBDashboardPage.tsx @@ -420,24 +420,22 @@ const Tile = forwardRef( /> )} - {queriedConfig?.displayType === DisplayType.Number && - isBuilderChartConfig(queriedConfig) && ( - - )} - {queriedConfig?.displayType === DisplayType.Pie && - isBuilderChartConfig(queriedConfig) && ( - - )} + {queriedConfig?.displayType === DisplayType.Number && ( + + )} + {queriedConfig?.displayType === DisplayType.Pie && ( + + )} {effectiveMarkdownConfig?.displayType === DisplayType.Markdown && 'markdown' in effectiveMarkdownConfig && ( { 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', () => { diff --git a/packages/app/src/components/ChartEditor/RawSqlChartInstructions.tsx b/packages/app/src/components/ChartEditor/RawSqlChartInstructions.tsx index b0965bb4ca..4e8eccbde9 100644 --- a/packages/app/src/components/ChartEditor/RawSqlChartInstructions.tsx +++ b/packages/app/src/components/ChartEditor/RawSqlChartInstructions.tsx @@ -23,6 +23,8 @@ import { IconCopy, } from '@tabler/icons-react'; +import { DISPLAY_TYPE_INSTRUCTIONS } from './constants'; + const helpOpenedAtom = atom(true); function ParamSnippet({ @@ -89,49 +91,7 @@ export function RawSqlChartInstructions({ - {(displayType === DisplayType.Line || - displayType === DisplayType.StackedBar) && ( - <> - - Result columns are plotted as follows: - - - - - Timestamp - - - {' '} - — The first Date or{' '} - DateTime column. - - - - - Series Value - - - {' '} - — Each numeric column will be plotted as a separate - series. These columns are generally aggregate function - values. - - - - - Group Names - - - {' '} - (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. - - - - - )} + {DISPLAY_TYPE_INSTRUCTIONS[displayType]} The following parameters can be referenced in this chart's SQL: diff --git a/packages/app/src/components/ChartEditor/__tests__/utils.test.ts b/packages/app/src/components/ChartEditor/__tests__/utils.test.ts index e0efc94b22..5b5efbf8ad 100644 --- a/packages/app/src/components/ChartEditor/__tests__/utils.test.ts +++ b/packages/app/src/components/ChartEditor/__tests__/utils.test.ts @@ -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: [], diff --git a/packages/app/src/components/ChartEditor/constants.ts b/packages/app/src/components/ChartEditor/constants.ts deleted file mode 100644 index b04815ea1b..0000000000 --- a/packages/app/src/components/ChartEditor/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DisplayType } from '@hyperdx/common-utils/dist/types'; - -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.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]: '', - [DisplayType.Number]: '', - [DisplayType.Search]: '', - [DisplayType.Heatmap]: '', - [DisplayType.Markdown]: '', -}; diff --git a/packages/app/src/components/ChartEditor/constants.tsx b/packages/app/src/components/ChartEditor/constants.tsx new file mode 100644 index 0000000000..ccfbfe90e5 --- /dev/null +++ b/packages/app/src/components/ChartEditor/constants.tsx @@ -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.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 = ( + <> + + Result columns are plotted as follows: + + + + + Timestamp + + + {' '} + — The first Date or DateTime{' '} + column. + + + + + Series Value + + + {' '} + — Each numeric column will be plotted as a separate series. These + columns are generally aggregate function values. + + + + + Group Names + + + {' '} + (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. + + + + +); + +export const DISPLAY_TYPE_INSTRUCTIONS: Partial< + Record +> = { + [DisplayType.Line]: TIMESERIES_INSTRUCTIONS, + [DisplayType.StackedBar]: TIMESERIES_INSTRUCTIONS, + [DisplayType.Pie]: ( + <> + + Result columns are plotted as follows: + + + + + Slice Value + + + {' '} + — The first numeric column determines each slice's size. + + + + + Slice Label + + + {' '} + (optional) — Each unique value of each string, map, and array type + columns will be used as a slice label. + + + + + ), + [DisplayType.Number]: ( + <> + + Result columns are displayed as follows: + + + + + Number + + + {' '} + — The value of the first numeric column in the first result row is + displayed as the number. + + + + + ), +}; diff --git a/packages/app/src/components/ChartEditor/utils.ts b/packages/app/src/components/ChartEditor/utils.ts index 364e4ae566..9cdab66916 100644 --- a/packages/app/src/components/ChartEditor/utils.ts +++ b/packages/app/src/components/ChartEditor/utils.ts @@ -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, diff --git a/packages/app/src/components/DBEditTimeChartForm.tsx b/packages/app/src/components/DBEditTimeChartForm.tsx index 6526e5a502..417556704d 100644 --- a/packages/app/src/components/DBEditTimeChartForm.tsx +++ b/packages/app/src/components/DBEditTimeChartForm.tsx @@ -1571,38 +1571,29 @@ export default function EditTimeChartForm({ thresholdType: alert.thresholdType, }) } + errorVariant="inline" showMVOptimizationIndicator={false} /> )} - {queryReady && - queriedConfig != null && - isBuilderChartConfig(queriedConfig) && - activeTab === 'pie' && ( -
- -
- )} - {queryReady && - queriedConfig != null && - isBuilderChartConfig(queriedConfig) && - activeTab === 'number' && ( -
- -
- )} + {queryReady && queriedConfig != null && activeTab === 'pie' && ( +
+ +
+ )} + {queryReady && queriedConfig != null && activeTab === 'number' && ( +
+ +
+ )} {queryReady && tableSource && queriedConfig != null && diff --git a/packages/app/src/components/DBNumberChart.tsx b/packages/app/src/components/DBNumberChart.tsx index b179deb71c..5dfb9a8199 100644 --- a/packages/app/src/components/DBNumberChart.tsx +++ b/packages/app/src/components/DBNumberChart.tsx @@ -1,7 +1,17 @@ import { useMemo } from 'react'; -import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; -import { BuilderChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; -import { Box, Code, Flex, Text } from '@mantine/core'; +import { + filterColumnMetaByType, + JSDataType, +} from '@hyperdx/common-utils/dist/clickhouse'; +import { + isBuilderChartConfig, + isRawSqlChartConfig, +} from '@hyperdx/common-utils/dist/guards'; +import { + BuilderChartConfigWithDateRange, + RawSqlConfigWithDateRange, +} from '@hyperdx/common-utils/dist/types'; +import { Flex, Text } from '@mantine/core'; import { buildMVDateRangeIndicator, @@ -13,8 +23,10 @@ import { useSource } from '@/source'; import { formatNumber } from '@/utils'; import ChartContainer from './charts/ChartContainer'; +import ChartErrorState, { + ChartErrorStateVariant, +} from './charts/ChartErrorState'; import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; -import { SQLPreview } from './ChartSQLPreview'; export default function DBNumberChart({ config, @@ -24,22 +36,30 @@ export default function DBNumberChart({ toolbarPrefix, toolbarSuffix, showMVOptimizationIndicator = true, + errorVariant, }: { - config: BuilderChartConfigWithDateRange; + config: BuilderChartConfigWithDateRange | RawSqlConfigWithDateRange; queryKeyPrefix?: string; enabled?: boolean; title?: React.ReactNode; toolbarPrefix?: React.ReactNode[]; toolbarSuffix?: React.ReactNode[]; showMVOptimizationIndicator?: boolean; + errorVariant?: ChartErrorStateVariant; }) { const queriedConfig = useMemo( - () => convertToNumberChartConfig(config), + () => + isBuilderChartConfig(config) + ? convertToNumberChartConfig(config) + : config, [config], ); + const builderQueriedConfig = isBuilderChartConfig(queriedConfig) + ? queriedConfig + : undefined; const { data: mvOptimizationData } = - useMVOptimizationExplanation(queriedConfig); + useMVOptimizationExplanation(builderQueriedConfig); const { data, isLoading, isError, error } = useQueriedChartConfig( queriedConfig, @@ -50,12 +70,25 @@ export default function DBNumberChart({ }, ); - const number = formatNumber( - (Object.values(data?.data?.[0] ?? {})?.[0] ?? Number.NaN) as number, - config.numberFormat, - ); + // The value is the first numeric value in the first row of the result + const valueColumn = data?.meta + ? filterColumnMetaByType(data?.meta, [JSDataType.Number])?.[0] + : undefined; + const resultError = + data && !valueColumn && isRawSqlChartConfig(queriedConfig) + ? new Error( + `No numeric columns found in result column metadata. Make sure a numeric column exists in the result set.\n\nResult Metadata: ${JSON.stringify(data.meta)}`, + ) + : error; + + const value = valueColumn + ? data?.data?.[0]?.[valueColumn.name] + : (Object.values(data?.data?.[0] ?? {})?.[0] ?? Number.NaN); + const formattedValue = formatNumber(value as number, config.numberFormat); - const { data: source } = useSource({ id: config.source }); + const { data: source } = useSource({ + id: isBuilderChartConfig(config) ? config.source : undefined, + }); const toolbarItemsMemo = useMemo(() => { const allToolbarItems = []; @@ -64,11 +97,11 @@ export default function DBNumberChart({ allToolbarItems.push(...toolbarPrefix); } - if (source && showMVOptimizationIndicator) { + if (source && showMVOptimizationIndicator && builderQueriedConfig) { allToolbarItems.push( , @@ -96,6 +129,7 @@ export default function DBNumberChart({ showMVOptimizationIndicator, mvOptimizationData, queriedConfig, + builderQueriedConfig, ]); return ( @@ -105,39 +139,16 @@ export default function DBNumberChart({ Loading Chart Data... ) : isError ? ( -
- - Error loading chart, please check your query or try again later. - - - - Error Message: - - - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> - - Sent Query: - - - - )} - -
+ + ) : resultError ? ( + ) : data?.data.length === 0 ? (
No data found within time range.
) : ( - {number ?? 'N/A'} + {formattedValue ?? 'N/A'} )} diff --git a/packages/app/src/components/DBPieChart.tsx b/packages/app/src/components/DBPieChart.tsx index 228320208c..b955f916a6 100644 --- a/packages/app/src/components/DBPieChart.tsx +++ b/packages/app/src/components/DBPieChart.tsx @@ -1,8 +1,11 @@ import { memo, useMemo } from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts'; -import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; -import { BuilderChartConfigWithOptTimestamp } from '@hyperdx/common-utils/dist/types'; -import { Box, Code, Flex, Text } from '@mantine/core'; +import { isBuilderChartConfig } from '@hyperdx/common-utils/dist/guards'; +import { + BuilderChartConfigWithOptTimestamp, + RawSqlConfigWithDateRange, +} from '@hyperdx/common-utils/dist/types'; +import { Flex } from '@mantine/core'; import { buildMVDateRangeIndicator, @@ -16,9 +19,11 @@ import type { NumberFormat } from '@/types'; import { getColorProps } from '@/utils'; import ChartContainer from './charts/ChartContainer'; +import ChartErrorState, { + ChartErrorStateVariant, +} from './charts/ChartErrorState'; import { ChartTooltipContainer, ChartTooltipItem } from './charts/ChartTooltip'; import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; -import { SQLPreview } from './ChartSQLPreview'; const PieChartTooltip = memo( ({ @@ -54,23 +59,32 @@ export const DBPieChart = ({ showMVOptimizationIndicator = true, toolbarPrefix, toolbarSuffix, + errorVariant, }: { - config: BuilderChartConfigWithOptTimestamp; + config: BuilderChartConfigWithOptTimestamp | RawSqlConfigWithDateRange; title?: React.ReactNode; enabled?: boolean; queryKeyPrefix?: string; showMVOptimizationIndicator?: boolean; toolbarPrefix?: React.ReactNode[]; toolbarSuffix?: React.ReactNode[]; + errorVariant?: ChartErrorStateVariant; }) => { - const { data: source } = useSource({ id: config.source }); + const { data: source } = useSource({ + id: isBuilderChartConfig(config) ? config.source : undefined, + }); const queriedConfig = useMemo(() => { - return convertToPieChartConfig(config); + return isBuilderChartConfig(config) + ? convertToPieChartConfig(config) + : config; }, [config]); + const builderQueriedConfig = isBuilderChartConfig(queriedConfig) + ? queriedConfig + : undefined; const { data: mvOptimizationData } = - useMVOptimizationExplanation(queriedConfig); + useMVOptimizationExplanation(builderQueriedConfig); const { data, isLoading, isError, error } = useQueriedChartConfig( queriedConfig, @@ -88,11 +102,11 @@ export const DBPieChart = ({ allToolbarItems.push(...toolbarPrefix); } - if (source && showMVOptimizationIndicator) { + if (source && showMVOptimizationIndicator && builderQueriedConfig) { allToolbarItems.push( , @@ -120,11 +134,16 @@ export const DBPieChart = ({ showMVOptimizationIndicator, mvOptimizationData, queriedConfig, + builderQueriedConfig, ]); - const pieChartData = useMemo(() => { - if (!data) return []; - return formatResponseForPieChart(data, getColorProps); + const [pieChartData, responseFormatError] = useMemo(() => { + if (!data) return [[], null]; + try { + return [formatResponseForPieChart(data, getColorProps), null]; + } catch (error) { + return [[], error instanceof Error ? error : new Error(String(error))]; + } }, [data]); return ( @@ -134,32 +153,9 @@ export const DBPieChart = ({ Loading Chart Data... ) : isError && error ? ( -
- - Error loading chart, please check your query or try again later. - - - - Error Message: - - - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> - - Sent Query: - - - - )} - -
+ + ) : responseFormatError ? ( + ) : data?.data.length === 0 ? (
No data found within time range. diff --git a/packages/app/src/components/DBTimeChart.tsx b/packages/app/src/components/DBTimeChart.tsx index b7c3caad2a..5ab15fcb2d 100644 --- a/packages/app/src/components/DBTimeChart.tsx +++ b/packages/app/src/components/DBTimeChart.tsx @@ -1,7 +1,6 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { add, differenceInSeconds } from 'date-fns'; -import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; import { convertGranularityToSeconds, getAlignedDateRange, @@ -16,24 +15,15 @@ import { DisplayType, } from '@hyperdx/common-utils/dist/types'; import { - Button, - Code, Divider, Group, - Modal, Popover, Portal, Stack, Text, Tooltip, } from '@mantine/core'; -import { useDisclosure } from '@mantine/hooks'; -import { - IconArrowsDiagonal, - IconChartBar, - IconChartLine, - IconSearch, -} from '@tabler/icons-react'; +import { IconChartBar, IconChartLine, IconSearch } from '@tabler/icons-react'; import api from '@/api'; import { @@ -53,10 +43,12 @@ import { useMVOptimizationExplanation } from '@/hooks/useMVOptimizationExplanati import { useSource } from '@/source'; import ChartContainer from './charts/ChartContainer'; +import ChartErrorState, { + ChartErrorStateVariant, +} from './charts/ChartErrorState'; import DateRangeIndicator from './charts/DateRangeIndicator'; import DisplaySwitcher from './charts/DisplaySwitcher'; import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; -import { SQLPreview } from './ChartSQLPreview'; type ActiveClickPayload = { x: number; @@ -205,57 +197,6 @@ function ActiveTimeTooltip({ ); } -function ErrorView({ error }: { error: Error | ClickHouseQueryError }) { - const [isErrorExpanded, errorExpansion] = useDisclosure(false); - - return ( -
- - Error loading chart, please check your query or try again later. - - - errorExpansion.close()} - title="Error Details" - size="lg" - > - - - Error Message: - - - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> - - Sent Query: - - - - )} - - -
- ); -} - type DBTimeChartComponentProps = { config: ChartConfigWithDateRange; disableQueryChunking?: boolean; @@ -278,6 +219,7 @@ type DBTimeChartComponentProps = { toolbarSuffix?: React.ReactNode[]; showMVOptimizationIndicator?: boolean; showDateRangeIndicator?: boolean; + errorVariant?: ChartErrorStateVariant; }; function DBTimeChartComponent({ @@ -300,6 +242,7 @@ function DBTimeChartComponent({ toolbarSuffix, showMVOptimizationIndicator = true, showDateRangeIndicator = true, + errorVariant, }: DBTimeChartComponentProps) { const [selectedSeriesSet, setSelectedSeriesSet] = useState>( new Set(), @@ -750,9 +693,10 @@ function DBTimeChartComponent({ Loading Chart Data...
) : isError ? ( - + ) : resultFormattingError ? ( - { jest.clearAllMocks(); mockUseQueriedChartConfig.mockReturnValue({ - data: { data: [{ test1: 1234, test2: 5678 }] }, + data: { + data: [{ test1: 1234, test2: 5678 }], + meta: [ + { name: 'test1', type: 'UInt64' }, + { name: 'test2', type: 'UInt64' }, + ], + }, isLoading: false, isError: false, }); @@ -77,7 +83,9 @@ describe('DBPieChart', () => { }); it('should render pie chart correctly', () => { - renderWithMantine(); + renderWithMantine( + , + ); expect(screen.getByTestId('pie-chart-container')).toBeInTheDocument(); }); diff --git a/packages/app/src/components/charts/ChartErrorState.tsx b/packages/app/src/components/charts/ChartErrorState.tsx new file mode 100644 index 0000000000..d233932f06 --- /dev/null +++ b/packages/app/src/components/charts/ChartErrorState.tsx @@ -0,0 +1,79 @@ +import { useMemo } from 'react'; +import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; +import { Button, Code, Group, Modal, Stack, Text } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { IconArrowsDiagonal } from '@tabler/icons-react'; + +import { SQLPreview } from '../ChartSQLPreview'; + +export type ChartErrorStateVariant = 'collapsible' | 'inline'; + +export default function ChartErrorState({ + error, + variant = 'collapsible', +}: { + error: Error | ClickHouseQueryError; + variant?: ChartErrorStateVariant; +}) { + const [isErrorExpanded, errorExpansion] = useDisclosure(false); + + const details = useMemo(() => { + return ( + + + Error Message: + + + {error.message} + + {error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} + + ); + }, [error]); + + return ( +
+ + Error loading chart, please check your query or try again later. + + + {variant === 'collapsible' ? ( + <> + + errorExpansion.close()} + title="Error Details" + size="lg" + > + {details} + + + ) : ( + details + )} +
+ ); +} diff --git a/packages/common-utils/src/rawSqlParams.ts b/packages/common-utils/src/rawSqlParams.ts index a55082d807..c92630ea6a 100644 --- a/packages/common-utils/src/rawSqlParams.ts +++ b/packages/common-utils/src/rawSqlParams.ts @@ -74,8 +74,14 @@ export const QUERY_PARAMS_BY_DISPLAY_TYPE: Record< QUERY_PARAMS.startDateMilliseconds, QUERY_PARAMS.endDateMilliseconds, ], - [DisplayType.Pie]: [], - [DisplayType.Number]: [], + [DisplayType.Pie]: [ + QUERY_PARAMS.startDateMilliseconds, + QUERY_PARAMS.endDateMilliseconds, + ], + [DisplayType.Number]: [ + QUERY_PARAMS.startDateMilliseconds, + QUERY_PARAMS.endDateMilliseconds, + ], [DisplayType.Search]: [], [DisplayType.Heatmap]: [], [DisplayType.Markdown]: [], @@ -90,13 +96,15 @@ WHERE TimestampTime >= fromUnixTimestamp64Milli ({startDateMilliseconds:Int64}) AND TimestampTime < fromUnixTimestamp64Milli ({endDateMilliseconds:Int64}) GROUP BY ServiceName, ts`; +export const DATE_RANGE_WHERE_EXAMPLE_SQL = `WHERE TimestampTime >= fromUnixTimestamp64Milli ({startDateMilliseconds:Int64}) + AND TimestampTime <= fromUnixTimestamp64Milli ({endDateMilliseconds:Int64})`; + export const QUERY_PARAM_EXAMPLES: Record = { [DisplayType.Line]: TIME_CHART_EXAMPLE_SQL, [DisplayType.StackedBar]: TIME_CHART_EXAMPLE_SQL, - [DisplayType.Table]: `WHERE Timestamp >= fromUnixTimestamp64Milli ({startDateMilliseconds:Int64}) - AND Timestamp <= fromUnixTimestamp64Milli ({endDateMilliseconds:Int64})`, - [DisplayType.Pie]: '', - [DisplayType.Number]: '', + [DisplayType.Table]: DATE_RANGE_WHERE_EXAMPLE_SQL, + [DisplayType.Pie]: DATE_RANGE_WHERE_EXAMPLE_SQL, + [DisplayType.Number]: DATE_RANGE_WHERE_EXAMPLE_SQL, [DisplayType.Search]: '', [DisplayType.Heatmap]: '', [DisplayType.Markdown]: '',