1- import type { DataFrame , ResolvedValue } from 'hightable'
1+ import type { DataFrame } from 'hightable'
22import { stringify } from 'hightable'
3- import { ReactNode , useCallback , useEffect , useState } from 'react'
3+ import { useEffect , useState } from 'react'
44import { useConfig } from '../../hooks/useConfig.js'
55import { cn } from '../../lib/utils.js'
66import ContentWrapper from '../ContentWrapper/ContentWrapper.js'
7+ import Dropdown from '../Dropdown/Dropdown.js'
78import Json from '../Json/Json.js'
89import jsonStyles from '../Json/Json.module.css'
910import SlideCloseButton from '../SlideCloseButton/SlideCloseButton.js'
@@ -18,78 +19,106 @@ interface ViewerProps {
1819 onClose : ( ) => void
1920}
2021
21- const UNLOADED_CELL_PLACEHOLDER = '<the content has not been fetched yet> '
22+ type Lens = 'text' | 'json '
2223
2324/**
2425 * Cell viewer displays a single cell from a table.
2526 */
2627export default function CellPanel ( { df, row, col, setProgress, setError, onClose } : ViewerProps ) {
27- const [ content , setContent ] = useState < ReactNode > ( )
28+ const [ value , setValue ] = useState < unknown > ( )
29+ const [ lens , setLens ] = useState < Lens > ( 'text' )
30+ const [ lensOptions , setLensOptions ] = useState < Lens [ ] > ( [ ] )
31+ const [ isLoading , setIsLoading ] = useState ( true )
2832 const { customClass } = useConfig ( )
2933
30- const fillContent = useCallback ( ( cell : ResolvedValue < unknown > | undefined ) => {
31- let content : ReactNode
32- if ( cell === undefined ) {
33- content =
34- < code className = { cn ( jsonStyles . textView , customClass ?. textView ) } >
35- { UNLOADED_CELL_PLACEHOLDER }
36- </ code >
37- } else {
38- const { value } = cell
39- if ( value instanceof Object && ! ( value instanceof Date ) ) {
40- content =
41- < code className = { cn ( jsonStyles . jsonView , customClass ?. jsonView ) } >
42- < Json json = { value } />
43- </ code >
44- } else {
45- content =
46- < code className = { cn ( styles . textView , customClass ?. textView ) } >
47- { stringify ( value ) }
48- </ code >
49- }
50- }
51- setContent ( content )
52- setError ( undefined )
53- } , [ customClass ?. textView , customClass ?. jsonView , setError ] )
54-
5534 // Load cell data
5635 useEffect ( ( ) => {
5736 async function loadCellData ( ) {
5837 try {
5938 setProgress ( 0.5 )
39+ setIsLoading ( true )
40+ setLensOptions ( [ ] )
6041
6142 const columnName = df . columnDescriptors [ col ] ?. name
6243 if ( columnName === undefined ) {
6344 throw new Error ( `Column name missing at index col=${ col } ` )
6445 }
6546 let cell = df . getCell ( { row, column : columnName } )
6647 if ( cell === undefined ) {
67- fillContent ( undefined )
68- return
48+ await df . fetch ?. ( { rowStart : row , rowEnd : row + 1 , columns : [ columnName ] } )
49+ cell = df . getCell ( { row , column : columnName } )
6950 }
70- await df . fetch ?.( { rowStart : row , rowEnd : row + 1 , columns : [ columnName ] } )
71- cell = df . getCell ( { row, column : columnName } )
7251 if ( cell === undefined ) {
7352 throw new Error ( `Cell at row=${ row } , column=${ columnName } is undefined` )
7453 }
75- fillContent ( cell )
54+
55+ // Parse string if valid JSON
56+ const value : unknown = attemptJSONParse ( cell . value )
57+
58+ const { options, defaultLens } = determineLensOptions ( value )
59+ setLensOptions ( options )
60+ setLens ( defaultLens )
61+ setValue ( value )
62+ setError ( undefined )
7663 } catch ( error ) {
7764 setError ( error as Error )
7865 } finally {
66+ setIsLoading ( false )
7967 setProgress ( 1 )
8068 }
8169 }
8270
8371 void loadCellData ( )
84- } , [ df , col , row , setProgress , setError , fillContent ] )
72+ } , [ df , col , row , setProgress , setError ] )
8573
8674 const headers = < >
8775 < SlideCloseButton onClick = { onClose } />
8876 < span > column: { df . columnDescriptors [ col ] ?. name } </ span >
8977 < span > row: { row + 1 } </ span >
78+ { lensOptions . length > 1 && < Dropdown label = { lens } align = 'right' >
79+ { lensOptions . map ( option =>
80+ < button key = { option } onClick = { ( ) => { setLens ( option ) } } >
81+ { option }
82+ </ button >
83+ ) }
84+ </ Dropdown > }
9085 </ >
9186
92- return < ContentWrapper headers = { headers } >
87+ let content
88+ if ( isLoading ) {
89+ content = undefined
90+ } else if ( value instanceof Error ) {
91+ content = < code className = { cn ( styles . textView , customClass ?. textView ) } > { value . name } : { value . message } </ code >
92+ } else if ( lens === 'json' && isJsonLike ( value ) ) {
93+ content = < code className = { cn ( jsonStyles . jsonView , customClass ?. jsonView ) } > < Json json = { value } /> </ code >
94+ } else {
95+ content = < code className = { cn ( styles . textView , customClass ?. textView ) } > { stringify ( value ) } </ code >
96+ }
97+
98+ return < ContentWrapper headers = { headers } isLoading = { isLoading } >
9399 { content }
94100 </ ContentWrapper >
95101}
102+
103+ function determineLensOptions ( cellValue : unknown ) : { options : Lens [ ] ; defaultLens : Lens } {
104+ if ( isJsonLike ( cellValue ) ) {
105+ return { options : [ 'json' , 'text' ] , defaultLens : 'json' }
106+ }
107+ return { options : [ 'text' ] , defaultLens : 'text' }
108+ }
109+
110+ function isJsonLike ( cellValue : unknown ) : boolean {
111+ if ( cellValue === null ) return false
112+ if ( cellValue instanceof Date ) return false
113+ if ( cellValue instanceof Error ) return false
114+ return typeof cellValue === 'object'
115+ }
116+
117+ function attemptJSONParse ( value : unknown ) : unknown {
118+ if ( typeof value !== 'string' ) return value
119+ try {
120+ return JSON . parse ( value ) as unknown
121+ } catch {
122+ return value
123+ }
124+ }
0 commit comments