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 diff --git a/packages/app/src/ClickhousePage.tsx b/packages/app/src/ClickhousePage.tsx index 3fd81c93c..7307fc430 100644 --- a/packages/app/src/ClickhousePage.tsx +++ b/packages/app/src/ClickhousePage.tsx @@ -363,13 +363,31 @@ function InsertsTab({ { + setIsLive(false); + const sort = sortState?.at(0); + setSearchedConfig({ + orderBy: sort + ? `${sort.id} ${sort.desc ? 'DESC' : 'ASC'}` + : defaultOrderBy, + }); + }, + [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) => { onTimeRangeSelect(d1, d2); @@ -1831,6 +1850,8 @@ function DBSearchPage() { onError={handleTableError} denoiseResults={denoiseResults} collapseAllRows={collapseAllRows} + onSortingChange={onSortingChange} + initialSortBy={orderByConfig ? [orderByConfig] : []} /> )} diff --git a/packages/app/src/HDXMultiSeriesTableChart.tsx b/packages/app/src/HDXMultiSeriesTableChart.tsx index 1dc86c616..f929e79b9 100644 --- a/packages/app/src/HDXMultiSeriesTableChart.tsx +++ b/packages/app/src/HDXMultiSeriesTableChart.tsx @@ -8,12 +8,14 @@ import { Getter, Row, Row as TableRow, + SortingState, useReactTable, } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; import { CsvExportButton } from './components/CsvExportButton'; +import TableHeader from './components/DBTable/TableHeader'; import { useCsvExport } from './hooks/useCsvExport'; import { UNDEFINED_WIDTH } from './tableUtils'; import type { NumberFormat } from './types'; @@ -24,11 +26,13 @@ export const Table = ({ groupColumnName, columns, getRowSearchLink, - onSortClick, tableBottom, + sorting, + onSortingChange, }: { data: any[]; columns: { + id: string; dataKey: string; displayName: string; sortOrder?: 'asc' | 'desc'; @@ -38,8 +42,9 @@ export const Table = ({ }[]; groupColumnName?: string; getRowSearchLink?: (row: any) => 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/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 d3fbc5e21..11f4b6fd5 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'; @@ -37,13 +37,20 @@ import { import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/utils'; import { Box, + Button, 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'; @@ -58,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'; @@ -76,6 +87,7 @@ import { useWindowSize, } from '@/utils'; +import TableHeader from './DBTable/TableHeader'; import { SQLPreview } from './ChartSQLPreview'; import { CsvExportButton } from './CsvExportButton'; import { @@ -301,9 +313,12 @@ export const RawLogTable = memo( source, onExpandedRowsChange, collapseAllRows, + enableSorting = false, + onSortingChange, + sortOrder, showExpandButton = true, }: { - wrapLines: boolean; + wrapLines?: boolean; displayedColumns: string[]; onSettingsClick?: () => void; onInstructionsClick?: () => void; @@ -319,7 +334,7 @@ export const RawLogTable = memo( hasNextPage?: boolean; highlightedLineId?: string; onScroll?: (scrollTop: number) => void; - isLive: boolean; + isLive?: boolean; onShowPatternsClick?: () => void; tableId?: string; columnNameMap?: Record; @@ -338,6 +353,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; @@ -383,6 +401,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(() => { @@ -437,6 +458,10 @@ export const RawLogTable = memo( column, jsColumnType, }, + // 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 header: `${columnNameMap?.[column] ?? column}${isDate ? (isUTC ? ' (UTC)' : ' (Local)') : ''}`, cell: info => { @@ -544,9 +569,22 @@ export const RawLogTable = memo( columns, getCoreRowModel: getCoreRowModel(), // debugTable: true, + enableSorting, + manualSorting: true, + onSortingChange: v => { + if (typeof v === 'function') { + const newSortVal = v(sortOrder ?? []); + onSortingChange?.(newSortVal ?? null); + } else { + onSortingChange?.(v ?? null); + } + }, + state: { + sorting: sortOrder ?? [], + }, enableColumnResizing: true, columnResizeMode: 'onChange' as ColumnResizeMode, - }; + } satisfies TableOptions; const columnSizeProps = { state: { @@ -562,6 +600,9 @@ export const RawLogTable = memo( columns, dedupedRows, tableId, + sortOrder, + enableSorting, + onSortingChange, columnSizeStorage, setColumnSizeStorage, ]); @@ -701,62 +742,15 @@ export const RawLogTable = memo( {table.getHeaderGroups().map(headerGroup => (
{headerGroup.headers.map((header, headerIndex) => { + const isLast = headerIndex === headerGroup.headers.length - 1; return ( - + + } + /> ); })} @@ -998,7 +991,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 @@ -1142,6 +1135,8 @@ function DBSqlRowTableComponent({ collapseAllRows, showExpandButton = true, renderRowDetails, + onSortingChange, + initialSortBy, }: { config: ChartConfigWithDateRange; sourceId?: string; @@ -1158,12 +1153,50 @@ function DBSqlRowTableComponent({ onExpandedRowsChange?: (hasExpandedRows: boolean) => void; collapseAllRows?: boolean; showExpandButton?: boolean; + initialSortBy?: SortingState; + onSortingChange?: (v: SortingState | null) => void; }) { const { data: me } = api.useMe(); - const mergedConfig = useConfigWithPrimaryAndPartitionKey({ - ...searchChartConfigDefaults(me?.team), - ...config, - }); + + const [orderBy, setOrderBy] = useState( + initialSortBy?.[0] ?? null, + ); + + const orderByArray = useMemo(() => (orderBy ? [orderBy] : []), [orderBy]); + + const _onSortingChange = useCallback( + (v: SortingState | null) => { + onSortingChange?.(v); + setOrderBy(v?.[0] ?? null); + }, + [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), + ...config, + }; + if (orderByArray.length) { + base.orderBy = orderByArray.map(o => { + return { + 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, { @@ -1360,12 +1393,15 @@ 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} + sortOrder={orderByArray} /> ); diff --git a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx index e254b0fb7..7a43391b2 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,8 @@ interface Props { collapseAllRows?: boolean; isNestedPanel?: boolean; breadcrumbPath?: BreadcrumbEntry[]; + onSortingChange?: (v: SortingState | null) => void; + initialSortBy?: SortingState; } export default function DBSqlRowTableWithSideBar({ @@ -51,6 +54,8 @@ export default function DBSqlRowTableWithSideBar({ isNestedPanel, breadcrumbPath, onSidebarOpen, + onSortingChange, + initialSortBy, }: Props) { const { data: sourceData } = useSource({ id: sourceId }); const [rowId, setRowId] = useQueryState('rowWhere'); @@ -89,7 +94,9 @@ export default function DBSqlRowTableWithSideBar({ enabled={enabled} isLive={isLive ?? true} queryKeyPrefix={'dbSqlRowTable'} + onSortingChange={onSortingChange} denoiseResults={denoiseResults} + initialSortBy={initialSortBy} renderRowDetails={r => { if (!sourceData) { return
Loading...
; diff --git a/packages/app/src/components/DBTable/TableHeader.tsx b/packages/app/src/components/DBTable/TableHeader.tsx new file mode 100644 index 000000000..8eef4eb9e --- /dev/null +++ b/packages/app/src/components/DBTable/TableHeader.tsx @@ -0,0 +1,102 @@ +import cx from 'classnames'; +import { Button, Group, Text } from '@mantine/core'; +import { + IconArrowDown, + IconArrowUp, + 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..053039ca2 100644 --- a/packages/app/src/components/DBTableChart.tsx +++ b/packages/app/src/components/DBTableChart.tsx @@ -1,10 +1,12 @@ -import { useMemo, useRef } from 'react'; +import { useMemo, useState } from 'react'; 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'; import { Table } from '@/HDXMultiSeriesTableChart'; import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery'; @@ -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; + config: ChatConfigWithOptTimestamp; 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,21 @@ export default function DBTableChart({ }); const { observerRef: fetchMoreRef } = useIntersectionObserver(fetchNextPage); + // Returns an array of aliases, so we can check if something is using an alias + const aliasMap = useMemo(() => { + // 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) { @@ -54,12 +80,15 @@ 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 => ({ + // 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, })); - }, [config.numberFormat, data]); + }, [config.numberFormat, aliasMap, queriedConfig.groupBy, data]); return isLoading && !data ? (
@@ -101,6 +130,8 @@ export default function DBTableChart({ data={data?.data ?? []} columns={columns} getRowSearchLink={getRowSearchLink} + sorting={sort} + onSortingChange={setSort} tableBottom={ hasNextPage && ( 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', }, diff --git a/packages/app/src/components/__tests__/DBRowTable.test.tsx b/packages/app/src/components/__tests__/DBRowTable.test.tsx index df998fcb3..234431268 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('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"', + }, + ]); + }); + }); +}); describe('appendSelectWithPrimaryAndPartitionKey', () => { it('should extract columns from partition key with nested function call', () => { diff --git a/packages/app/src/hooks/useOffsetPaginatedQuery.tsx b/packages/app/src/hooks/useOffsetPaginatedQuery.tsx index 36ce3f1ae..4df9abd2c 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; @@ -428,7 +428,7 @@ function flattenData(data: TData | undefined): TQueryFnData | null { } export default function useOffsetPaginatedQuery( - config: ChartConfigWithDateRange, + config: ChatConfigWithOptTimestamp, { isLive, enabled = true, 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'}`; + }, +}); diff --git a/packages/common-utils/src/clickhouse/index.ts b/packages/common-utils/src/clickhouse/index.ts index c829246c8..bb55f5b4e 100644 --- a/packages/common-utils/src/clickhouse/index.ts +++ b/packages/common-utils/src/clickhouse/index.ts @@ -470,11 +470,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( 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, diff --git a/packages/common-utils/src/utils.ts b/packages/common-utils/src/utils.ts index 1535c5949..a18b03dbe 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, @@ -546,10 +547,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'
- {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.isPlaceholder ? null : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- )} - {header.column.getCanResize() && - headerIndex !== headerGroup.headers.length - 1 && ( -
- -
- )} - {headerIndex === headerGroup.headers.length - 1 && ( -
- {tableId != null && + header={header} + isLast={isLast} + lastItemButtons={ + <> + {tableId && Object.keys(columnSizeStorage).length > 0 && (
setWrapLinesEnabled(prev => !prev)} - className="ms-2" > @@ -788,7 +781,7 @@ export const RawLogTable = memo( {onSettingsClick != null && (
onSettingsClick()} >
)} -
- )} -
+ + {!header.column.getCanSort() ? ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ) : ( + + )} + + + {header.column.getCanResize() && !isLast && ( +
+ +
+ )} + {isLast && ( + + {lastItemButtons} + + )} +
+
+