Files
supabase/apps/studio/components/ui/DataTable/DataTableInfinite.tsx
T
Joshen LimandJonathan Summers-Muir f223b455d6 Unified Logs (Part 1) (#36298)
* init new unified page

* moar logs

* init

* add infinite and live logs example

* Update useLogsPreview.tsx

* add more sources

* wrapped auth logs with edge logs

* add role and user id

* move unified logs

* init

* move demo pages. create a new directory to work in

* extracted beta unified logs into own components

* add example base page and components

* add new files to use actual logging query

* more organization

* change import

* adds new logs page. adds new query

* add data table to UI pacakges

* revert

* table styles

* text size

* add timestamp, table, icons for log types, status code styling

* add host

* add log count to edge functions

* starts to add dynamic filtering

* spiking trace UI

* Update status-code.ts

* add new linik

* now using POST

* fix chart data for default 1 hour view

* update API to accept POST requests

* new filters

* Update level.ts

* fixed up chart to work on level filter. split up the logic into new files

* prep for log type

* prepped query for WHERE

* fix: issue with white space in url param column parsing

* level param now being removed correctly.

* fix issue with chart showing wrong buckets for different time ranges

* remove old query

* refactor the queries into function for each source

* total count fixed

* lots of layout

* start fixing log counts

* comment out min and max for a while

* added trace logging prototype in

* random trace logs added for demo

* added logs and ui to view logs if any

* add Auth user

* fix the live logs issue

* some left over code

* Midway

* First pass refactor + clean up + reorganize files

* Fix TS issues

* Remove unused files

* Clean up

* Final clean up

* more clean up

* More clean up

* Remove unused packages

* Fix

* Lint

* Add feature flag for unified logs

* Refactor

* Remove trace UI

* Snake case log types

* more clean up

* More clean up

* Fix ts

* more clean up

* fixes

* add flag check and redirect if flag is false

* Update middleware.ts

* Nit lint

* Fix

* Last refactors

---------

Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
2025-06-20 12:46:17 +08:00

239 lines
8.4 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, memo, ReactNode, UIEvent, useCallback, useRef } from 'react'
import { useHotKey } from 'hooks/ui/useHotKey'
import { Button, cn } from 'ui'
import { formatCompactNumber } from './DataTable.utils'
import { useDataTable } from './providers/DataTableProvider'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './Table'
// 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
}
export function DataTableInfinite<TData, TValue, TMeta>({
columns,
defaultColumnVisibility = {},
isFetching,
isLoading,
fetchNextPage,
hasNextPage,
totalRows = 0,
filterRows = 0,
totalRowsFetched = 0,
renderLiveRow,
setColumnOrder,
setColumnVisibility,
searchParamsParser,
}: DataTableInfiniteProps<TData, TValue, TMeta>) {
const { table } = useDataTable()
const tableRef = useRef<HTMLTableElement>(null)
const headerGroups = table.getHeaderGroups()
const onScroll = useCallback(
(e: UIEvent<HTMLElement>) => {
const onPageBottom =
Math.ceil(e.currentTarget.scrollTop + e.currentTarget.clientHeight) >=
e.currentTarget.scrollHeight
if (onPageBottom && !isFetching && totalRowsFetched < filterRows) {
fetchNextPage()
}
},
[fetchNextPage, isFetching, filterRows, totalRowsFetched]
)
useHotKey(() => {
setColumnOrder([])
setColumnVisibility(defaultColumnVisibility)
}, 'u')
return (
<>
<Table
containerProps={{
onScroll,
}}
ref={tableRef} // REMINDER: https://stackoverflow.com/questions/50361698/border-style-do-not-work-with-sticky-position-element
className="z-0"
>
<TableHeader>
{headerGroups.map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-surface-75">
{headerGroup.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))' }}
>
{table.getRowModel().rows?.length ? (
table.getRowModel().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 })}
<MemoizedRow
row={row}
table={table}
searchParamsParser={searchParamsParser}
selected={row.getIsSelected()}
/>
</Fragment>
))
) : (
<Fragment>
{renderLiveRow?.()}
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
</Fragment>
)}
<TableRow className="hover:bg-transparent data-[state=selected]:bg-transparent">
<TableCell colSpan={columns.length} className="text-center">
{hasNextPage || isFetching || isLoading ? (
<div className="flex flex-col items-center gap-2">
<Button
disabled={isFetching || isLoading}
onClick={() => fetchNextPage()}
size="small"
type="outline"
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-sm text-foreground-lighter py-4">
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
}) {
// REMINDER: rerender the row when live mode is toggled - used to opacity the row
// via the `getRowClassName` prop - but for some reasons it wil render the row on data fetch
useQueryState('live', searchParamsParser.live)
const rowClassName = (table.options.meta as any)?.getRowClassName?.(row)
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)}
>
{row.getVisibleCells().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>
)
}
const MemoizedRow = memo(
DataTableRow,
(prev, next) => prev.row.id === next.row.id && prev.selected === next.selected
) as typeof DataTableRow