diff --git a/.changeset/great-papayas-obey.md b/.changeset/great-papayas-obey.md new file mode 100644 index 0000000000..6f4b2c8f49 --- /dev/null +++ b/.changeset/great-papayas-obey.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +fix: persist column widths in search results table diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index ad6de5f7f2..7ed4f2d3a7 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -1314,6 +1314,14 @@ export function DBSearchPage() { }; }, [chartConfig, searchedTimeRange]); + // Stable key for persisting column widths in localStorage. Scoped per saved + // search when one is loaded, else per source for ad-hoc searches. + const columnSizeTableId = savedSearchId + ? `db-search-saved-${savedSearchId}` + : searchedConfig.source + ? `db-search-source-${searchedConfig.source}` + : undefined; + const displayedColumns = useMemo(() => { // `select` is typed as `string | DerivedColumn[]` upstream, but in the // search page we always supply a string. Guard for type safety. @@ -2284,6 +2292,7 @@ export function DBSearchPage() { context={rowTableContext} config={dbSqlRowTableConfig} sourceId={searchedConfig.source} + tableId={columnSizeTableId} onSidebarOpen={onSidebarOpen} onExpandedRowsChange={onExpandedRowsChange} enabled={isReady} diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 5b4b297cbe..9d2c8c31e6 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1530,6 +1530,7 @@ function DBSqlRowTableComponent({ initialSortBy, variant = 'default', enableSmallFirstWindow, + tableId, }: { config: BuilderChartConfigWithDateRange; sourceId?: string; @@ -1554,6 +1555,7 @@ function DBSqlRowTableComponent({ onSortingChange?: (v: SortingState | null) => void; variant?: DBRowTableVariant; enableSmallFirstWindow?: boolean; + tableId?: string; }) { const { data: me } = api.useMe(); const { toggleColumn, displayedColumns: contextDisplayedColumns } = @@ -1863,6 +1865,7 @@ function DBSqlRowTableComponent({ getRowWhere={getRowWhere} variant={variant} onRemoveColumn={toggleColumn ? onRemoveColumnFromTable : undefined} + tableId={tableId} /> ); diff --git a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx index 718a43cbd5..083205df77 100644 --- a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx +++ b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx @@ -43,6 +43,7 @@ interface Props { initialSortBy?: SortingState; variant?: DBRowTableVariant; enableSmallFirstWindow?: boolean; + tableId?: string; } export default function DBSqlRowTableWithSideBar({ @@ -63,6 +64,7 @@ export default function DBSqlRowTableWithSideBar({ initialSortBy, variant, enableSmallFirstWindow, + tableId, }: Props) { const { data: sourceData } = useSource({ id: sourceId }); const [rowId, setRowId] = useQueryState('rowWhere', parseAsStringEncoded); @@ -142,6 +144,7 @@ export default function DBSqlRowTableWithSideBar({ collapseAllRows={collapseAllRows} variant={variant} enableSmallFirstWindow={enableSmallFirstWindow} + tableId={tableId} /> ); diff --git a/packages/app/src/components/__tests__/DBRowTable.test.tsx b/packages/app/src/components/__tests__/DBRowTable.test.tsx index 406148bf99..ffb459b2f5 100644 --- a/packages/app/src/components/__tests__/DBRowTable.test.tsx +++ b/packages/app/src/components/__tests__/DBRowTable.test.tsx @@ -143,6 +143,60 @@ describe('RawLogTable', () => { ]); }); }); + + describe('Column width persistence', () => { + const baseProps = { + displayedColumns: ['col1', 'col2'], + rows: [{ col1: 'value1', col2: 'value2' }], + isLoading: false, + dedupRows: false, + hasNextPage: false, + onRowDetailsClick: () => {}, + generateRowId: () => mockRowWhereResult, + columnTypeMap: new Map(), + showExpandButton: false, + }; + + beforeEach(() => { + window.localStorage.clear(); + }); + + it('applies stored column width when tableId is provided', () => { + // useLocalStorage stringifies under the same key the component reads. + window.localStorage.setItem( + 't1-column-sizes', + JSON.stringify({ col1: 250 }), + ); + + const { container } = renderWithMantine( + , + ); + + // Two rendered: col1 (non-last, takes stored width) and col2 (last, + // takes remaining viewport width). Only col1 has a stored size to check. + const headers = container.querySelectorAll('th'); + expect(headers).toHaveLength(2); + expect((headers[0] as HTMLElement).style.width).toBe('250px'); + }); + + it('isolates stored widths per tableId', () => { + // A different table's stored widths must not leak into a different + // scope — this is what guarantees per-saved-search / per-source + // isolation when widths are persisted across the app. + window.localStorage.setItem( + 't1-column-sizes', + JSON.stringify({ col1: 250 }), + ); + + const { container } = renderWithMantine( + , + ); + + const headers = container.querySelectorAll('th'); + expect(headers).toHaveLength(2); + expect((headers[0] as HTMLElement).style.width).not.toBe('250px'); + }); + }); }); describe('appendSelectWithAdditionalKeys', () => {