mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Log Results Sorting (#1232)
This commit is contained in:
parent
b46ae2f204
commit
b34480411e
16 changed files with 605 additions and 234 deletions
5
.changeset/young-eyes-build.md
Normal file
5
.changeset/young-eyes-build.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
Add Sorting Feature to all search tables
|
||||
|
|
@ -363,13 +363,31 @@ function InsertsTab({
|
|||
</Text>
|
||||
<DBTableChart
|
||||
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',
|
||||
|
|
@ -661,10 +679,22 @@ function ClickhousePage() {
|
|||
<DBTableChart
|
||||
config={{
|
||||
select: [
|
||||
`count() as "Count"`,
|
||||
`sum(query_duration_ms) as "Total Duration (ms)"`,
|
||||
`any(query) as "Query Example"`,
|
||||
].join(','),
|
||||
{
|
||||
aggFn: 'count',
|
||||
valueExpression: '',
|
||||
alias: `Count`,
|
||||
},
|
||||
{
|
||||
aggFn: 'sum',
|
||||
valueExpression: 'query_duration_ms',
|
||||
alias: `Total Duration (ms)`,
|
||||
},
|
||||
{
|
||||
aggFn: 'any',
|
||||
valueExpression: 'query',
|
||||
alias: `Query Example`,
|
||||
},
|
||||
],
|
||||
dateRange: searchedTimeRange,
|
||||
from,
|
||||
where: `(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -104,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';
|
||||
|
|
@ -1155,6 +1156,24 @@ 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],
|
||||
);
|
||||
// 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] : []}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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<number>;
|
||||
row: Row<any>;
|
||||
}) => {
|
||||
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<number>;
|
||||
row: Row<any>;
|
||||
}) => {
|
||||
const value = getValue();
|
||||
let formattedValue: string | number | null = value ?? null;
|
||||
if (numberFormat) {
|
||||
formattedValue = formatNumber(value, numberFormat);
|
||||
}
|
||||
if (getRowSearchLink == null) {
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={getRowSearchLink(row.original)}
|
||||
passHref
|
||||
className={'align-top overflow-hidden py-1 pe-3'}
|
||||
style={{
|
||||
display: 'block',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{formattedValue}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
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 (
|
||||
<Link
|
||||
href={getRowSearchLink(row.original)}
|
||||
passHref
|
||||
className={'align-top overflow-hidden py-1 pe-3'}
|
||||
style={{
|
||||
display: 'block',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{formattedValue}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
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 (
|
||||
<div
|
||||
className="overflow-auto h-100 fs-8 bg-inherit"
|
||||
className="overflow-auto h-100 fs-8 bg-inherit dark:bg-dark"
|
||||
ref={tableContainerRef}
|
||||
>
|
||||
<table className="w-100 bg-inherit" style={{ tableLayout: 'fixed' }}>
|
||||
|
|
@ -187,87 +211,33 @@ export const Table = ({
|
|||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, headerIndex) => {
|
||||
const sortOrder = columns[headerIndex - 1]?.sortOrder;
|
||||
return (
|
||||
<th
|
||||
className="overflow-hidden text-truncate"
|
||||
<TableHeader
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
style={{
|
||||
width:
|
||||
header.getSize() === UNDEFINED_WIDTH
|
||||
? '100%'
|
||||
: header.getSize(),
|
||||
minWidth: 100,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<Flex justify="space-between">
|
||||
<div>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</div>
|
||||
<Flex gap="sm">
|
||||
{headerIndex > 0 && onSortClick != null && (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => onSortClick(headerIndex - 1)}
|
||||
header={header}
|
||||
isLast={headerIndex === headerGroup.headers.length - 1}
|
||||
lastItemButtons={
|
||||
<>
|
||||
{headerIndex === headerGroup.headers.length - 1 && (
|
||||
<div className="d-flex align-items-center">
|
||||
<UnstyledButton
|
||||
onClick={() => setWrapLinesEnabled(prev => !prev)}
|
||||
>
|
||||
{sortOrder === 'asc' ? (
|
||||
<Text c="green">
|
||||
<i className="bi bi-sort-numeric-up-alt"></i>
|
||||
</Text>
|
||||
) : sortOrder === 'desc' ? (
|
||||
<Text c="green">
|
||||
<i className="bi bi-sort-numeric-down-alt"></i>
|
||||
</Text>
|
||||
) : (
|
||||
<Text c="dark.2">
|
||||
<i className="bi bi-sort-numeric-down-alt"></i>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{header.column.getCanResize() &&
|
||||
headerIndex !== headerGroup.headers.length - 1 && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={`resizer text-gray-600 cursor-grab ${
|
||||
header.column.getIsResizing()
|
||||
? 'isResizing'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<i className="bi bi-three-dots-vertical" />
|
||||
</div>
|
||||
)}
|
||||
{headerIndex === headerGroup.headers.length - 1 && (
|
||||
<div className="d-flex align-items-center">
|
||||
<UnstyledButton
|
||||
onClick={() =>
|
||||
setWrapLinesEnabled(prev => !prev)
|
||||
}
|
||||
>
|
||||
<i className="bi bi-text-wrap" />
|
||||
</UnstyledButton>
|
||||
<CsvExportButton
|
||||
data={csvData}
|
||||
filename="HyperDX_table_results"
|
||||
className="fs-8 text-muted-hover ms-2"
|
||||
title="Download table as CSV"
|
||||
>
|
||||
<i className="bi bi-download" />
|
||||
</CsvExportButton>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</th>
|
||||
<i className="bi bi-text-wrap" />
|
||||
</UnstyledButton>
|
||||
<CsvExportButton
|
||||
data={csvData}
|
||||
filename="HyperDX_table_results"
|
||||
className="fs-8 text-muted-hover ms-2"
|
||||
title="Download table as CSV"
|
||||
>
|
||||
<i className="bi bi-download" />
|
||||
</CsvExportButton>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { useCSVDownloader } from 'react-papaparse';
|
||||
import { UnstyledButton } from '@mantine/core';
|
||||
|
||||
interface CsvExportButtonProps {
|
||||
data: Record<string, any>[];
|
||||
|
|
@ -57,9 +58,8 @@ export const CsvExportButton: React.FC<CsvExportButtonProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<UnstyledButton
|
||||
className={className}
|
||||
role="button"
|
||||
title={title}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
|
|
@ -88,6 +88,6 @@ export const CsvExportButton: React.FC<CsvExportButtonProps> = ({
|
|||
>
|
||||
{children}
|
||||
</CSVDownloader>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
|
|
@ -338,6 +353,9 @@ export const RawLogTable = memo(
|
|||
collapseAllRows?: boolean;
|
||||
showExpandButton?: boolean;
|
||||
renderRowDetails?: (row: Record<string, any>) => 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<HTMLDivElement>(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<any>;
|
||||
|
||||
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 => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, headerIndex) => {
|
||||
const isLast = headerIndex === headerGroup.headers.length - 1;
|
||||
return (
|
||||
<th
|
||||
className="overflow-hidden text-truncate bg-hdx-dark"
|
||||
<TableHeader
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
style={{
|
||||
width:
|
||||
header.getSize() === UNDEFINED_WIDTH
|
||||
? '100%'
|
||||
: header.getSize(),
|
||||
// Allow unknown width columns to shrink to 0
|
||||
minWidth:
|
||||
header.getSize() === UNDEFINED_WIDTH
|
||||
? 0
|
||||
: header.getSize(),
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{header.column.getCanResize() &&
|
||||
headerIndex !== headerGroup.headers.length - 1 && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={`resizer text-gray-600 cursor-col-resize ${
|
||||
header.column.getIsResizing() ? 'isResizing' : ''
|
||||
}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 4,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 12,
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-three-dots-vertical" />
|
||||
</div>
|
||||
)}
|
||||
{headerIndex === headerGroup.headers.length - 1 && (
|
||||
<div
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
{tableId != null &&
|
||||
header={header}
|
||||
isLast={isLast}
|
||||
lastItemButtons={
|
||||
<>
|
||||
{tableId &&
|
||||
Object.keys(columnSizeStorage).length > 0 && (
|
||||
<div
|
||||
className="fs-8 text-muted-hover disabled"
|
||||
|
|
@ -778,7 +772,6 @@ export const RawLogTable = memo(
|
|||
)}
|
||||
<UnstyledButton
|
||||
onClick={() => setWrapLinesEnabled(prev => !prev)}
|
||||
className="ms-2"
|
||||
>
|
||||
<MantineTooltip label="Wrap lines">
|
||||
<i className="bi bi-text-wrap" />
|
||||
|
|
@ -788,7 +781,7 @@ export const RawLogTable = memo(
|
|||
<CsvExportButton
|
||||
data={csvData}
|
||||
filename={`hyperdx_search_results_${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`}
|
||||
className="fs-6 text-muted-hover ms-2"
|
||||
className="fs-6 text-muted-hover "
|
||||
>
|
||||
<MantineTooltip
|
||||
label={`Download table as CSV (max ${maxRows.toLocaleString()} rows)${isLimited ? ' - data truncated' : ''}`}
|
||||
|
|
@ -798,16 +791,16 @@ export const RawLogTable = memo(
|
|||
</CsvExportButton>
|
||||
{onSettingsClick != null && (
|
||||
<div
|
||||
className="fs-8 text-muted-hover ms-2"
|
||||
className="fs-8 text-muted-hover"
|
||||
role="button"
|
||||
onClick={() => onSettingsClick()}
|
||||
>
|
||||
<i className="bi bi-gear-fill" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
|
|
@ -998,7 +991,7 @@ export const RawLogTable = memo(
|
|||
) : hasNextPage == false &&
|
||||
isLoading == false &&
|
||||
dedupedRows.length === 0 ? (
|
||||
<div className="my-3">
|
||||
<div className="my-3" data-testid="db-row-table-no-results">
|
||||
No results found.
|
||||
<Text mt="sm" c="gray.3">
|
||||
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<SortingState[number] | null>(
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 <div className="p-3 text-muted">Loading...</div>;
|
||||
|
|
|
|||
102
packages/app/src/components/DBTable/TableHeader.tsx
Normal file
102
packages/app/src/components/DBTable/TableHeader.tsx
Normal file
|
|
@ -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<any, any>;
|
||||
lastItemButtons?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<th
|
||||
className="overflow-hidden bg-hdx-dark"
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
style={{
|
||||
width: header.getSize() === UNDEFINED_WIDTH ? '100%' : header.getSize(),
|
||||
// Allow unknown width columns to shrink to 0
|
||||
minWidth: header.getSize() === UNDEFINED_WIDTH ? 0 : header.getSize(),
|
||||
}}
|
||||
>
|
||||
<Group wrap="nowrap" gap={0} align="center">
|
||||
{!header.column.getCanSort() ? (
|
||||
<Text truncate="end" size="xs" flex="1">
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</Text>
|
||||
) : (
|
||||
<Button
|
||||
size="xxs"
|
||||
p={1}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
flex="1"
|
||||
justify="space-between"
|
||||
data-testid="raw-log-table-sort-button"
|
||||
>
|
||||
<>
|
||||
{header.isPlaceholder ? null : (
|
||||
<Text truncate="end" size="xs" flex="1" c="white">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{header.column.getIsSorted() && (
|
||||
<div
|
||||
data-testid="raw-log-table-sort-indicator"
|
||||
className={
|
||||
header.column.getIsSorted() === 'asc'
|
||||
? 'sorted-asc'
|
||||
: 'sorted-desc'
|
||||
}
|
||||
>
|
||||
<>
|
||||
{header.column.getIsSorted() === 'asc' ? (
|
||||
<IconArrowUp size={12} />
|
||||
) : (
|
||||
<IconArrowDown size={12} />
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Group gap={0} wrap="nowrap" align="center">
|
||||
{header.column.getCanResize() && !isLast && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={cx(
|
||||
`resizer text-gray-600 cursor-col-resize`,
|
||||
header.column.getIsResizing() && 'isResizing',
|
||||
)}
|
||||
>
|
||||
<IconDotsVertical size={12} />
|
||||
</div>
|
||||
)}
|
||||
{isLast && (
|
||||
<Group gap={2} wrap="nowrap">
|
||||
{lastItemButtons}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<SortingState>([]);
|
||||
|
||||
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 ? (
|
||||
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
|
||||
|
|
@ -101,6 +130,8 @@ export default function DBTableChart({
|
|||
data={data?.data ?? []}
|
||||
columns={columns}
|
||||
getRowSearchLink={getRowSearchLink}
|
||||
sorting={sort}
|
||||
onSortingChange={setSort}
|
||||
tableBottom={
|
||||
hasNextPage && (
|
||||
<Text ref={fetchMoreRef} ta="center">
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ export const createExpandButtonColumn = (
|
|||
},
|
||||
size: 32,
|
||||
enableResizing: false,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
className: 'text-center',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<RawLogTable
|
||||
displayedColumns={['col1', 'col2']}
|
||||
rows={[]}
|
||||
isLoading={false}
|
||||
dedupRows={false}
|
||||
hasNextPage={false}
|
||||
onRowDetailsClick={() => {}}
|
||||
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(<RawLogTable {...baseProps} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('raw-log-table-sort-button'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should allow changing sort', async () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
renderWithMantine(
|
||||
<RawLogTable {...baseProps} enableSorting onSortingChange={callback} />,
|
||||
);
|
||||
|
||||
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(
|
||||
<RawLogTable
|
||||
{...baseProps}
|
||||
enableSorting
|
||||
sortOrder={[
|
||||
{
|
||||
desc: false,
|
||||
id: 'col1',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<RawLogTable {...baseProps} enableSorting onSortingChange={callback} />,
|
||||
);
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string>({
|
|||
parse: value => value.replace(/%0A/g, '\n'),
|
||||
serialize: value => value.replace(/\n/g, '%0A'),
|
||||
});
|
||||
|
||||
export const parseAsSortingStateString = createParser<SortingState[number]>({
|
||||
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'}`;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in a new issue