@@ -12,6 +12,7 @@ import cn from 'classnames'
1212import { differenceInMilliseconds } from 'date-fns'
1313import { memo , useCallback , useMemo , useRef , useState } from 'react'
1414import { match } from 'ts-pattern'
15+ import { JsonValue } from 'type-fest'
1516
1617import { api } from '@oxide/api'
1718import { Logs16Icon , Logs24Icon } from '@oxide/design-system/icons/react'
@@ -77,78 +78,63 @@ const Primitive = ({ value }: { value: null | boolean | number | string }) => (
7778 </ span >
7879)
7980
80- // TODO: avoid converting JSON to string and then parsing again. just need a better memo
81-
82- // silly faux highlighting
83- // avoids unnecessary import of a library and all that overhead
84- const HighlightJSON = memo ( ( { jsonString } : { jsonString : string } ) => {
85- const renderValue = (
86- value : null | boolean | number | string | object ,
87- depth = 0
88- ) : React . ReactNode => {
89- if (
90- value === null ||
91- typeof value === 'boolean' ||
92- typeof value === 'number' ||
93- typeof value === 'string'
94- ) {
95- return < Primitive value = { value } />
96- }
97-
98- if ( Array . isArray ( value ) ) {
99- if ( value . length === 0 ) return < span className = "text-quaternary" > []</ span >
81+ // memo is important to avoid re-renders if the value hasn't changed. value
82+ // passed in must be referentially stable, which should generally be the case
83+ // with API responses
84+ const HighlightJSON = memo ( ( { json, depth = 0 } : { json : JsonValue ; depth ?: number } ) => {
85+ if ( json === undefined ) return null
86+
87+ if (
88+ json === null ||
89+ typeof json === 'boolean' ||
90+ typeof json === 'number' ||
91+ typeof json === 'string'
92+ ) {
93+ return < Primitive value = { json } />
94+ }
10095
101- return (
102- < >
103- < span className = "text-quaternary" > [</ span >
104- { '\n' }
105- { value . map ( ( item , index ) => (
106- < span key = { index } >
107- < Indent depth = { depth + 1 } />
108- { renderValue ( item , depth + 1 ) }
109- { index < value . length - 1 && < span className = "text-quaternary" > ,</ span > }
110- { '\n' }
111- </ span >
112- ) ) }
113- < Indent depth = { depth } />
114- < span className = "text-quaternary" > ]</ span >
115- </ >
116- )
117- }
96+ if ( Array . isArray ( json ) ) {
97+ if ( json . length === 0 ) return < span className = "text-quaternary" > []</ span >
98+
99+ return (
100+ < >
101+ < span className = "text-quaternary" > [</ span >
102+ { '\n' }
103+ { json . map ( ( item , index ) => (
104+ < span key = { index } >
105+ < Indent depth = { depth + 1 } />
106+ < HighlightJSON json = { item } depth = { depth + 1 } />
107+ { index < json . length - 1 && < span className = "text-quaternary" > ,</ span > }
108+ { '\n' }
109+ </ span >
110+ ) ) }
111+ < Indent depth = { depth } />
112+ < span className = "text-quaternary" > ]</ span >
113+ </ >
114+ )
115+ }
118116
119- if ( typeof value === 'object' ) {
120- const entries = Object . entries ( value )
121- if ( entries . length === 0 ) return < span className = "text-quaternary" > { '{}' } </ span >
117+ const entries = Object . entries ( json )
118+ if ( entries . length === 0 ) return < span className = "text-quaternary" > { '{}' } </ span >
122119
123- return (
124- < >
125- < span className = "text-quaternary" > { '{' } </ span >
120+ return (
121+ < >
122+ < span className = "text-quaternary" > { '{' } </ span >
123+ { '\n' }
124+ { entries . map ( ( [ key , val ] , index ) => (
125+ < span key = { key } >
126+ < Indent depth = { depth + 1 } />
127+ < span className = "text-default" > { key } </ span >
128+ < span className = "text-quaternary" > : </ span >
129+ < HighlightJSON json = { val } depth = { depth + 1 } />
130+ { index < entries . length - 1 && < span className = "text-quaternary" > ,</ span > }
126131 { '\n' }
127- { entries . map ( ( [ key , val ] , index ) => (
128- < span key = { key } >
129- < Indent depth = { depth + 1 } />
130- < span className = "text-default" > { key } </ span >
131- < span className = "text-quaternary" > : </ span >
132- { renderValue ( val , depth + 1 ) }
133- { index < entries . length - 1 && < span className = "text-quaternary" > ,</ span > }
134- { '\n' }
135- </ span >
136- ) ) }
137- < Indent depth = { depth } />
138- < span className = "text-quaternary" > { '}' } </ span >
139- </ >
140- )
141- }
142-
143- return String ( value )
144- }
145-
146- try {
147- const parsed = JSON . parse ( jsonString )
148- return < > { renderValue ( parsed ) } </ >
149- } catch {
150- return < > { jsonString } </ >
151- }
132+ </ span >
133+ ) ) }
134+ < Indent depth = { depth } />
135+ < span className = "text-quaternary" > { '}' } </ span >
136+ </ >
137+ )
152138} )
153139
154140// todo
@@ -254,9 +240,7 @@ export default function SiloAuditLogsPage() {
254240 const log = allItems [ virtualRow . index ]
255241 const isExpanded = expandedItem === virtualRow . index . toString ( )
256242 // only bother doing all this computation if we're the expanded row
257- const jsonString = isExpanded
258- ? JSON . stringify ( camelToSnakeJson ( log ) , null , 2 )
259- : ''
243+ const json = isExpanded ? camelToSnakeJson ( log ) : undefined
260244
261245 const [ userId , siloId ] = match ( log . actor )
262246 . with ( { kind : 'silo_user' } , ( actor ) => [ actor . siloUserId , actor . siloId ] )
@@ -336,7 +320,7 @@ export default function SiloAuditLogsPage() {
336320 { isExpanded && (
337321 < div className = "h-72 border-t px-[var(--content-gutter)] py-3 border-secondary" >
338322 < pre className = "h-full overflow-auto border-l pl-4 text-mono-code border-secondary" >
339- < HighlightJSON jsonString = { jsonString } />
323+ < HighlightJSON json = { json as JsonValue } />
340324 </ pre >
341325 </ div >
342326 ) }
0 commit comments