From 6a6d8d7749692e9ba24eadba179133591ad2caa6 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 30 Sep 2025 10:51:22 -0600 Subject: [PATCH 01/20] log table sorting - ui logic --- .../app/src/components/CsvExportButton.tsx | 6 +- packages/app/src/components/DBRowTable.tsx | 193 ++++++++++-------- .../app/src/components/ExpandableRowTable.tsx | 1 + 3 files changed, 113 insertions(+), 87 deletions(-) diff --git a/packages/app/src/components/CsvExportButton.tsx b/packages/app/src/components/CsvExportButton.tsx index f420893a1..82c1b3767 100644 --- a/packages/app/src/components/CsvExportButton.tsx +++ b/packages/app/src/components/CsvExportButton.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useCSVDownloader } from 'react-papaparse'; +import { UnstyledButton } from '@mantine/core'; interface CsvExportButtonProps { data: Record[]; @@ -57,9 +58,8 @@ export const CsvExportButton: React.FC = ({ } return ( -
= ({ > {children} -
+ ); }; diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index d29dd8b66..9aec3eb36 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -36,14 +36,21 @@ import { } from '@hyperdx/common-utils/dist/types'; import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/utils'; import { + ActionIcon, Box, Code, Flex, + Group, Modal, Text, Tooltip as MantineTooltip, UnstyledButton, } from '@mantine/core'; +import { + IconChevronDown, + IconChevronUp, + IconDotsVertical, +} from '@tabler/icons-react'; import { FetchNextPageOptions, useQuery } from '@tanstack/react-query'; import { ColumnDef, @@ -51,6 +58,7 @@ import { flexRender, getCoreRowModel, Row as TableRow, + SortingState, TableOptions, useReactTable, } from '@tanstack/react-table'; @@ -529,6 +537,8 @@ export const RawLogTable = memo( fetchMoreOnBottomReached(tableContainerRef.current); }, [fetchMoreOnBottomReached]); + const [sorting, setSorting] = useState([]); + const reactTableProps = useMemo((): TableOptions => { //TODO: fix any const onColumnSizingChange = (updaterOrValue: any) => { @@ -544,9 +554,17 @@ export const RawLogTable = memo( columns, getCoreRowModel: getCoreRowModel(), // debugTable: true, + enableSorting: true, + manualSorting: true, + onSortingChange: setSorting, + state: { + sorting, + }, + // onSortingChange: (helo: any) => { enableColumnResizing: true, + columnResizeMode: 'onChange' as ColumnResizeMode, - }; + } satisfies TableOptions; const columnSizeProps = { state: { @@ -701,9 +719,11 @@ export const RawLogTable = memo( {table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map((header, headerIndex) => { + const isLast = headerIndex === headerGroup.headers.length - 1; + return ( - {header.isPlaceholder ? null : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- )} - {header.column.getCanResize() && - headerIndex !== headerGroup.headers.length - 1 && ( -
- -
- )} - {headerIndex === headerGroup.headers.length - 1 && ( -
- {tableId != null && - Object.keys(columnSizeStorage).length > 0 && ( -
setColumnSizeStorage({})} - title="Reset Column Widths" - > - -
+ + {header.isPlaceholder ? null : ( + + {flexRender( + header.column.columnDef.header, + header.getContext(), )} - {config && ( - handleSqlModalOpen(true)} + + )} + + {header.column.getCanSort() && ( + - - - - + {header.column.getIsSorted() ? ( + + ) : ( + + )} + )} - setWrapLinesEnabled(prev => !prev)} - className="ms-2" - > - - - - - - - - - - - {onSettingsClick != null && ( + + {header.column.getCanResize() && !isLast && (
onSettingsClick()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={cx( + `resizer text-gray-600 cursor-col-resize`, + header.column.getIsResizing() && 'isResizing', + )} > - +
)} -
- )} + {isLast && ( + + {tableId && + Object.keys(columnSizeStorage).length > 0 && ( +
setColumnSizeStorage({})} + title="Reset Column Widths" + > + +
+ )} + {config && ( + handleSqlModalOpen(true)} + > + + + + + )} + + setWrapLinesEnabled(prev => !prev) + } + > + + + + + + + + + + + {onSettingsClick != null && ( +
onSettingsClick()} + > + +
+ )} +
+ )} + + ); })} @@ -1174,6 +1198,7 @@ function DBSqlRowTableComponent({ enabled: enabled && mergedConfig != null && getSelectLength(config.select) > 0, isLive, + // sort: config.orderBy, queryKeyPrefix, }); diff --git a/packages/app/src/components/ExpandableRowTable.tsx b/packages/app/src/components/ExpandableRowTable.tsx index 98cfda4d6..4f0468653 100644 --- a/packages/app/src/components/ExpandableRowTable.tsx +++ b/packages/app/src/components/ExpandableRowTable.tsx @@ -196,6 +196,7 @@ export const createExpandButtonColumn = ( }, size: 32, enableResizing: false, + enableSorting: false, meta: { className: 'text-center', }, From 7b57c18c4c34162273ecd6c78e426d1a723ed53e Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 30 Sep 2025 16:03:59 -0600 Subject: [PATCH 02/20] implement sorting logic (blanket solution, needs to be dialed down per instance) --- packages/app/src/components/DBRowTable.tsx | 45 +++++++++++++++++-- packages/common-utils/src/clickhouse/index.ts | 6 +-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 9aec3eb36..c9f3b0532 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -11,6 +11,12 @@ import { format, formatDistance } from 'date-fns'; import { isString } from 'lodash'; import curry from 'lodash/curry'; import ms from 'ms'; +import { + createParser, + parseAsString, + parseAsStringLiteral, + useQueryState, +} from 'nuqs'; import { useHotkeys } from 'react-hotkeys-hook'; import { Bar, @@ -20,6 +26,7 @@ import { XAxis, YAxis, } from 'recharts'; +import { z } from 'zod'; import { chSqlToAliasMap, ClickHouseQueryError, @@ -110,6 +117,27 @@ const ACCESSOR_MAP: Record = { const MAX_SCROLL_FETCH_LINES = 1000; const MAX_CELL_LENGTH = 500; +export const parseAsSort = createParser({ + parse(query) { + if (!query) return null; + const result = parseAsString.parse(query)?.split(' ') ?? []; + // pop the last element, then join the rest + const direction = result.pop() ?? 'ASC'; + const key = result.join(' '); + const desc = + parseAsStringLiteral(['ASC', 'DESC']).parse(direction) ?? 'ASC'; + if (!key) return null; + return { + id: key, + desc: desc === 'DESC', + }; + }, + serialize(value) { + if (!value) return ''; + return `${value.id} ${value.desc ? 'DESC' : 'ASC'}`; + }, +}); + const getRowId = (row: Record): string => row.__hyperdx_id; function retrieveColumnValue(column: string, row: Row): any { @@ -445,6 +473,7 @@ export const RawLogTable = memo( column, jsColumnType, }, + id: column, accessorFn: curry(retrieveColumnValue)(column), // Columns can contain '.' and will not work with accessorKey header: `${columnNameMap?.[column] ?? column}${isDate ? (isUTC ? ' (UTC)' : ' (Local)') : ''}`, cell: info => { @@ -537,7 +566,8 @@ export const RawLogTable = memo( fetchMoreOnBottomReached(tableContainerRef.current); }, [fetchMoreOnBottomReached]); - const [sorting, setSorting] = useState([]); + const [orderBy, setOrderBy] = useQueryState('orderBy', parseAsSort); + const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); const reactTableProps = useMemo((): TableOptions => { //TODO: fix any @@ -556,9 +586,16 @@ export const RawLogTable = memo( // debugTable: true, enableSorting: true, manualSorting: true, - onSortingChange: setSorting, + onSortingChange: v => { + if (typeof v === 'function') { + const newSortVal = v(orderByArray); + setOrderBy(newSortVal.at(0) ?? null); + } else { + setOrderBy(v.at(0) ?? null); + } + }, state: { - sorting, + sorting: orderByArray, }, // onSortingChange: (helo: any) => { enableColumnResizing: true, @@ -580,6 +617,8 @@ export const RawLogTable = memo( columns, dedupedRows, tableId, + orderByArray, + setOrderBy, columnSizeStorage, setColumnSizeStorage, ]); diff --git a/packages/common-utils/src/clickhouse/index.ts b/packages/common-utils/src/clickhouse/index.ts index b0bd88b9b..b039015f6 100644 --- a/packages/common-utils/src/clickhouse/index.ts +++ b/packages/common-utils/src/clickhouse/index.ts @@ -428,11 +428,11 @@ export abstract class BaseClickhouseClient { } // eslint-disable-next-line no-console - console.log('--------------------------------------------------------'); + console.debug('--------------------------------------------------------'); // eslint-disable-next-line no-console - console.log('Sending Query:', debugSql); + console.debug('Sending Query:', debugSql); // eslint-disable-next-line no-console - console.log('--------------------------------------------------------'); + console.debug('--------------------------------------------------------'); } protected processClickhouseSettings( From 2086e6fa3fd47791d14c795eaeee3e68a13347cf Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 30 Sep 2025 16:22:33 -0600 Subject: [PATCH 03/20] clean code --- packages/app/src/components/DBRowTable.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index c9f3b0532..1dc3b804b 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -597,9 +597,7 @@ export const RawLogTable = memo( state: { sorting: orderByArray, }, - // onSortingChange: (helo: any) => { enableColumnResizing: true, - columnResizeMode: 'onChange' as ColumnResizeMode, } satisfies TableOptions; From 569f3726b63ce6d3dcaa5e47c0350dbe4a4b5815 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 30 Sep 2025 16:26:54 -0600 Subject: [PATCH 04/20] fix bug where buttons show when loading --- packages/app/src/components/DBRowTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 1dc3b804b..e80a58409 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -812,7 +812,7 @@ export const RawLogTable = memo( )} - {isLast && ( + {!isLoading && isLast && ( {tableId && Object.keys(columnSizeStorage).length > 0 && ( From 943e2e8f4f2a0b93889a986d75d859449e7a53a7 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Wed, 1 Oct 2025 09:08:21 -0600 Subject: [PATCH 05/20] make core table not contain sorting logic, move to parent component --- packages/app/src/components/DBRowTable.tsx | 63 +++++++++++++++------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index e80a58409..0f2a8e893 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -7,7 +7,7 @@ import React, { useState, } from 'react'; import cx from 'classnames'; -import { format, formatDistance } from 'date-fns'; +import { formatDistance } from 'date-fns'; import { isString } from 'lodash'; import curry from 'lodash/curry'; import ms from 'ms'; @@ -26,7 +26,6 @@ import { XAxis, YAxis, } from 'recharts'; -import { z } from 'zod'; import { chSqlToAliasMap, ClickHouseQueryError, @@ -337,6 +336,9 @@ export const RawLogTable = memo( source, onExpandedRowsChange, collapseAllRows, + enableSorting = false, + onSortingChange, + sortOrder, showExpandButton = true, }: { wrapLines: boolean; @@ -374,6 +376,9 @@ export const RawLogTable = memo( collapseAllRows?: boolean; showExpandButton?: boolean; renderRowDetails?: (row: Record) => React.ReactNode; + enableSorting?: boolean; + sortOrder?: SortingState; + onSortingChange?: (v: SortingState | null) => void; }) => { const generateRowMatcher = generateRowId; @@ -566,9 +571,6 @@ export const RawLogTable = memo( fetchMoreOnBottomReached(tableContainerRef.current); }, [fetchMoreOnBottomReached]); - const [orderBy, setOrderBy] = useQueryState('orderBy', parseAsSort); - const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); - const reactTableProps = useMemo((): TableOptions => { //TODO: fix any const onColumnSizingChange = (updaterOrValue: any) => { @@ -584,22 +586,22 @@ export const RawLogTable = memo( columns, getCoreRowModel: getCoreRowModel(), // debugTable: true, - enableSorting: true, + enableSorting, manualSorting: true, onSortingChange: v => { if (typeof v === 'function') { - const newSortVal = v(orderByArray); - setOrderBy(newSortVal.at(0) ?? null); + const newSortVal = v(sortOrder ?? []); + onSortingChange?.(newSortVal ?? null); } else { - setOrderBy(v.at(0) ?? null); + onSortingChange?.(v ?? null); } }, state: { - sorting: orderByArray, + sorting: sortOrder ?? [], }, enableColumnResizing: true, columnResizeMode: 'onChange' as ColumnResizeMode, - } satisfies TableOptions; + }; const columnSizeProps = { state: { @@ -615,8 +617,9 @@ export const RawLogTable = memo( columns, dedupedRows, tableId, - orderByArray, - setOrderBy, + sortOrder, + enableSorting, + onSortingChange, columnSizeStorage, setColumnSizeStorage, ]); @@ -1225,17 +1228,38 @@ function DBSqlRowTableComponent({ showExpandButton?: boolean; }) { const { data: me } = api.useMe(); - const mergedConfig = useConfigWithPrimaryAndPartitionKey({ - ...searchChartConfigDefaults(me?.team), - ...config, - }); + + const [orderBy, setOrderBy] = useQueryState('orderBy', parseAsSort); + const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); + + const onSortingChange = useCallback( + (v: SortingState | null) => { + setOrderBy(v?.[0] ?? null); + }, + [setOrderBy], + ); + + const mergedConfigObj = useMemo(() => { + const base = { + ...searchChartConfigDefaults(me?.team), + ...config, + }; + if (orderByArray.length) { + base.orderBy = orderByArray.map(o => ({ + valueExpression: o.id, + ordering: o.desc ? 'DESC' : 'ASC', + })); + } + return base; + }, [me, config, orderByArray]); + + const mergedConfig = useConfigWithPrimaryAndPartitionKey(mergedConfigObj); const { data, fetchNextPage, hasNextPage, isFetching, isError, error } = useOffsetPaginatedQuery(mergedConfig ?? config, { enabled: enabled && mergedConfig != null && getSelectLength(config.select) > 0, isLive, - // sort: config.orderBy, queryKeyPrefix, }); @@ -1432,6 +1456,9 @@ function DBSqlRowTableComponent({ onExpandedRowsChange={onExpandedRowsChange} collapseAllRows={collapseAllRows} showExpandButton={showExpandButton} + enableSorting={true} + onSortingChange={onSortingChange} + sortOrder={orderByArray} /> ); From 5df226ebc96e4787dd949ba80b3f2f944807f0e7 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Wed, 1 Oct 2025 09:56:20 -0600 Subject: [PATCH 06/20] improve ux of the table --- packages/app/src/components/DBRowTable.tsx | 57 ++++++++++++++-------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 0f2a8e893..221ef7925 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -42,8 +42,8 @@ import { } from '@hyperdx/common-utils/dist/types'; import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/utils'; import { - ActionIcon, Box, + Button, Code, Flex, Group, @@ -601,7 +601,7 @@ export const RawLogTable = memo( }, enableColumnResizing: true, columnResizeMode: 'onChange' as ColumnResizeMode, - }; + } satisfies TableOptions; const columnSizeProps = { state: { @@ -779,30 +779,49 @@ export const RawLogTable = memo( }} > - {header.isPlaceholder ? null : ( + {!header.column.getCanSort() ? ( {flexRender( header.column.columnDef.header, header.getContext(), )} - )} - - {header.column.getCanSort() && ( - - {header.column.getIsSorted() ? ( - - ) : ( - + ) : ( + + )} + + {header.column.getCanResize() && !isLast && (
)} {!isLoading && isLast && ( - + {tableId && Object.keys(columnSizeStorage).length > 0 && (
Date: Wed, 1 Oct 2025 09:58:56 -0600 Subject: [PATCH 07/20] swap icons --- packages/app/src/components/DBRowTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 221ef7925..3652dd9e4 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -810,9 +810,9 @@ export const RawLogTable = memo(
<> {header.column.getIsSorted() === 'asc' ? ( - - ) : ( + ) : ( + )}
From 34cd889b3993c29e66c7f9d427de5d49b8f1121f Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Wed, 1 Oct 2025 10:47:28 -0600 Subject: [PATCH 08/20] use state instead of query params to support nested tables --- packages/app/src/components/DBRowTable.tsx | 24 +++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 3652dd9e4..8bc0ff53c 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1248,9 +1248,14 @@ function DBSqlRowTableComponent({ }) { const { data: me } = api.useMe(); - const [orderBy, setOrderBy] = useQueryState('orderBy', parseAsSort); + // const [orderBy, setOrderBy] = useQueryState('orderBy', parseAsSort); + const [orderBy, setOrderBy] = useState(null); const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); + useEffect(() => { + setOrderBy(null); + }, [sourceId]); + const onSortingChange = useCallback( (v: SortingState | null) => { setOrderBy(v?.[0] ?? null); @@ -1264,10 +1269,19 @@ function DBSqlRowTableComponent({ ...config, }; if (orderByArray.length) { - base.orderBy = orderByArray.map(o => ({ - valueExpression: o.id, - ordering: o.desc ? 'DESC' : 'ASC', - })); + base.orderBy = orderByArray.map(o => { + if (typeof base.select === 'string') { + return { + valueExpression: o.id, + ordering: o.desc ? 'DESC' : 'ASC', + }; + } + const matchingSelect = base.select?.find(s => s.alias === o.id); + return { + valueExpression: matchingSelect?.valueExpression ?? o.id, + ordering: o.desc ? 'DESC' : 'ASC', + }; + }); } return base; }, [me, config, orderByArray]); From d32b164b4fd195d89f82695ce3d0819b77d40314 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Wed, 1 Oct 2025 12:55:04 -0600 Subject: [PATCH 09/20] correctly resolve aliases at inner level to avoid logic at parent --- packages/app/src/DBSearchPage.tsx | 20 +++++-- packages/app/src/components/DBRowTable.tsx | 57 ++++++------------- .../components/DBSqlRowTableWithSidebar.tsx | 4 ++ 3 files changed, 36 insertions(+), 45 deletions(-) diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index c70671eac..93a691c4c 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -56,6 +56,7 @@ import { } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; import { useIsFetching } from '@tanstack/react-query'; +import { SortingState } from '@tanstack/react-table'; import CodeMirror from '@uiw/react-codemirror'; import { ContactSupportText } from '@/components/ContactSupportText'; @@ -75,10 +76,7 @@ import { Tags } from '@/components/Tags'; import { TimePicker } from '@/components/TimePicker'; import WhereLanguageControlled from '@/components/WhereLanguageControlled'; import { IS_LOCAL_MODE } from '@/config'; -import { - useAliasMapFromChartConfig, - useQueriedChartConfig, -} from '@/hooks/useChartConfig'; +import { useAliasMapFromChartConfig } from '@/hooks/useChartConfig'; import { useExplainQuery } from '@/hooks/useExplainQuery'; import { withAppNav } from '@/layout'; import { @@ -1154,6 +1152,19 @@ function DBSearchPage() { [onSubmit], ); + const onSortingChange = useCallback( + (sortState: SortingState | null) => { + setIsLive(false); + const sort = sortState?.at(0); + setSearchedConfig({ + orderBy: sort + ? `${sort.id} ${sort.desc ? 'DESC' : 'ASC'}` + : defaultOrderBy, + }); + }, + [setIsLive, defaultOrderBy, setSearchedConfig], + ); + const handleTimeRangeSelect = useCallback( (d1: Date, d2: Date) => { onTimeRangeSelect(d1, d2); @@ -1830,6 +1841,7 @@ function DBSearchPage() { onError={handleTableError} denoiseResults={denoiseResults} collapseAllRows={collapseAllRows} + onSortingChange={onSortingChange} /> )} diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 8bc0ff53c..8ed2f1508 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -11,12 +11,6 @@ import { formatDistance } from 'date-fns'; import { isString } from 'lodash'; import curry from 'lodash/curry'; import ms from 'ms'; -import { - createParser, - parseAsString, - parseAsStringLiteral, - useQueryState, -} from 'nuqs'; import { useHotkeys } from 'react-hotkeys-hook'; import { Bar, @@ -72,7 +66,10 @@ import { useVirtualizer } from '@tanstack/react-virtual'; import api from '@/api'; import { searchChartConfigDefaults } from '@/defaults'; -import { useRenderedSqlChartConfig } from '@/hooks/useChartConfig'; +import { + useAliasMapFromChartConfig, + useRenderedSqlChartConfig, +} from '@/hooks/useChartConfig'; import { useCsvExport } from '@/hooks/useCsvExport'; import { useTableMetadata } from '@/hooks/useMetadata'; import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery'; @@ -116,27 +113,6 @@ const ACCESSOR_MAP: Record = { const MAX_SCROLL_FETCH_LINES = 1000; const MAX_CELL_LENGTH = 500; -export const parseAsSort = createParser({ - parse(query) { - if (!query) return null; - const result = parseAsString.parse(query)?.split(' ') ?? []; - // pop the last element, then join the rest - const direction = result.pop() ?? 'ASC'; - const key = result.join(' '); - const desc = - parseAsStringLiteral(['ASC', 'DESC']).parse(direction) ?? 'ASC'; - if (!key) return null; - return { - id: key, - desc: desc === 'DESC', - }; - }, - serialize(value) { - if (!value) return ''; - return `${value.id} ${value.desc ? 'DESC' : 'ASC'}`; - }, -}); - const getRowId = (row: Record): string => row.__hyperdx_id; function retrieveColumnValue(column: string, row: Row): any { @@ -424,6 +400,9 @@ export const RawLogTable = memo( //we need a reference to the scrolling element for logic down below const tableContainerRef = useRef(null); + // Get the alias map from the config so we resolve correct column ids + const { data: aliasMap } = useAliasMapFromChartConfig(config); + // Reset scroll when live tail is enabled for the first time const prevIsLive = usePrevious(isLive); useEffect(() => { @@ -478,7 +457,8 @@ export const RawLogTable = memo( column, jsColumnType, }, - id: column, + // Prefer real column name over alias when possible for ID + id: aliasMap?.[column] ?? column, accessorFn: curry(retrieveColumnValue)(column), // Columns can contain '.' and will not work with accessorKey header: `${columnNameMap?.[column] ?? column}${isDate ? (isUTC ? ' (UTC)' : ' (Local)') : ''}`, cell: info => { @@ -1229,6 +1209,7 @@ function DBSqlRowTableComponent({ collapseAllRows, showExpandButton = true, renderRowDetails, + onSortingChange, }: { config: ChartConfigWithDateRange; sourceId?: string; @@ -1245,10 +1226,10 @@ function DBSqlRowTableComponent({ onExpandedRowsChange?: (hasExpandedRows: boolean) => void; collapseAllRows?: boolean; showExpandButton?: boolean; + onSortingChange?: (v: SortingState | null) => void; }) { const { data: me } = api.useMe(); - // const [orderBy, setOrderBy] = useQueryState('orderBy', parseAsSort); const [orderBy, setOrderBy] = useState(null); const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); @@ -1256,8 +1237,9 @@ function DBSqlRowTableComponent({ setOrderBy(null); }, [sourceId]); - const onSortingChange = useCallback( + const _onSortingChange = useCallback( (v: SortingState | null) => { + onSortingChange?.(v); setOrderBy(v?.[0] ?? null); }, [setOrderBy], @@ -1270,15 +1252,8 @@ function DBSqlRowTableComponent({ }; if (orderByArray.length) { base.orderBy = orderByArray.map(o => { - if (typeof base.select === 'string') { - return { - valueExpression: o.id, - ordering: o.desc ? 'DESC' : 'ASC', - }; - } - const matchingSelect = base.select?.find(s => s.alias === o.id); return { - valueExpression: matchingSelect?.valueExpression ?? o.id, + valueExpression: o.id, ordering: o.desc ? 'DESC' : 'ASC', }; }); @@ -1483,14 +1458,14 @@ function DBSqlRowTableComponent({ columnTypeMap={columnMap} dateRange={config.dateRange} loadingDate={loadingDate} - config={config} + config={mergedConfigObj} onChildModalOpen={onChildModalOpen} source={source} onExpandedRowsChange={onExpandedRowsChange} collapseAllRows={collapseAllRows} showExpandButton={showExpandButton} enableSorting={true} - onSortingChange={onSortingChange} + onSortingChange={_onSortingChange} sortOrder={orderByArray} /> diff --git a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx index e254b0fb7..b81b3098c 100644 --- a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx +++ b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx @@ -5,6 +5,7 @@ import { ChartConfigWithDateRange, TSource, } from '@hyperdx/common-utils/dist/types'; +import { SortingState } from '@tanstack/react-table'; import { useSource } from '@/source'; import TabBar from '@/TabBar'; @@ -35,6 +36,7 @@ interface Props { collapseAllRows?: boolean; isNestedPanel?: boolean; breadcrumbPath?: BreadcrumbEntry[]; + onSortingChange?: (v: SortingState | null) => void; } export default function DBSqlRowTableWithSideBar({ @@ -51,6 +53,7 @@ export default function DBSqlRowTableWithSideBar({ isNestedPanel, breadcrumbPath, onSidebarOpen, + onSortingChange, }: Props) { const { data: sourceData } = useSource({ id: sourceId }); const [rowId, setRowId] = useQueryState('rowWhere'); @@ -89,6 +92,7 @@ export default function DBSqlRowTableWithSideBar({ enabled={enabled} isLive={isLive ?? true} queryKeyPrefix={'dbSqlRowTable'} + onSortingChange={onSortingChange} denoiseResults={denoiseResults} renderRowDetails={r => { if (!sourceData) { From 5c7ac98f98251e1a7e42ba3f56a297a87eaebf75 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Wed, 1 Oct 2025 14:16:44 -0600 Subject: [PATCH 10/20] add tests for sorting --- packages/app/src/components/DBRowTable.tsx | 17 ++- .../components/__tests__/DBRowTable.test.tsx | 139 +++++++++++++++++- 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index d6b25f20f..16c12579c 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -317,7 +317,7 @@ export const RawLogTable = memo( sortOrder, showExpandButton = true, }: { - wrapLines: boolean; + wrapLines?: boolean; displayedColumns: string[]; onSettingsClick?: () => void; onInstructionsClick?: () => void; @@ -333,7 +333,7 @@ export const RawLogTable = memo( hasNextPage?: boolean; highlightedLineId?: string; onScroll?: (scrollTop: number) => void; - isLive: boolean; + isLive?: boolean; onShowPatternsClick?: () => void; tableId?: string; columnNameMap?: Record; @@ -740,7 +740,6 @@ export const RawLogTable = memo( {headerGroup.headers.map((header, headerIndex) => { const isLast = headerIndex === headerGroup.headers.length - 1; - return ( <> {header.isPlaceholder ? null : ( @@ -787,7 +787,14 @@ export const RawLogTable = memo( )} {header.column.getIsSorted() && ( -
+
<> {header.column.getIsSorted() === 'asc' ? ( @@ -1061,7 +1068,7 @@ export const RawLogTable = memo( ) : hasNextPage == false && isLoading == false && dedupedRows.length === 0 ? ( -
+
No results found. Try checking the query explainer in the search bar if diff --git a/packages/app/src/components/__tests__/DBRowTable.test.tsx b/packages/app/src/components/__tests__/DBRowTable.test.tsx index df998fcb3..bda443735 100644 --- a/packages/app/src/components/__tests__/DBRowTable.test.tsx +++ b/packages/app/src/components/__tests__/DBRowTable.test.tsx @@ -1,4 +1,141 @@ -import { appendSelectWithPrimaryAndPartitionKey } from '@/components/DBRowTable'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { + appendSelectWithPrimaryAndPartitionKey, + RawLogTable, +} from '@/components/DBRowTable'; + +import * as useChartConfigModule from '../../hooks/useChartConfig'; + +describe.only('RawLogTable', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest + .spyOn(useChartConfigModule, 'useAliasMapFromChartConfig') + .mockReturnValue({ + data: {}, + isLoading: false, + error: null, + } as any); + }); + + it('should render no results message when no results found', async () => { + renderWithMantine( + {}} + generateRowId={() => ''} + columnTypeMap={new Map()} + />, + ); + + expect(await screen.findByTestId('db-row-table-no-results')).toBeTruthy(); + }); + + describe('Sorting', () => { + const baseProps = { + displayedColumns: ['col1', 'col2'], + rows: [ + { + col1: 'value1', + col2: 'value2', + }, + ], + isLoading: false, + dedupRows: false, + hasNextPage: false, + onRowDetailsClick: () => {}, + generateRowId: () => '', + columnTypeMap: new Map(), + }; + it('Should not allow changing sort if disabled', () => { + renderWithMantine(); + + expect( + screen.queryByTestId('raw-log-table-sort-button'), + ).not.toBeInTheDocument(); + }); + + it('Should allow changing sort', async () => { + const callback = jest.fn(); + + renderWithMantine( + , + ); + + const sortElements = await screen.findAllByTestId( + 'raw-log-table-sort-button', + ); + expect(sortElements).toHaveLength(2); + + await userEvent.click(sortElements.at(0)!); + + expect(callback).toHaveBeenCalledWith([ + { + desc: false, + id: 'col1', + }, + ]); + }); + + it('Should show sort indicator', async () => { + renderWithMantine( + , + ); + + const sortElements = await screen.findByTestId( + 'raw-log-table-sort-indicator', + ); + expect(sortElements).toBeInTheDocument(); + expect(sortElements).toHaveClass('sorted-asc'); + }); + + it('Should reference alias map when possible', async () => { + jest + .spyOn(useChartConfigModule, 'useAliasMapFromChartConfig') + .mockReturnValue({ + data: { + col1: 'col1_alias', + col2: 'col2_alias', + }, + isLoading: false, + error: null, + } as any); + + const callback = jest.fn(); + renderWithMantine( + , + ); + const sortElements = await screen.findAllByTestId( + 'raw-log-table-sort-button', + ); + expect(sortElements).toHaveLength(2); + + await userEvent.click(sortElements.at(0)!); + + expect(callback).toHaveBeenCalledWith([ + { + desc: false, + id: 'col1_alias', + }, + ]); + }); + }); +}); describe('appendSelectWithPrimaryAndPartitionKey', () => { it('should extract columns from partition key with nested function call', () => { From 9583adfb8e765dd2299bb84913131edd07bc8ef1 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Wed, 1 Oct 2025 14:35:39 -0600 Subject: [PATCH 11/20] add changeset --- .changeset/young-eyes-build.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/young-eyes-build.md diff --git a/.changeset/young-eyes-build.md b/.changeset/young-eyes-build.md new file mode 100644 index 000000000..83ad05d1d --- /dev/null +++ b/.changeset/young-eyes-build.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +Add Sorting Feature to all search tables From 98582123a8456440d22cff79613b7afb4c7c027b Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Wed, 1 Oct 2025 16:21:35 -0600 Subject: [PATCH 12/20] disable sorting on dynamic fields --- packages/app/src/components/DBRowTable.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 16c12579c..b2d0f8dc2 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -459,6 +459,8 @@ export const RawLogTable = memo( }, // Prefer real column name over alias when possible for ID id: aliasMap?.[column] ?? column, + // TODO: add support for sorting on Dynamic JSON fields + enableSorting: jsColumnType !== JSDataType.Dynamic, accessorFn: curry(retrieveColumnValue)(column), // Columns can contain '.' and will not work with accessorKey header: `${columnNameMap?.[column] ?? column}${isDate ? (isUTC ? ' (UTC)' : ' (Local)') : ''}`, cell: info => { @@ -778,7 +780,12 @@ export const RawLogTable = memo( > <> {header.isPlaceholder ? null : ( - + {flexRender( header.column.columnDef.header, header.getContext(), From 0cf8af679e44755d0e5eb8b32fb2f09fc4bd7479 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Thu, 2 Oct 2025 09:56:57 -0600 Subject: [PATCH 13/20] add support to the db table chart component --- packages/app/src/ClickhousePage.tsx | 1 + packages/app/src/HDXMultiSeriesTableChart.tsx | 226 ++++++++---------- packages/app/src/components/DBRowTable.tsx | 188 ++++----------- .../src/components/DBTable/TableHeader.tsx | 102 ++++++++ packages/app/src/components/DBTableChart.tsx | 24 +- 5 files changed, 273 insertions(+), 268 deletions(-) create mode 100644 packages/app/src/components/DBTable/TableHeader.tsx diff --git a/packages/app/src/ClickhousePage.tsx b/packages/app/src/ClickhousePage.tsx index 3fd81c93c..bd70d7619 100644 --- a/packages/app/src/ClickhousePage.tsx +++ b/packages/app/src/ClickhousePage.tsx @@ -363,6 +363,7 @@ function InsertsTab({ string; - onSortClick?: (columnNumber: number) => void; tableBottom?: React.ReactNode; + sorting: SortingState; + onSortingChange: (sorting: SortingState) => void; }) => { const MIN_COLUMN_WIDTH_PX = 100; //we need a reference to the scrolling element for logic down below @@ -78,54 +83,60 @@ export const Table = ({ : []), ...columns .filter(c => c.visible !== false) - .map(({ dataKey, displayName, numberFormat, columnWidthPercent }, i) => ({ - accessorKey: dataKey, - header: displayName, - accessorFn: (row: any) => row[dataKey], - cell: ({ - getValue, - row, - }: { - getValue: Getter; - row: Row; - }) => { - const value = getValue(); - let formattedValue: string | number | null = value ?? null; - if (numberFormat) { - formattedValue = formatNumber(value, numberFormat); - } - if (getRowSearchLink == null) { - return formattedValue; - } + .map( + ( + { id, dataKey, displayName, numberFormat, columnWidthPercent }, + i, + ) => ({ + id: id, + accessorKey: dataKey, + header: displayName, + accessorFn: (row: any) => row[dataKey], + cell: ({ + getValue, + row, + }: { + getValue: Getter; + row: Row; + }) => { + const value = getValue(); + let formattedValue: string | number | null = value ?? null; + if (numberFormat) { + formattedValue = formatNumber(value, numberFormat); + } + if (getRowSearchLink == null) { + return formattedValue; + } - return ( - - {formattedValue} - - ); - }, - size: - i === numColumns - 2 - ? UNDEFINED_WIDTH - : tableWidth != null && columnWidthPercent != null - ? Math.max( - tableWidth * (columnWidthPercent / 100), - MIN_COLUMN_WIDTH_PX, - ) - : tableWidth != null - ? tableWidth / numColumns - : 200, - enableResizing: i !== numColumns - 2, - })), + return ( + + {formattedValue} + + ); + }, + size: + i === numColumns - 2 + ? UNDEFINED_WIDTH + : tableWidth != null && columnWidthPercent != null + ? Math.max( + tableWidth * (columnWidthPercent / 100), + MIN_COLUMN_WIDTH_PX, + ) + : tableWidth != null + ? tableWidth / numColumns + : 200, + enableResizing: i !== numColumns - 2, + }), + ), ]; const table = useReactTable({ @@ -134,6 +145,19 @@ export const Table = ({ getCoreRowModel: getCoreRowModel(), enableColumnResizing: true, columnResizeMode: 'onChange', + enableSorting: true, + manualSorting: true, + onSortingChange: v => { + if (typeof v === 'function') { + const newSortVal = v(sorting); + onSortingChange?.(newSortVal ?? null); + } else { + onSortingChange?.(v ?? null); + } + }, + state: { + sorting, + }, }); const { rows } = table.getRowModel(); @@ -172,7 +196,7 @@ export const Table = ({ return (
@@ -187,87 +211,33 @@ export const Table = ({ {table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map((header, headerIndex) => { - const sortOrder = columns[headerIndex - 1]?.sortOrder; return ( - + + + + + + + )} + + } + /> ); })} diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index b2d0f8dc2..dd40069f5 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -87,6 +87,7 @@ import { useWindowSize, } from '@/utils'; +import TableHeader from './DBTable/TableHeader'; import { SQLPreview } from './ChartSQLPreview'; import { CsvExportButton } from './CsvExportButton'; import { @@ -743,148 +744,63 @@ export const RawLogTable = memo( {headerGroup.headers.map((header, headerIndex) => { const isLast = headerIndex === headerGroup.headers.length - 1; return ( - + + } + /> ); })} @@ -1252,7 +1168,7 @@ function DBSqlRowTableComponent({ onSortingChange?.(v); setOrderBy(v?.[0] ?? null); }, - [setOrderBy], + [setOrderBy, onSortingChange], ); const mergedConfigObj = useMemo(() => { diff --git a/packages/app/src/components/DBTable/TableHeader.tsx b/packages/app/src/components/DBTable/TableHeader.tsx new file mode 100644 index 000000000..4af90caaf --- /dev/null +++ b/packages/app/src/components/DBTable/TableHeader.tsx @@ -0,0 +1,102 @@ +import cx from 'classnames'; +import { Button, Group, Text, UnstyledButton } from '@mantine/core'; +import { + IconChevronDown, + IconChevronUp, + IconDotsVertical, +} from '@tabler/icons-react'; +import { flexRender, Header } from '@tanstack/react-table'; + +import { UNDEFINED_WIDTH } from '@/tableUtils'; + +export default function TableHeader({ + isLast, + header, + lastItemButtons, +}: { + isLast: boolean; + header: Header; + lastItemButtons?: React.ReactNode; +}) { + return ( + + ); +} diff --git a/packages/app/src/components/DBTableChart.tsx b/packages/app/src/components/DBTableChart.tsx index 0773d4aaf..c11e052ad 100644 --- a/packages/app/src/components/DBTableChart.tsx +++ b/packages/app/src/components/DBTableChart.tsx @@ -1,12 +1,14 @@ -import { useMemo, useRef } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; import { ChartConfigWithDateRange, ChartConfigWithOptDateRange, } from '@hyperdx/common-utils/dist/types'; import { Box, Code, Text } from '@mantine/core'; +import { SortingState } from '@tanstack/react-table'; import { Table } from '@/HDXMultiSeriesTableChart'; +import { useAliasMapFromChartConfig } from '@/hooks/useChartConfig'; import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery'; import { omit, useIntersectionObserver } from '@/utils'; @@ -15,17 +17,17 @@ import { SQLPreview } from './ChartSQLPreview'; // TODO: Support clicking in to view matched events export default function DBTableChart({ config, - onSortClick, getRowSearchLink, enabled = true, queryKeyPrefix, }: { config: ChartConfigWithOptDateRange; - onSortClick?: (seriesIndex: number) => void; getRowSearchLink?: (row: any) => string; queryKeyPrefix?: string; enabled?: boolean; }) { + const [sort, setSort] = useState([]); + const queriedConfig = (() => { const _config = omit(config, ['granularity']); if (!_config.limit) { @@ -34,6 +36,15 @@ export default function DBTableChart({ if (_config.groupBy && typeof _config.groupBy === 'string') { _config.orderBy = _config.groupBy; } + + if (sort.length) { + _config.orderBy = sort?.map(o => { + return { + valueExpression: o.id, + ordering: o.desc ? 'DESC' : 'ASC', + }; + }); + } return _config; })(); @@ -44,6 +55,8 @@ export default function DBTableChart({ }); const { observerRef: fetchMoreRef } = useIntersectionObserver(fetchNextPage); + // Get the alias map from the config so we resolve correct column ids + const { data: aliasMap } = useAliasMapFromChartConfig(queriedConfig); const columns = useMemo(() => { const rows = data?.data ?? []; if (rows.length === 0) { @@ -55,11 +68,12 @@ export default function DBTableChart({ groupByKeys = queriedConfig.groupBy.split(',').map(v => v.trim()); } return Object.keys(rows?.[0]).map(key => ({ + id: aliasMap?.[key] ?? key, dataKey: key, displayName: key, numberFormat: groupByKeys.includes(key) ? undefined : config.numberFormat, })); - }, [config.numberFormat, data]); + }, [config.numberFormat, aliasMap, queriedConfig.groupBy, data]); return isLoading && !data ? (
@@ -101,6 +115,8 @@ export default function DBTableChart({ data={data?.data ?? []} columns={columns} getRowSearchLink={getRowSearchLink} + sorting={sort} + onSortingChange={setSort} tableBottom={ hasNextPage && ( From cf9cc5c345eaf2ab33bb768c099625b4ac5d9cd8 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Thu, 2 Oct 2025 14:11:14 -0600 Subject: [PATCH 14/20] fix aliasing support --- packages/app/src/ClickhousePage.tsx | 49 ++++++++++++++++---- packages/app/src/components/DBTableChart.tsx | 24 ++++++++-- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/packages/app/src/ClickhousePage.tsx b/packages/app/src/ClickhousePage.tsx index bd70d7619..7307fc430 100644 --- a/packages/app/src/ClickhousePage.tsx +++ b/packages/app/src/ClickhousePage.tsx @@ -365,12 +365,29 @@ function InsertsTab({ config={{ dateRange: searchedTimeRange, select: [ - `count() as "Part Count"`, - `sum(rows) as Rows`, - 'database as Database', - 'table as Table', - 'partition as Partition', - ].join(','), + { + aggFn: 'count', + valueExpression: '', + alias: 'Part Count', + }, + { + aggFn: 'sum', + valueExpression: 'rows', + alias: 'Rows', + }, + { + valueExpression: 'database', + alias: 'Database', + }, + { + valueExpression: 'table', + alias: 'Table', + }, + { + valueExpression: 'partition', + alias: 'Partition', + }, + ], from: { databaseName: 'system', tableName: 'parts', @@ -662,10 +679,22 @@ function ClickhousePage() { { + // If the config.select is a string, we can't infer this. + // One day, we could potentially run this through chSqlToAliasMap but AST parsing + // doesn't work for most DBTableChart queries. + if (typeof config.select === 'string') { + return []; + } + return config.select.reduce((acc, select) => { + if (select.alias) { + acc.push(select.alias); + } + return acc; + }, [] as string[]); + }, [config?.select]); const columns = useMemo(() => { const rows = data?.data ?? []; if (rows.length === 0) { @@ -67,8 +79,10 @@ export default function DBTableChart({ if (queriedConfig.groupBy && typeof queriedConfig.groupBy === 'string') { groupByKeys = queriedConfig.groupBy.split(',').map(v => v.trim()); } + return Object.keys(rows?.[0]).map(key => ({ - id: aliasMap?.[key] ?? key, + // If it's an alias, wrap in quotes to support a variety of formats (ex "Time (ms)", "Req/s", etc) + id: aliasMap.includes(key) ? `"${key}"` : key, dataKey: key, displayName: key, numberFormat: groupByKeys.includes(key) ? undefined : config.numberFormat, From 0f6f73cd892dd0e2c20e421a21bfe2abad252351 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Fri, 3 Oct 2025 16:13:24 -0600 Subject: [PATCH 15/20] improve types to catch missing dateRange in future --- packages/app/src/components/DBTableChart.tsx | 3 ++- packages/app/src/hooks/useOffsetPaginatedQuery.tsx | 12 ++++++------ packages/common-utils/src/types.ts | 7 +++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/DBTableChart.tsx b/packages/app/src/components/DBTableChart.tsx index 41e3f5573..053039ca2 100644 --- a/packages/app/src/components/DBTableChart.tsx +++ b/packages/app/src/components/DBTableChart.tsx @@ -3,6 +3,7 @@ import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; import { ChartConfigWithDateRange, ChartConfigWithOptDateRange, + ChatConfigWithOptTimestamp, } from '@hyperdx/common-utils/dist/types'; import { Box, Code, Text } from '@mantine/core'; import { SortingState } from '@tanstack/react-table'; @@ -20,7 +21,7 @@ export default function DBTableChart({ enabled = true, queryKeyPrefix, }: { - config: ChartConfigWithOptDateRange; + config: ChatConfigWithOptTimestamp; getRowSearchLink?: (row: any) => string; queryKeyPrefix?: string; enabled?: boolean; diff --git a/packages/app/src/hooks/useOffsetPaginatedQuery.tsx b/packages/app/src/hooks/useOffsetPaginatedQuery.tsx index b92a91f7d..5de1dbd05 100644 --- a/packages/app/src/hooks/useOffsetPaginatedQuery.tsx +++ b/packages/app/src/hooks/useOffsetPaginatedQuery.tsx @@ -7,7 +7,7 @@ import { ColumnMetaType, } from '@hyperdx/common-utils/dist/clickhouse'; import { renderChartConfig } from '@hyperdx/common-utils/dist/renderChartConfig'; -import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; +import { ChatConfigWithOptTimestamp } from '@hyperdx/common-utils/dist/types'; import { isFirstOrderByAscending, isTimestampExpressionInFirstOrderBy, @@ -26,12 +26,12 @@ import { omit } from '@/utils'; type TQueryKey = readonly [ string, - ChartConfigWithDateRange, + ChatConfigWithOptTimestamp, number | undefined, ]; function queryKeyFn( prefix: string, - config: ChartConfigWithDateRange, + config: ChatConfigWithOptTimestamp, queryTimeout?: number, ): TQueryKey { return [prefix, config, queryTimeout]; @@ -130,7 +130,7 @@ function generateTimeWindowsAscending(startDate: Date, endDate: Date) { // Get time window from page param function getTimeWindowFromPageParam( - config: ChartConfigWithDateRange, + config: ChatConfigWithOptTimestamp, pageParam: TPageParam, ): TimeWindow { const [startDate, endDate] = config.dateRange; @@ -148,7 +148,7 @@ function getTimeWindowFromPageParam( function getNextPageParam( lastPage: TQueryFnData | null, allPages: TQueryFnData[], - config: ChartConfigWithDateRange, + config: ChatConfigWithOptTimestamp, ): TPageParam | undefined { if (lastPage == null) { return undefined; @@ -427,7 +427,7 @@ function flattenData(data: TData | undefined): TQueryFnData | null { } export default function useOffsetPaginatedQuery( - config: ChartConfigWithDateRange, + config: ChatConfigWithOptTimestamp, { isLive, enabled = true, diff --git a/packages/common-utils/src/types.ts b/packages/common-utils/src/types.ts index 8bb6d6de1..c74874fc1 100644 --- a/packages/common-utils/src/types.ts +++ b/packages/common-utils/src/types.ts @@ -421,6 +421,13 @@ export type DateRange = { }; export type ChartConfigWithDateRange = ChartConfig & DateRange; + +export type ChatConfigWithOptTimestamp = Omit< + ChartConfigWithDateRange, + 'timestampValueExpression' +> & { + timestampValueExpression?: string; +}; // For non-time-based searches (ex. grab 1 row) export type ChartConfigWithOptDateRange = Omit< ChartConfig, From febfa646966e793d21d6d6d9cca4f6c5099d7bb4 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Fri, 3 Oct 2025 16:34:18 -0600 Subject: [PATCH 16/20] fix linting --- packages/common-utils/src/utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/common-utils/src/utils.ts b/packages/common-utils/src/utils.ts index 8ba32959b..d591042ac 100644 --- a/packages/common-utils/src/utils.ts +++ b/packages/common-utils/src/utils.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { ChartConfigWithDateRange, + ChatConfigWithOptTimestamp, DashboardFilter, DashboardFilterSchema, DashboardSchema, @@ -418,10 +419,11 @@ export const removeTrailingDirection = (s: string) => { }; export const isTimestampExpressionInFirstOrderBy = ( - config: ChartConfigWithDateRange, + config: ChatConfigWithOptTimestamp, ) => { const firstOrderingItem = getFirstOrderingItem(config.orderBy); - if (!firstOrderingItem) return false; + if (!firstOrderingItem || config.timestampValueExpression == null) + return false; const firstOrderingExpression = typeof firstOrderingItem === 'string' From 393bd18de19c45d8d5fb419becaa9f1275db8fe6 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Mon, 6 Oct 2025 15:53:31 -0600 Subject: [PATCH 17/20] bug fixes --- packages/app/src/components/DBRowTable.tsx | 4 ++-- packages/app/src/components/DBTable/TableHeader.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index dd40069f5..04be9e70b 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -458,8 +458,8 @@ export const RawLogTable = memo( column, jsColumnType, }, - // Prefer real column name over alias when possible for ID - id: aliasMap?.[column] ?? column, + // If the column is an alias, wrap in quotes. + id: aliasMap?.[column] ? `"${column}"` : column, // TODO: add support for sorting on Dynamic JSON fields enableSorting: jsColumnType !== JSDataType.Dynamic, accessorFn: curry(retrieveColumnValue)(column), // Columns can contain '.' and will not work with accessorKey diff --git a/packages/app/src/components/DBTable/TableHeader.tsx b/packages/app/src/components/DBTable/TableHeader.tsx index 4af90caaf..8eef4eb9e 100644 --- a/packages/app/src/components/DBTable/TableHeader.tsx +++ b/packages/app/src/components/DBTable/TableHeader.tsx @@ -1,8 +1,8 @@ import cx from 'classnames'; -import { Button, Group, Text, UnstyledButton } from '@mantine/core'; +import { Button, Group, Text } from '@mantine/core'; import { - IconChevronDown, - IconChevronUp, + IconArrowDown, + IconArrowUp, IconDotsVertical, } from '@tabler/icons-react'; import { flexRender, Header } from '@tanstack/react-table'; @@ -66,9 +66,9 @@ export default function TableHeader({ > <> {header.column.getIsSorted() === 'asc' ? ( - + ) : ( - + )}
From e713c17b38135d983d7b6de559a89c7f5629bac1 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Mon, 6 Oct 2025 16:12:18 -0600 Subject: [PATCH 18/20] change initial sort logic --- packages/app/src/DBSearchPage.tsx | 11 +++++++++- packages/app/src/components/DBRowTable.tsx | 10 +++++++-- .../components/DBSqlRowTableWithSidebar.tsx | 3 +++ packages/app/src/utils/queryParsers.ts | 22 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 24e74cd50..73f706715 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -102,7 +102,10 @@ import PatternTable from './components/PatternTable'; import SourceSchemaPreview from './components/SourceSchemaPreview'; import { useTableMetadata } from './hooks/useMetadata'; import { useSqlSuggestions } from './hooks/useSqlSuggestions'; -import { parseAsStringWithNewLines } from './utils/queryParsers'; +import { + parseAsSortingStateString, + parseAsStringWithNewLines, +} from './utils/queryParsers'; import api from './api'; import { LOCAL_STORE_CONNECTIONS_KEY } from './connection'; import { DBSearchPageAlertModal } from './DBSearchPageAlertModal'; @@ -1165,6 +1168,11 @@ function DBSearchPage() { }, [setIsLive, defaultOrderBy, setSearchedConfig], ); + // Parse the orderBy string into a SortingState. We need the string + // version in other places so we keep this parser separate. + const orderByConfig = parseAsSortingStateString.parse( + searchedConfig.orderBy ?? '', + ); const handleTimeRangeSelect = useCallback( (d1: Date, d2: Date) => { @@ -1843,6 +1851,7 @@ function DBSearchPage() { denoiseResults={denoiseResults} collapseAllRows={collapseAllRows} onSortingChange={onSortingChange} + initialSortBy={orderByConfig ? [orderByConfig] : []} /> )} diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 04be9e70b..1757213c5 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1136,6 +1136,7 @@ function DBSqlRowTableComponent({ showExpandButton = true, renderRowDetails, onSortingChange, + initialSortBy, }: { config: ChartConfigWithDateRange; sourceId?: string; @@ -1152,15 +1153,20 @@ function DBSqlRowTableComponent({ onExpandedRowsChange?: (hasExpandedRows: boolean) => void; collapseAllRows?: boolean; showExpandButton?: boolean; + initialSortBy?: SortingState; onSortingChange?: (v: SortingState | null) => void; }) { const { data: me } = api.useMe(); - const [orderBy, setOrderBy] = useState(null); + const [orderBy, setOrderBy] = useState( + initialSortBy?.[0] ?? null, + ); + const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); useEffect(() => { - setOrderBy(null); + setOrderBy(initialSortBy?.at(0) ?? null); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sourceId]); const _onSortingChange = useCallback( diff --git a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx index b81b3098c..7a43391b2 100644 --- a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx +++ b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx @@ -37,6 +37,7 @@ interface Props { isNestedPanel?: boolean; breadcrumbPath?: BreadcrumbEntry[]; onSortingChange?: (v: SortingState | null) => void; + initialSortBy?: SortingState; } export default function DBSqlRowTableWithSideBar({ @@ -54,6 +55,7 @@ export default function DBSqlRowTableWithSideBar({ breadcrumbPath, onSidebarOpen, onSortingChange, + initialSortBy, }: Props) { const { data: sourceData } = useSource({ id: sourceId }); const [rowId, setRowId] = useQueryState('rowWhere'); @@ -94,6 +96,7 @@ export default function DBSqlRowTableWithSideBar({ queryKeyPrefix={'dbSqlRowTable'} onSortingChange={onSortingChange} denoiseResults={denoiseResults} + initialSortBy={initialSortBy} renderRowDetails={r => { if (!sourceData) { return
Loading...
; diff --git a/packages/app/src/utils/queryParsers.ts b/packages/app/src/utils/queryParsers.ts index 79a896391..f7c749440 100644 --- a/packages/app/src/utils/queryParsers.ts +++ b/packages/app/src/utils/queryParsers.ts @@ -1,4 +1,5 @@ import { createParser } from 'nuqs'; +import { SortingState } from '@tanstack/react-table'; // Note: this can be deleted once we upgrade to nuqs v2.2.3 // https://github.com/47ng/nuqs/pull/783 @@ -6,3 +7,24 @@ export const parseAsStringWithNewLines = createParser({ parse: value => value.replace(/%0A/g, '\n'), serialize: value => value.replace(/\n/g, '%0A'), }); + +export const parseAsSortingStateString = createParser({ + parse: value => { + if (!value) { + return null; + } + const keys = value.split(' '); + const direction = keys.pop(); + const key = keys.join(' '); + return { + id: key, + desc: direction === 'DESC', + }; + }, + serialize: value => { + if (!value) { + return ''; + } + return `${value.id} ${value.desc ? 'DESC' : 'ASC'}`; + }, +}); From 0cfb723303dbc45fc2aac68b1722fb444ef53984 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Mon, 6 Oct 2025 16:21:33 -0600 Subject: [PATCH 19/20] tweak logic to correctly change sort only when sources changes --- packages/app/src/components/DBRowTable.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 1757213c5..11f4b6fd5 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1164,11 +1164,6 @@ function DBSqlRowTableComponent({ const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); - useEffect(() => { - setOrderBy(initialSortBy?.at(0) ?? null); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sourceId]); - const _onSortingChange = useCallback( (v: SortingState | null) => { onSortingChange?.(v); @@ -1177,6 +1172,14 @@ function DBSqlRowTableComponent({ [setOrderBy, onSortingChange], ); + const prevSourceId = usePrevious(sourceId); + useEffect(() => { + if (prevSourceId && prevSourceId !== sourceId) { + _onSortingChange(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sourceId]); + const mergedConfigObj = useMemo(() => { const base = { ...searchChartConfigDefaults(me?.team), From c9d31ce1b68d96b19453c255eb77302e28a4d46a Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Mon, 6 Oct 2025 16:22:57 -0600 Subject: [PATCH 20/20] improve aliasing test --- packages/app/src/components/__tests__/DBRowTable.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/__tests__/DBRowTable.test.tsx b/packages/app/src/components/__tests__/DBRowTable.test.tsx index bda443735..234431268 100644 --- a/packages/app/src/components/__tests__/DBRowTable.test.tsx +++ b/packages/app/src/components/__tests__/DBRowTable.test.tsx @@ -8,7 +8,7 @@ import { import * as useChartConfigModule from '../../hooks/useChartConfig'; -describe.only('RawLogTable', () => { +describe('RawLogTable', () => { beforeEach(() => { jest.clearAllMocks(); jest @@ -130,7 +130,7 @@ describe.only('RawLogTable', () => { expect(callback).toHaveBeenCalledWith([ { desc: false, - id: 'col1_alias', + id: '"col1"', }, ]); });
- {header.isPlaceholder ? null : ( - -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- - {headerIndex > 0 && onSortClick != null && ( -
onSortClick(headerIndex - 1)} + header={header} + isLast={headerIndex === headerGroup.headers.length - 1} + lastItemButtons={ + <> + {headerIndex === headerGroup.headers.length - 1 && ( +
+ setWrapLinesEnabled(prev => !prev)} > - {sortOrder === 'asc' ? ( - - - - ) : sortOrder === 'desc' ? ( - - - - ) : ( - - - - )} -
- )} - {header.column.getCanResize() && - headerIndex !== headerGroup.headers.length - 1 && ( -
- -
- )} - {headerIndex === headerGroup.headers.length - 1 && ( -
- - setWrapLinesEnabled(prev => !prev) - } - > - - - - - -
- )} - - - )} -
- - {!header.column.getCanSort() ? ( - - {flexRender( - header.column.columnDef.header, - header.getContext(), + header={header} + isLast={isLast} + lastItemButtons={ + <> + {tableId && + Object.keys(columnSizeStorage).length > 0 && ( +
setColumnSizeStorage({})} + title="Reset Column Widths" + > + +
)} -
- ) : ( - - )} - - - {header.column.getCanResize() && !isLast && ( + + + + + + + + + + + {onSettingsClick != null && (
onSettingsClick()} > - +
)} - {!isLoading && isLast && ( - - {tableId && - Object.keys(columnSizeStorage).length > 0 && ( -
setColumnSizeStorage({})} - title="Reset Column Widths" - > - -
- )} - {config && ( - handleSqlModalOpen(true)} - > - - - - - )} - - setWrapLinesEnabled(prev => !prev) - } - > - - - - - - - - - - - {onSettingsClick != null && ( -
onSettingsClick()} - > - -
- )} -
- )} -
-
-
+ + {!header.column.getCanSort() ? ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ) : ( + + )} + + + {header.column.getCanResize() && !isLast && ( +
+ +
+ )} + {isLast && ( + + {lastItemButtons} + + )} +
+
+