Files
supabase/apps/studio/components/ui/DataTable/DataTableInfinite.tsx
kemal.earth b2e5476146 feat(studio): tidy up bottom tray in logs interface (#45371)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Tidying up the bottom panel in unified logs. Taking care of some visual
quirks etc. Also preparing this area to house some other future concepts
via tabs.

| Before | After |
|--------|--------|
| <img width="828" height="384" alt="Screenshot 2026-04-30 at 11 24 09"
src="https://github.com/user-attachments/assets/804bdf1c-7cdb-4dd8-bf1e-31c434ef1436"
/> | <img width="830" height="407" alt="Screenshot 2026-04-30 at 11 22
53"
src="https://github.com/user-attachments/assets/28555efe-f893-4bae-bcb0-284e6db733e6"
/> |




<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Redesigned service flow panel with Overview and Raw JSON tabs
* Added Previous/Next navigation controls with Arrow Up/Down keyboard
support
* New detail components and section headers with icons for clearer
organization
  * Improved Postgres detail view and message/session display

* **Bug Fixes / Changes**
  * Removed legacy header UI and related controls

* **UI / Style**
  * Enhanced copy-to-clipboard feedback animation
  * Updated "Load more" button styling
  * Adjusted panel sizing for improved resizing behavior
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-05-06 16:32:31 +01:00

235 lines
8.5 KiB
TypeScript

import { type FetchNextPageOptions } from '@tanstack/react-query'
import type { ColumnDef, Row, Table as TTable, VisibilityState } from '@tanstack/react-table'
import { flexRender } from '@tanstack/react-table'
import { LoaderCircle } from 'lucide-react'
import { useQueryState } from 'nuqs'
import { Fragment, ReactNode, UIEvent, useCallback, useRef } from 'react'
import { Button, cn } from 'ui'
import { formatCompactNumber } from './DataTable.utils'
import { useDataTable } from './providers/DataTableProvider'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './Table'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
// TODO: add a possible chartGroupBy
export interface DataTableInfiniteProps<TData, TValue, _TMeta> {
columns: ColumnDef<TData, TValue>[]
defaultColumnVisibility?: VisibilityState
totalRows?: number
filterRows?: number
totalRowsFetched?: number
isFetching?: boolean
isLoading?: boolean
hasNextPage?: boolean
fetchNextPage: (options?: FetchNextPageOptions | undefined) => Promise<unknown>
renderLiveRow?: (props?: { row: Row<TData> }) => ReactNode
setColumnOrder: (columnOrder: string[]) => void
setColumnVisibility: (columnVisibility: VisibilityState) => void
// [Joshen] See if we can type this properly
searchParamsParser: any
}
// [Joshen] JFYI this component is NOT virtualized and hence will struggle handling many data points
export function DataTableInfinite<TData, TValue, TMeta>({
columns,
defaultColumnVisibility = {},
fetchNextPage,
hasNextPage,
totalRows = 0,
filterRows = 0,
totalRowsFetched = 0,
renderLiveRow,
setColumnOrder,
setColumnVisibility,
searchParamsParser,
}: DataTableInfiniteProps<TData, TValue, TMeta>) {
const { table, isLoading, isFetching } = useDataTable()
const tableRef = useRef<HTMLTableElement>(null)
const headerGroups = table.getHeaderGroups()
const headers = headerGroups[0].headers
const rows = table.getRowModel().rows ?? []
const onScroll = useCallback(
(e: UIEvent<HTMLElement>) => {
const onPageBottom =
Math.ceil(e.currentTarget.scrollTop + e.currentTarget.clientHeight) >=
e.currentTarget.scrollHeight
if (onPageBottom && !isFetching && totalRows > totalRowsFetched) {
fetchNextPage()
}
},
[fetchNextPage, isFetching, totalRows, totalRowsFetched]
)
useShortcut(SHORTCUT_IDS.DATA_TABLE_RESET_COLUMNS, () => {
setColumnOrder([])
setColumnVisibility(defaultColumnVisibility)
})
return (
<Table ref={tableRef} onScroll={onScroll}>
<TableHeader>
<TableRow className="bg-surface-75">
{headers.map((header) => {
const sort = header.column.getIsSorted()
const canResize = header.column.getCanResize()
const onResize = header.getResizeHandler()
const headerClassName = (header.column.columnDef.meta as any)?.headerClassName
return (
<TableHead
key={header.id}
className={headerClassName}
aria-sort={sort === 'asc' ? 'ascending' : sort === 'desc' ? 'descending' : 'none'}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{canResize && (
<div
onDoubleClick={() => header.column.resetSize()}
onMouseDown={onResize}
onTouchStart={onResize}
className={cn(
'user-select-none absolute -right-2 top-0 z-10 flex h-full w-4 cursor-col-resize touch-none justify-center',
'before:absolute before:inset-y-0 before:w-px before:translate-x-px before:bg-border'
)}
/>
)}
</TableHead>
)
})}
</TableRow>
</TableHeader>
<TableBody
id="content"
tabIndex={-1}
// REMINDER: avoids scroll (skipping the table header) when using skip to content
style={{ scrollMarginTop: 'calc(var(--top-bar-height))' }}
>
{rows.length ? (
rows.map((row) => (
// REMINDER: if we want to add arrow navigation https://github.com/TanStack/table/discussions/2752#discussioncomment-192558
<Fragment key={row.id}>
{renderLiveRow?.({ row: row as any })}
<DataTableRow
row={row}
table={table}
searchParamsParser={searchParamsParser}
selected={row.getIsSelected()}
/>
</Fragment>
))
) : (
<Fragment>
{renderLiveRow?.()}
<TableRow>
<TableCell colSpan={columns.length} className="h-[32vh] text-center">
<div className="flex flex-col items-center justify-center h-full gap-3">
{isLoading ? (
<>
<LoaderCircle className="h-6 w-6 animate-spin text-foreground-muted" />
<p className="text-foreground-light text-sm">Retrieving logs...</p>
</>
) : (
<p className="text-foreground-light text-sm">No results found</p>
)}
</div>
</TableCell>
</TableRow>
</Fragment>
)}
{/* Only show load more section if we have rows OR if we're not in initial loading state */}
{(rows.length > 0 || (!isLoading && !rows.length)) && (
<TableRow className="hover:bg-transparent data-[state=selected]:bg-transparent">
<TableCell colSpan={columns.length} className="text-center py-2!">
{hasNextPage || isFetching ? (
<div className="flex flex-col items-center gap-2">
<Button
disabled={isFetching}
onClick={() => fetchNextPage()}
size="small"
type="default"
icon={
isFetching ? <LoaderCircle className="mr-2 h-4 w-4 animate-spin" /> : null
}
>
Load more
</Button>
<p className="text-xs text-foreground-lighter">
Showing{' '}
<span className="font-mono font-medium">
{formatCompactNumber(totalRowsFetched)}
</span>{' '}
of{' '}
<span className="font-mono font-medium">{formatCompactNumber(totalRows)}</span>{' '}
rows
</p>
</div>
) : (
<p className="text-xs text-foreground-lighter">
No more data to load (
<span className="font-mono font-medium">{formatCompactNumber(filterRows)}</span>{' '}
of <span className="font-mono font-medium">{formatCompactNumber(totalRows)}</span>{' '}
rows)
</p>
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}
/**
* REMINDER: this is the heaviest component in the table if lots of rows
* Some other components are rendered more often necessary, but are fixed size (not like rows that can grow in height)
* e.g. DataTableFilterControls, DataTableFilterCommand, DataTableToolbar, DataTableHeader
*/
function DataTableRow<TData>({
row,
table,
selected,
searchParamsParser,
}: {
row: Row<TData>
table: TTable<TData>
selected?: boolean
searchParamsParser: any
}) {
useQueryState('live', searchParamsParser.live)
const rowClassName = (table.options.meta as any)?.getRowClassName?.(row)
const cells = row.getVisibleCells()
return (
<TableRow
id={row.id}
tabIndex={0}
data-state={selected && 'selected'}
onClick={() => row.toggleSelected()}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault()
row.toggleSelected()
}
}}
className={cn(rowClassName)}
>
{cells.map((cell) => {
const cellClassName = (cell.column.columnDef.meta as any)?.cellClassName
return (
<TableCell key={cell.id} className={cn(cellClassName)}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
)
})}
</TableRow>
)
}