From 7445bbf96fede16b45b6a774781e8e29953ae3ba Mon Sep 17 00:00:00 2001 From: Drew Davis Date: Fri, 17 Oct 2025 14:24:05 -0400 Subject: [PATCH] feat: Include displayed timestamp in default order by --- .changeset/twelve-beers-buy.md | 5 + packages/app/src/DBSearchPage.tsx | 48 ++++++-- .../app/src/__tests__/DBSearchPage.test.tsx | 113 +++++++++++++++--- packages/app/src/components/SourceForm.tsx | 17 ++- 4 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 .changeset/twelve-beers-buy.md diff --git a/.changeset/twelve-beers-buy.md b/.changeset/twelve-beers-buy.md new file mode 100644 index 000000000..9ec985094 --- /dev/null +++ b/.changeset/twelve-beers-buy.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +feat: Include displayed timestamp in default order by diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index a13dc9a26..0b0462143 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -532,14 +532,26 @@ function useSearchedConfigToChartConfig({ function optimizeDefaultOrderBy( timestampExpr: string, + displayedTimestampExpr: string | undefined, sortingKey: string | undefined, ) { const defaultModifier = 'DESC'; - const fallbackOrderByItems = [ - getFirstTimestampValueExpression(timestampExpr ?? ''), - defaultModifier, - ]; - const fallbackOrderBy = fallbackOrderByItems.join(' '); + const firstTimestampValueExpression = + getFirstTimestampValueExpression(timestampExpr ?? '') ?? ''; + const defaultOrderByItems = [firstTimestampValueExpression]; + const trimmedDisplayedTimestampExpr = displayedTimestampExpr?.trim(); + + if ( + trimmedDisplayedTimestampExpr && + trimmedDisplayedTimestampExpr !== firstTimestampValueExpression + ) { + defaultOrderByItems.push(trimmedDisplayedTimestampExpr); + } + + const fallbackOrderBy = + defaultOrderByItems.length > 1 + ? `(${defaultOrderByItems.join(', ')}) ${defaultModifier}` + : `${defaultOrderByItems[0]} ${defaultModifier}`; if (!sortingKey) return fallbackOrderBy; @@ -547,13 +559,17 @@ function optimizeDefaultOrderBy( const sortKeys = splitAndTrimWithBracket(sortingKey); for (let i = 0; i < sortKeys.length; i++) { const sortKey = sortKeys[i]; - if (sortKey.includes('toStartOf') && sortKey.includes(timestampExpr)) { + if ( + sortKey.includes('toStartOf') && + sortKey.includes(firstTimestampValueExpression) + ) { orderByArr.push(sortKey); } else if ( - sortKey === timestampExpr || + sortKey === firstTimestampValueExpression || (sortKey.startsWith('toUnixTimestamp') && - sortKey.includes(timestampExpr)) || - (sortKey.startsWith('toDateTime') && sortKey.includes(timestampExpr)) + sortKey.includes(firstTimestampValueExpression)) || + (sortKey.startsWith('toDateTime') && + sortKey.includes(firstTimestampValueExpression)) ) { if (orderByArr.length === 0) { // fallback if the first sort key is the timestamp sort key @@ -562,6 +578,8 @@ function optimizeDefaultOrderBy( orderByArr.push(sortKey); break; } + } else if (sortKey === trimmedDisplayedTimestampExpr) { + orderByArr.push(sortKey); } } @@ -570,7 +588,16 @@ function optimizeDefaultOrderBy( return fallbackOrderBy; } - return `(${orderByArr.join(', ')}) ${defaultModifier}`; + if ( + trimmedDisplayedTimestampExpr && + !orderByArr.includes(trimmedDisplayedTimestampExpr) + ) { + orderByArr.push(trimmedDisplayedTimestampExpr); + } + + return orderByArr.length > 1 + ? `(${orderByArr.join(', ')}) ${defaultModifier}` + : `${orderByArr[0]} ${defaultModifier}`; } export function useDefaultOrderBy(sourceID: string | undefined | null) { @@ -582,6 +609,7 @@ export function useDefaultOrderBy(sourceID: string | undefined | null) { () => optimizeDefaultOrderBy( source?.timestampValueExpression ?? '', + source?.displayedTimestampValueExpression, tableMetadata?.sorting_key, ), [source, tableMetadata], diff --git a/packages/app/src/__tests__/DBSearchPage.test.tsx b/packages/app/src/__tests__/DBSearchPage.test.tsx index 9e6e5475e..f29009797 100644 --- a/packages/app/src/__tests__/DBSearchPage.test.tsx +++ b/packages/app/src/__tests__/DBSearchPage.test.tsx @@ -17,82 +17,157 @@ describe('useDefaultOrderBy', () => { describe('optimizeOrderBy function', () => { describe('should handle', () => { - const mockSource = { - timestampValueExpression: 'Timestamp', - }; - const testCases = [ { - input: undefined, + sortingKey: undefined, expected: 'Timestamp DESC', }, { - input: '', + sortingKey: '', expected: 'Timestamp DESC', }, { // Traces Table - input: 'ServiceName, SpanName, toDateTime(Timestamp)', + sortingKey: 'ServiceName, SpanName, toDateTime(Timestamp)', expected: 'Timestamp DESC', }, { // Optimized Traces Table - input: + sortingKey: 'toStartOfHour(Timestamp), ServiceName, SpanName, toDateTime(Timestamp)', expected: '(toStartOfHour(Timestamp), toDateTime(Timestamp)) DESC', }, { // Unsupported for now as it's not a great sort key, want to just // use default behavior for this - input: 'toDateTime(Timestamp), ServiceName, SpanName, Timestamp', + sortingKey: 'toDateTime(Timestamp), ServiceName, SpanName, Timestamp', expected: 'Timestamp DESC', }, { // Unsupported prefix sort key - input: 'toDateTime(Timestamp), ServiceName, SpanName', + sortingKey: 'toDateTime(Timestamp), ServiceName, SpanName', expected: 'Timestamp DESC', }, { // Inverted sort key order, we should not try to optimize this - input: + sortingKey: 'ServiceName, toDateTime(Timestamp), SeverityText, toStartOfHour(Timestamp)', expected: 'Timestamp DESC', }, { - input: 'toStartOfHour(Timestamp), other_column, Timestamp', + sortingKey: 'toStartOfHour(Timestamp), other_column, Timestamp', expected: '(toStartOfHour(Timestamp), Timestamp) DESC', }, { - input: 'Timestamp, other_column', + sortingKey: 'Timestamp, other_column', expected: 'Timestamp DESC', }, { - input: 'user_id, toStartOfHour(Timestamp), status, Timestamp', + sortingKey: 'user_id, toStartOfHour(Timestamp), status, Timestamp', expected: '(toStartOfHour(Timestamp), Timestamp) DESC', }, { - input: + sortingKey: 'toStartOfMinute(Timestamp), user_id, status, toUnixTimestamp(Timestamp)', expected: '(toStartOfMinute(Timestamp), toUnixTimestamp(Timestamp)) DESC', }, { // test variation of toUnixTimestamp - input: + sortingKey: 'toStartOfMinute(Timestamp), user_id, status, toUnixTimestamp64Nano(Timestamp)', expected: '(toStartOfMinute(Timestamp), toUnixTimestamp64Nano(Timestamp)) DESC', }, { - input: + sortingKey: 'toUnixTimestamp(toStartOfMinute(Timestamp)), user_id, status, Timestamp', expected: '(toUnixTimestamp(toStartOfMinute(Timestamp)), Timestamp) DESC', }, + { + sortingKey: 'toStartOfMinute(Timestamp), user_id, status, Timestamp', + timestampValueExpression: 'Timestamp, toStartOfMinute(Timestamp)', + expected: '(toStartOfMinute(Timestamp), Timestamp) DESC', + }, + { + sortingKey: 'Timestamp', + displayedTimestampValueExpression: 'Timestamp64', + expected: '(Timestamp, Timestamp64) DESC', + }, + { + sortingKey: 'Timestamp', + displayedTimestampValueExpression: 'Timestamp64 ', + expected: '(Timestamp, Timestamp64) DESC', + }, + { + sortingKey: 'Timestamp', + displayedTimestampValueExpression: 'Timestamp', + expected: 'Timestamp DESC', + }, + { + sortingKey: 'Timestamp', + displayedTimestampValueExpression: '', + expected: 'Timestamp DESC', + }, + { + sortingKey: 'Timestamp, ServiceName, Timestamp64', + displayedTimestampValueExpression: 'Timestamp64', + expected: '(Timestamp, Timestamp64) DESC', + }, + { + sortingKey: + 'toStartOfMinute(Timestamp), Timestamp, ServiceName, Timestamp64', + displayedTimestampValueExpression: 'Timestamp64', + expected: '(toStartOfMinute(Timestamp), Timestamp, Timestamp64) DESC', + }, + { + sortingKey: + 'toStartOfMinute(Timestamp), Timestamp64, ServiceName, Timestamp', + displayedTimestampValueExpression: 'Timestamp64', + expected: '(toStartOfMinute(Timestamp), Timestamp64, Timestamp) DESC', + }, + { + sortingKey: 'SomeOtherTimeColumn', + displayedTimestampValueExpression: 'Timestamp64', + expected: '(Timestamp, Timestamp64) DESC', + }, + { + sortingKey: '', + displayedTimestampValueExpression: 'Timestamp64', + expected: '(Timestamp, Timestamp64) DESC', + }, + { + sortingKey: 'ServiceName, TimestampTime, Timestamp', + timestampValueExpression: 'TimestampTime, Timestamp', + displayedTimestampValueExpression: 'Timestamp', + expected: '(TimestampTime, Timestamp) DESC', + }, + { + sortingKey: 'ServiceName, TimestampTime, Timestamp', + timestampValueExpression: 'Timestamp, TimestampTime', + displayedTimestampValueExpression: 'Timestamp', + expected: 'Timestamp DESC', + }, + { + sortingKey: '', + timestampValueExpression: 'Timestamp, TimestampTime', + displayedTimestampValueExpression: '', + expected: 'Timestamp DESC', + }, ]; for (const testCase of testCases) { - it(`${testCase.input}`, () => { - const mockTableMetadata = { sorting_key: testCase.input }; + it(`${testCase.sortingKey}`, () => { + const mockSource = { + timestampValueExpression: + testCase.timestampValueExpression || 'Timestamp', + displayedTimestampValueExpression: + testCase.displayedTimestampValueExpression, + }; + + const mockTableMetadata = { + sorting_key: testCase.sortingKey, + }; jest.spyOn(sourceModule, 'useSource').mockReturnValue({ data: mockSource, diff --git a/packages/app/src/components/SourceForm.tsx b/packages/app/src/components/SourceForm.tsx index 98fe39a22..184aca33e 100644 --- a/packages/app/src/components/SourceForm.tsx +++ b/packages/app/src/components/SourceForm.tsx @@ -285,7 +285,7 @@ export function LogTableModelForm({ control, watch }: TableModelProps) { + + + ); }