diff --git a/web/src/__tests__/numeric-comparator.spec.ts b/web/src/__tests__/numeric-comparator.spec.ts new file mode 100644 index 000000000..e978eb737 --- /dev/null +++ b/web/src/__tests__/numeric-comparator.spec.ts @@ -0,0 +1,72 @@ +import { numericComparator } from '../sort-utils'; + +describe('numericComparator', () => { + it('should return -1 when a < b with positive multiplier', () => { + expect(numericComparator(1, 2, 1)).toBe(-1); + }); + + it('should return 1 when a > b with positive multiplier', () => { + expect(numericComparator(2, 1, 1)).toBe(1); + }); + + it('should return 0 when a === b', () => { + expect(numericComparator(1, 1, 1)).toBe(0); + }); + + it('should invert result with negative multiplier (descending sort)', () => { + expect(numericComparator(1, 2, -1)).toBe(1); + expect(numericComparator(2, 1, -1)).toBe(-1); + }); + + it('should use fallback comparison when values are equal', () => { + expect(numericComparator(5, 5, 1, 3)).toBe(3); + expect(numericComparator(5, 5, 1, -2)).toBe(-2); + }); + + it('should apply direction multiplier to fallback comparison', () => { + expect(numericComparator(5, 5, -1, 3)).toBe(-3); + expect(numericComparator(5, 5, -1, -2)).toBe(2); + }); + + it('should ignore fallback when values are not equal', () => { + expect(numericComparator(1, 2, 1, 100)).toBe(-1); + expect(numericComparator(2, 1, 1, 100)).toBe(1); + }); + + it('should work with timestamps', () => { + const timestamp1 = 1679000000000; + const timestamp2 = 1679000000001; + expect(numericComparator(timestamp1, timestamp2, 1)).toBe(-1); + expect(numericComparator(timestamp2, timestamp1, 1)).toBe(1); + expect(numericComparator(timestamp1, timestamp1, 1)).toBe(0); + }); + + it('should use logIndex as tiebreaker for equal timestamps in ascending sort', () => { + const timestamp = 1679000000000; + const logs = [ + { timestamp, logIndex: 0 }, + { timestamp, logIndex: 1 }, + { timestamp, logIndex: 2 }, + ]; + + const sortedAsc = [...logs].sort((a, b) => + numericComparator(a.timestamp, b.timestamp, 1, a.logIndex - b.logIndex), + ); + expect(sortedAsc.map((l) => l.logIndex)).toEqual([0, 1, 2]); + }); + + it('should use logIndex as tiebreaker for equal timestamps in descending sort', () => { + const timestamp = 1679000000000; + const logs = [ + { timestamp, logIndex: 0 }, + { timestamp, logIndex: 1 }, + { timestamp, logIndex: 2 }, + ]; + + const sortedDesc = [...logs].sort((a, b) => + numericComparator(a.timestamp, b.timestamp, -1, a.logIndex - b.logIndex), + ); + // Direction multiplier is applied to fallback, so order is reversed + expect(sortedDesc.map((l) => l.logIndex)).toEqual([2, 1, 0]); + }); +}); diff --git a/web/src/components/logs-table.tsx b/web/src/components/logs-table.tsx index 3eacb13c2..03b153c16 100644 --- a/web/src/components/logs-table.tsx +++ b/web/src/components/logs-table.tsx @@ -15,6 +15,7 @@ import { } from '../logs.types'; import { parseName, parseResources, ResourceLabel } from '../parse-resources'; import { severityFromString } from '../severity'; +import { numericComparator } from '../sort-utils'; import { TestIds } from '../test-ids'; import { LogDetail } from './log-detail'; import './logs-table.css'; @@ -38,8 +39,6 @@ interface LogsTableProps { schema: Schema; } -type TableCellValue = string | number | Resource | Array; - const isJSONObject = (value: string): boolean => { const trimmedValue = value.trim(); @@ -96,13 +95,6 @@ const getSeverityClass = (severity: string) => { return severity ? `lv-plugin__table__severity-${severity}` : ''; }; -// sort with an appropriate numeric comparator for big floats -const numericComparator = ( - a: T, - b: T, - directionMultiplier: number, -): number => (a < b ? -1 : a > b ? 1 : 0) * directionMultiplier; - const columns: Array> = [ { id: 'expand', @@ -119,7 +111,12 @@ const columns: Array> = [ }, sort: (data, sortDirection) => data.sort((a, b) => - numericComparator(a.timestamp, b.timestamp, sortDirection === 'asc' ? 1 : -1), + numericComparator( + a.timestamp, + b.timestamp, + sortDirection === 'asc' ? 1 : -1, + a.logIndex - b.logIndex, + ), ), }, { @@ -306,7 +303,9 @@ export const LogsTable: React.FC = ({ } } - return tableData.sort((a, b) => numericComparator(a.timestamp, b.timestamp, -1)); + return tableData.sort((a, b) => + numericComparator(a.timestamp, b.timestamp, -1, a.logIndex - b.logIndex), + ); }, [tableData, columns, sortBy]); const dataIsEmpty = sortedData.length === 0; diff --git a/web/src/sort-utils.ts b/web/src/sort-utils.ts new file mode 100644 index 000000000..b9ab819ef --- /dev/null +++ b/web/src/sort-utils.ts @@ -0,0 +1,15 @@ +type SortableValue = string | number; + +// sort with an appropriate numeric comparator for big floats +export const numericComparator = ( + a: T, + b: T, + directionMultiplier: number, + fallbackComparison?: number, +): number => { + const result = a < b ? -1 : a > b ? 1 : 0; + if (result === 0 && fallbackComparison !== undefined) { + return fallbackComparison * directionMultiplier; + } + return result * directionMultiplier; +};