@@ -10,7 +10,7 @@ import { useInfiniteQuery, useIsFetching } from '@tanstack/react-query'
10
10
import { useVirtualizer } from '@tanstack/react-virtual'
11
11
import cn from 'classnames'
12
12
import { differenceInMilliseconds } from 'date-fns'
13
- import { useMemo , useRef } from 'react'
13
+ import { memo , useCallback , useMemo , useRef , useState } from 'react'
14
14
import { match } from 'ts-pattern'
15
15
16
16
import { api } from '@oxide/api'
@@ -31,6 +31,88 @@ import { docLinks } from '~/util/links'
31
31
32
32
export const handle = { crumb : 'Audit Log' }
33
33
34
+ const Indent = ( { depth } : { depth : number } ) => (
35
+ < span className = "inline-block" style = { { width : `${ depth * 4 + 1 } ch` } } />
36
+ )
37
+
38
+ const Primitive = ( { value } : { value : null | boolean | number | string } ) => (
39
+ < span className = "text-[var(--base-blue-600)]" >
40
+ { value === null ? 'null' : typeof value === 'string' ? `"${ value } "` : String ( value ) }
41
+ </ span >
42
+ )
43
+
44
+ // silly faux highlighting
45
+ // avoids unnecessary import of a library and all that overhead
46
+ const HighlightJSON = memo ( ( { jsonString } : { jsonString : string } ) => {
47
+ const renderValue = (
48
+ value : null | boolean | number | string | object ,
49
+ depth = 0
50
+ ) : React . ReactNode => {
51
+ if (
52
+ value === null ||
53
+ typeof value === 'boolean' ||
54
+ typeof value === 'number' ||
55
+ typeof value === 'string'
56
+ ) {
57
+ return < Primitive value = { value } />
58
+ }
59
+
60
+ if ( Array . isArray ( value ) ) {
61
+ if ( value . length === 0 ) return < span className = "text-quaternary" > []</ span >
62
+
63
+ return (
64
+ < >
65
+ < span className = "text-quaternary" > [</ span >
66
+ { '\n' }
67
+ { value . map ( ( item , index ) => (
68
+ < span key = { index } >
69
+ < Indent depth = { depth + 1 } />
70
+ { renderValue ( item , depth + 1 ) }
71
+ { index < value . length - 1 && < span className = "text-quaternary" > ,</ span > }
72
+ { '\n' }
73
+ </ span >
74
+ ) ) }
75
+ < Indent depth = { depth } />
76
+ < span className = "text-quaternary" > ]</ span >
77
+ </ >
78
+ )
79
+ }
80
+
81
+ if ( typeof value === 'object' ) {
82
+ const entries = Object . entries ( value )
83
+ if ( entries . length === 0 ) return < span className = "text-quaternary" > { '{}' } </ span >
84
+
85
+ return (
86
+ < >
87
+ < span className = "text-quaternary" > { '{' } </ span >
88
+ { '\n' }
89
+ { entries . map ( ( [ key , val ] , index ) => (
90
+ < span key = { key } >
91
+ < Indent depth = { depth + 1 } />
92
+ < span className = "text-default" > { key } </ span >
93
+ < span className = "text-quaternary" > : </ span >
94
+ { renderValue ( val , depth + 1 ) }
95
+ { index < entries . length - 1 && < span className = "text-quaternary" > ,</ span > }
96
+ { '\n' }
97
+ </ span >
98
+ ) ) }
99
+ < Indent depth = { depth } />
100
+ < span className = "text-quaternary" > { '}' } </ span >
101
+ </ >
102
+ )
103
+ }
104
+
105
+ return String ( value )
106
+ }
107
+
108
+ try {
109
+ const parsed = JSON . parse ( jsonString )
110
+ return < > { renderValue ( parsed ) } </ >
111
+ } catch {
112
+ return < > { jsonString } </ >
113
+ }
114
+ } )
115
+
34
116
// todo
35
117
// might want to still render the items in case of error
36
118
const ErrorState = ( ) => {
@@ -48,10 +130,11 @@ const colWidths = {
48
130
49
131
const HeaderCell = classed . div `text-mono-sm text-tertiary`
50
132
51
- // for virtualizer
52
- const estimateSize = ( ) => 36
133
+ const EXPANDED_HEIGHT = 288 // h-72 * 4
53
134
54
135
export default function SiloAuditLogsPage ( ) {
136
+ const [ expandedItem , setExpandedItem ] = useState < string | null > ( null )
137
+
55
138
// pass refetch interval to this to keep the date up to date
56
139
const { preset, startTime, endTime, dateTimeRangePicker, onRangeChange } =
57
140
useDateTimeRangePicker ( {
@@ -104,10 +187,23 @@ export default function SiloAuditLogsPage() {
104
187
const rowVirtualizer = useVirtualizer ( {
105
188
count : allItems . length ,
106
189
getScrollElement : ( ) => document . querySelector ( '#scroll-container' ) ,
107
- estimateSize,
190
+ estimateSize : useCallback (
191
+ ( index ) => {
192
+ return expandedItem === index . toString ( ) ? 36 + EXPANDED_HEIGHT : 36
193
+ } ,
194
+ [ expandedItem ]
195
+ ) ,
108
196
overscan : 20 ,
109
197
} )
110
198
199
+ const handleToggle = useCallback (
200
+ ( index : string | null ) => {
201
+ setExpandedItem ( index )
202
+ rowVirtualizer . measure ( )
203
+ } ,
204
+ [ rowVirtualizer ]
205
+ )
206
+
111
207
const logTable = (
112
208
< >
113
209
< div
@@ -118,6 +214,8 @@ export default function SiloAuditLogsPage() {
118
214
>
119
215
{ rowVirtualizer . getVirtualItems ( ) . map ( ( virtualRow ) => {
120
216
const log = allItems [ virtualRow . index ]
217
+ const isExpanded = expandedItem === virtualRow . index . toString ( )
218
+ const jsonString = JSON . stringify ( log , null , 2 )
121
219
122
220
const [ userId , siloId ] = match ( log . actor )
123
221
. with ( { kind : 'silo_user' } , ( actor ) => [ actor . siloUserId , actor . siloId ] )
@@ -134,12 +232,18 @@ export default function SiloAuditLogsPage() {
134
232
transform : `translateY(${ virtualRow . start } px)` ,
135
233
} }
136
234
>
137
- < div
235
+ < button
138
236
className = { cn (
139
- 'grid h-9 w-full items-center gap-8 px-[var(--content-gutter)] text-left text-sans-md border-secondary' ,
237
+ 'grid h-9 w-full cursor-pointer items-center gap-8 px-[var(--content-gutter)] text-left text-sans-md border-secondary' ,
238
+ isExpanded ? 'bg-raise' : 'hover:bg-raise' ,
140
239
virtualRow . index !== 0 && 'border-t'
141
240
) }
142
241
style = { colWidths }
242
+ onClick = { ( ) => {
243
+ const newValue = isExpanded ? null : virtualRow . index . toString ( )
244
+ handleToggle ( newValue )
245
+ } }
246
+ type = "button"
143
247
>
144
248
{ /* TODO: might be especially useful here to get the original UTC timestamp in a tooltip */ }
145
249
< div className = "overflow-hidden whitespace-nowrap text-mono-sm" >
@@ -188,7 +292,14 @@ export default function SiloAuditLogsPage() {
188
292
{ differenceInMilliseconds ( new Date ( log . timeCompleted ) , log . timeStarted ) }
189
293
ms
190
294
</ div >
191
- </ div >
295
+ </ button >
296
+ { isExpanded && (
297
+ < div className = "h-72 border-t px-[var(--content-gutter)] py-3 border-secondary" >
298
+ < pre className = "h-full overflow-auto border-l pl-4 text-mono-code border-secondary" >
299
+ < HighlightJSON jsonString = { jsonString } />
300
+ </ pre >
301
+ </ div >
302
+ ) }
192
303
</ div >
193
304
)
194
305
} ) }
0 commit comments