11import * as React from "react"
22import { Result } from "@/lib/effect-atom"
3- import {
4- type ColumnDef ,
5- flexRender ,
6- getCoreRowModel ,
7- useReactTable ,
8- } from "@tanstack/react-table"
93import { useVirtualizer } from "@tanstack/react-virtual"
104
115import { Skeleton } from "@maple/ui/components/ui/skeleton"
126import { type Log } from "@/api/tinybird/logs"
13- import { SeverityBadge } from "./severity-badge"
147import { LogDetailSheet } from "./log-detail-sheet"
158import type { LogsSearchParams } from "@/routes/logs"
169import { useTimezonePreference } from "@/hooks/use-timezone-preference"
17- import { formatTimestampInTimezone } from "@/lib/timezone-format"
18- import { formatRelativeTime } from "@/lib/format "
10+ import { formatCompactTimeInTimezone } from "@/lib/timezone-format"
11+ import { getSeverityColor } from "@/lib/severity "
1912import { useInfiniteLogs , FETCH_THRESHOLD } from "@/hooks/use-infinite-logs"
2013
21- function truncateBody ( body : string , maxLength = 100 ) : string {
22- if ( body . length <= maxLength ) return body
23- return body . slice ( 0 , maxLength ) + "..."
24- }
25-
26- const ROW_HEIGHT = 44
14+ const ROW_HEIGHT = 28
2715
2816interface LogsTableProps {
2917 filters ?: LogsSearchParams
3018}
3119
3220function LoadingState ( ) {
3321 return (
34- < div className = "flex-1 min-h-0 flex flex-col gap-4" >
35- < div className = "rounded-md border" >
36- < table className = "w-full caption-bottom text-sm" >
37- < thead className = "[&_tr]:border-b" >
38- < tr className = "border-b transition-colors hover:bg-muted/50" >
39- < th className = "h-10 px-2 text-left align-middle font-medium text-muted-foreground w-[160px]" > Timestamp</ th >
40- < th className = "h-10 px-2 text-left align-middle font-medium text-muted-foreground w-[120px]" > Service</ th >
41- < th className = "h-10 px-2 text-left align-middle font-medium text-muted-foreground w-[80px]" > Severity</ th >
42- < th className = "h-10 px-2 text-left align-middle font-medium text-muted-foreground" > Message</ th >
43- </ tr >
44- </ thead >
45- < tbody className = "[&_tr:last-child]:border-0" >
46- { Array . from ( { length : 10 } ) . map ( ( _ , i ) => (
47- < tr key = { i } className = "border-b transition-colors" >
48- < td className = "p-2 align-middle" > < Skeleton className = "h-4 w-32" /> </ td >
49- < td className = "p-2 align-middle" > < Skeleton className = "h-4 w-20" /> </ td >
50- < td className = "p-2 align-middle" > < Skeleton className = "h-4 w-12" /> </ td >
51- < td className = "p-2 align-middle" > < Skeleton className = "h-4 w-full" /> </ td >
52- </ tr >
53- ) ) }
54- </ tbody >
55- </ table >
22+ < div className = "flex-1 min-h-0 flex flex-col" >
23+ < div className = "rounded-md border overflow-hidden flex-1 min-h-0" >
24+ { Array . from ( { length : 40 } ) . map ( ( _ , i ) => (
25+ < div
26+ key = { i }
27+ className = "border-l-2 border-l-transparent flex items-center gap-2 px-3 py-1 border-b border-border"
28+ >
29+ < Skeleton className = "h-3 w-[72px] shrink-0" />
30+ < Skeleton className = "h-3 w-16 shrink-0" />
31+ < Skeleton className = "h-3 flex-1" />
32+ </ div >
33+ ) ) }
5634 </ div >
5735 </ div >
5836 )
@@ -81,60 +59,8 @@ function LogsTableContent({
8159 setSheetOpen ( true )
8260 } , [ ] )
8361
84- const columns = React . useMemo < ColumnDef < Log > [ ] > (
85- ( ) => [
86- {
87- accessorKey : "timestamp" ,
88- header : "Timestamp" ,
89- size : 160 ,
90- cell : ( { row } ) => (
91- < span className = "font-mono text-muted-foreground" >
92- { formatTimestampInTimezone ( row . original . timestamp , {
93- timeZone : effectiveTimezone ,
94- } ) } { " " }
95- < span className = "text-muted-foreground/60" >
96- ({ formatRelativeTime ( row . original . timestamp ) } )
97- </ span >
98- </ span >
99- ) ,
100- } ,
101- {
102- accessorKey : "serviceName" ,
103- header : "Service" ,
104- size : 120 ,
105- cell : ( { row } ) => (
106- < span className = "font-mono text-xs" > { row . original . serviceName } </ span >
107- ) ,
108- } ,
109- {
110- accessorKey : "severityText" ,
111- header : "Severity" ,
112- size : 80 ,
113- cell : ( { row } ) => (
114- < SeverityBadge severity = { row . original . severityText } />
115- ) ,
116- } ,
117- {
118- accessorKey : "body" ,
119- header : "Message" ,
120- cell : ( { row } ) => (
121- < span className = "font-mono text-xs" > { truncateBody ( row . original . body ) } </ span >
122- ) ,
123- } ,
124- ] ,
125- [ effectiveTimezone ] ,
126- )
127-
128- const table = useReactTable ( {
129- data : allData ,
130- columns,
131- getCoreRowModel : getCoreRowModel ( ) ,
132- } )
133-
134- const { rows } = table . getRowModel ( )
135-
13662 const virtualizer = useVirtualizer ( {
137- count : rows . length ,
63+ count : allData . length ,
13864 getScrollElement : ( ) => scrollContainerRef . current ,
13965 estimateSize : ( ) => ROW_HEIGHT ,
14066 overscan : 10 ,
@@ -146,31 +72,16 @@ function LogsTableContent({
14672 const lastItem = virtualItems [ virtualItems . length - 1 ]
14773 if ( ! lastItem ) return
14874
149- if ( lastItem . index >= rows . length - FETCH_THRESHOLD && hasNextPage && ! isFetchingNextPage ) {
75+ if ( lastItem . index >= allData . length - FETCH_THRESHOLD && hasNextPage && ! isFetchingNextPage ) {
15076 fetchNextPage ( )
15177 }
152- } , [ virtualItems , rows . length , hasNextPage , isFetchingNextPage , fetchNextPage ] )
78+ } , [ virtualItems , allData . length , hasNextPage , isFetchingNextPage , fetchNextPage ] )
15379
15480 if ( allData . length === 0 ) {
15581 return (
15682 < div className = "flex-1 min-h-0 flex flex-col gap-4" >
157- < div className = "rounded-md border" >
158- < table className = "w-full caption-bottom text-sm" >
159- < thead className = "[&_tr]:border-b" >
160- < tr className = "border-b transition-colors hover:bg-muted/50" >
161- < th className = "h-10 px-2 text-left align-middle font-medium text-muted-foreground" colSpan = { 4 } >
162- < span className = "sr-only" > Log columns</ span >
163- </ th >
164- </ tr >
165- </ thead >
166- < tbody >
167- < tr >
168- < td colSpan = { 4 } className = "h-24 text-center" >
169- No logs found
170- </ td >
171- </ tr >
172- </ tbody >
173- </ table >
83+ < div className = "rounded-md border flex items-center justify-center h-48" >
84+ < span className = "text-sm text-muted-foreground" > No logs found</ span >
17485 </ div >
17586 </ div >
17687 )
@@ -183,81 +94,51 @@ function LogsTableContent({
18394 ref = { scrollContainerRef }
18495 className = "flex-1 min-h-0 overflow-auto rounded-md border"
18596 >
186- < table className = "w-full caption-bottom text-sm" aria-label = "Logs" >
187- < thead className = "[&_tr]:border-b sticky top-0 z-10 bg-background" >
188- { table . getHeaderGroups ( ) . map ( ( headerGroup ) => (
189- < tr key = { headerGroup . id } className = "border-b transition-colors hover:bg-muted/50" >
190- { headerGroup . headers . map ( ( header ) => (
191- < th
192- key = { header . id }
193- className = { `h-10 px-2 text-left align-middle font-medium text-muted-foreground ${
194- header . id === "serviceName" ? "hidden md:table-cell" : ""
195- } `}
196- style = { { width : header . getSize ( ) !== 150 ? header . getSize ( ) : undefined } }
197- >
198- { header . isPlaceholder
199- ? null
200- : flexRender ( header . column . columnDef . header , header . getContext ( ) ) }
201- </ th >
202- ) ) }
203- </ tr >
204- ) ) }
205- </ thead >
206- < tbody className = "[&_tr:last-child]:border-0" >
207- { virtualItems . length > 0 && (
208- < tr style = { { height : virtualItems [ 0 ] . start } } aria-hidden = "true" >
209- < td />
210- </ tr >
211- ) }
212- { virtualItems . map ( ( virtualRow ) => {
213- const row = rows [ virtualRow . index ]
214- return (
215- < tr
216- key = { row . id }
217- ref = { virtualizer . measureElement }
218- data-index = { virtualRow . index }
219- className = "border-b transition-colors hover:bg-muted/50 cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-inset"
220- tabIndex = { 0 }
221- onClick = { ( ) => handleRowClick ( row . original ) }
222- onKeyDown = { ( e ) => {
223- if ( e . key === "Enter" || e . key === " " ) {
224- e . preventDefault ( )
225- handleRowClick ( row . original )
226- }
227- } }
228- >
229- { row . getVisibleCells ( ) . map ( ( cell ) => (
230- < td
231- key = { cell . id }
232- className = { `p-2 align-middle [&:has([role=checkbox])]:pr-0 ${
233- cell . column . id === "serviceName" ? "hidden md:table-cell" : ""
234- } ${ cell . column . id === "body" ? " max-w-md" : "" } `}
235- >
236- { flexRender ( cell . column . columnDef . cell , cell . getContext ( ) ) }
237- </ td >
238- ) ) }
239- </ tr >
240- )
241- } ) }
242- { virtualItems . length > 0 && (
243- < tr
97+ < div
98+ style = { { height : virtualizer . getTotalSize ( ) , position : "relative" } }
99+ role = "log"
100+ >
101+ { virtualItems . map ( ( virtualRow ) => {
102+ const log = allData [ virtualRow . index ]
103+ return (
104+ < div
105+ key = { virtualRow . index }
106+ ref = { virtualizer . measureElement }
107+ data-index = { virtualRow . index }
244108 style = { {
245- height : virtualizer . getTotalSize ( ) - ( virtualItems [ virtualItems . length - 1 ] . end ) ,
109+ position : "absolute" ,
110+ top : 0 ,
111+ left : 0 ,
112+ width : "100%" ,
113+ transform : `translateY(${ virtualRow . start } px)` ,
114+ borderLeftColor : getSeverityColor ( log . severityText ) ,
115+ } }
116+ className = "border-l-2 flex items-center gap-2 px-3 py-1 text-xs font-mono cursor-pointer border-b border-border hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-inset"
117+ tabIndex = { 0 }
118+ role = "listitem"
119+ onClick = { ( ) => handleRowClick ( log ) }
120+ onKeyDown = { ( e ) => {
121+ if ( e . key === "Enter" || e . key === " " ) {
122+ e . preventDefault ( )
123+ handleRowClick ( log )
124+ }
246125 } }
247- aria-hidden = "true"
248126 >
249- < td />
250- </ tr >
251- ) }
252- { isFetchingNextPage && (
253- < tr className = "border-b transition-colors" >
254- < td colSpan = { 4 } className = "p-2 text-center text-sm text-muted-foreground" >
255- Loading more logs...
256- </ td >
257- </ tr >
258- ) }
259- </ tbody >
260- </ table >
127+ < span className = "shrink-0 text-muted-foreground tabular-nums" >
128+ { formatCompactTimeInTimezone ( log . timestamp , {
129+ timeZone : effectiveTimezone ,
130+ } ) }
131+ </ span >
132+ < span className = "shrink-0 text-muted-foreground/60 truncate max-w-[120px] hidden md:inline" >
133+ { log . serviceName }
134+ </ span >
135+ < span className = "min-w-0 flex-1 truncate text-foreground" >
136+ { log . body }
137+ </ span >
138+ </ div >
139+ )
140+ } ) }
141+ </ div >
261142 </ div >
262143
263144 < div className = "text-sm text-muted-foreground shrink-0" >
0 commit comments