mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 09:50:33 -04:00
b2e5476146
## 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>
235 lines
8.5 KiB
TypeScript
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>
|
|
)
|
|
}
|