diff --git a/web/default/src/components/data-table/hooks/use-data-table.ts b/web/default/src/components/data-table/hooks/use-data-table.ts index 5b02ac90..bcd0a613 100644 --- a/web/default/src/components/data-table/hooks/use-data-table.ts +++ b/web/default/src/components/data-table/hooks/use-data-table.ts @@ -55,6 +55,7 @@ type DataTableStateOptions = { sorting?: SortingState onSortingChange?: OnChangeFn initialColumnVisibility?: VisibilityState + columnVisibilityStorageKey?: string | false columnVisibility?: VisibilityState onColumnVisibilityChange?: OnChangeFn initialRowSelection?: RowSelectionState @@ -122,6 +123,32 @@ function useControllableTableState( return [value, setValue] } +function readColumnVisibility(storageKey: string | undefined): VisibilityState { + if (!storageKey || typeof window === 'undefined') return {} + + try { + const raw = window.localStorage.getItem(storageKey) + if (!raw) return {} + + const parsed = JSON.parse(raw) as unknown + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return {} + } + + return Object.entries(parsed).reduce( + (visibility, [key, value]) => { + if (typeof value === 'boolean') { + visibility[key] = value + } + return visibility + }, + {} + ) + } catch { + return {} + } +} + export function useDataTable(options: UseDataTableOptions) { const { data, @@ -144,6 +171,18 @@ export function useDataTable(options: UseDataTableOptions) { withExpandedRowModel = false, } = options + const columnVisibilityStorageKey = + typeof options.columnVisibilityStorageKey === 'string' + ? options.columnVisibilityStorageKey + : undefined + const resolvedInitialColumnVisibility = React.useMemo( + () => ({ + ...initialColumnVisibility, + ...readColumnVisibility(columnVisibilityStorageKey), + }), + [columnVisibilityStorageKey, initialColumnVisibility] + ) + const [sorting, onSortingChange] = useControllableTableState( options.sorting, initialSorting, @@ -152,9 +191,13 @@ export function useDataTable(options: UseDataTableOptions) { const [columnVisibility, onColumnVisibilityChange] = useControllableTableState( options.columnVisibility, - initialColumnVisibility, + resolvedInitialColumnVisibility, options.onColumnVisibilityChange ) + const hydratedColumnVisibilityStorageKeyRef = React.useRef( + columnVisibilityStorageKey + ) + const skipNextColumnVisibilityPersistRef = React.useRef(false) const [rowSelection, onRowSelectionChange] = useControllableTableState( options.rowSelection, initialRowSelection, @@ -228,6 +271,43 @@ export function useDataTable(options: UseDataTableOptions) { ensurePageInRange?.(actualPageCount) }, [actualPageCount, ensurePageInRange]) + React.useEffect(() => { + if ( + options.columnVisibility !== undefined || + columnVisibilityStorageKey === + hydratedColumnVisibilityStorageKeyRef.current + ) { + return + } + + hydratedColumnVisibilityStorageKeyRef.current = columnVisibilityStorageKey + skipNextColumnVisibilityPersistRef.current = true + onColumnVisibilityChange(() => resolvedInitialColumnVisibility) + }, [ + columnVisibilityStorageKey, + onColumnVisibilityChange, + options.columnVisibility, + resolvedInitialColumnVisibility, + ]) + + React.useEffect(() => { + if (!columnVisibilityStorageKey || typeof window === 'undefined') return + + if (skipNextColumnVisibilityPersistRef.current) { + skipNextColumnVisibilityPersistRef.current = false + return + } + + try { + window.localStorage.setItem( + columnVisibilityStorageKey, + JSON.stringify(columnVisibility) + ) + } catch { + // Storage can be unavailable in private mode; table controls still work. + } + }, [columnVisibility, columnVisibilityStorageKey]) + return { table, } diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx index edfa662f..53f66d48 100644 --- a/web/default/src/features/channels/components/channels-table.tsx +++ b/web/default/src/features/channels/components/channels-table.tsx @@ -55,6 +55,8 @@ import { useChannels } from './channels-provider' import { DataTableBulkActions } from './data-table-bulk-actions' const route = getRouteApi('/_authenticated/channels/') +const CHANNELS_COLUMN_VISIBILITY_STORAGE_KEY = + 'channels:column-visibility' const CHANNEL_SORTABLE_COLUMNS = new Set([ 'id', @@ -268,6 +270,7 @@ export function ChannelsTable() { models: false, tag: false, }, + columnVisibilityStorageKey: CHANNELS_COLUMN_VISIBILITY_STORAGE_KEY, columnFilters, pagination, globalFilter, diff --git a/web/default/src/features/keys/components/api-keys-table.tsx b/web/default/src/features/keys/components/api-keys-table.tsx index 5375fd8a..3378d084 100644 --- a/web/default/src/features/keys/components/api-keys-table.tsx +++ b/web/default/src/features/keys/components/api-keys-table.tsx @@ -57,6 +57,8 @@ import { DataTableBulkActions } from './data-table-bulk-actions' import { DataTableRowActions } from './data-table-row-actions' const route = getRouteApi('/_authenticated/keys/') +const API_KEYS_COLUMN_VISIBILITY_STORAGE_KEY = + 'api-keys:column-visibility' function isDisabledApiKeyRow(apiKey: ApiKey) { return apiKey.status !== API_KEY_STATUS.ENABLED @@ -264,6 +266,7 @@ export function ApiKeysTable() { columns, enableRowSelection: true, columnFilters, + columnVisibilityStorageKey: API_KEYS_COLUMN_VISIBILITY_STORAGE_KEY, globalFilter, pagination, globalFilterFn: () => true, diff --git a/web/default/src/features/usage-logs/components/usage-logs-table.tsx b/web/default/src/features/usage-logs/components/usage-logs-table.tsx index fcdf79b7..63e5d636 100644 --- a/web/default/src/features/usage-logs/components/usage-logs-table.tsx +++ b/web/default/src/features/usage-logs/components/usage-logs-table.tsx @@ -49,6 +49,13 @@ const logTypeRowTint: Record = { [LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15', } +function getColumnVisibilityStorageKey( + logCategory: LogCategory, + isAdmin: boolean +): string { + return `usage-logs:${logCategory}:${isAdmin ? 'admin' : 'user'}:column-visibility` +} + function deserializeLogTypeFilter(value: unknown): unknown[] { const values = Array.isArray(value) ? value : value ? [value] : [] return values.filter((item) => String(item) !== LOG_TYPE_ALL_VALUE) @@ -146,6 +153,10 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) { data: logs as Record[], columns: columns as ColumnDef>[], columnFilters, + columnVisibilityStorageKey: getColumnVisibilityStorageKey( + logCategory, + isAdmin + ), pagination, enableRowSelection: false, onPaginationChange,