1- import { useEffect , useCallback } from "react" ;
1+ import { useEffect , useCallback , useState } from "react" ;
22import { useExplorerStore } from "../../store/useExplorerStore" ;
33import { useHashRoute } from "../../hooks/useHashRoute" ;
44import { listDirectory } from "../../api/explorer-client" ;
5+ import { getStateDbStatus , getStateDbTables } from "../../api/statedb-client" ;
6+ import type { StateDbTable } from "../../types/statedb" ;
57
68function FileTreeNode ( { path, name, type, depth } : {
79 path : string ;
@@ -113,9 +115,114 @@ function FileTreeNode({ path, name, type, depth }: {
113115 ) ;
114116}
115117
118+ function StateDbSection ( { onDbMissing } : { onDbMissing : ( ) => void } ) {
119+ const [ tables , setTables ] = useState < StateDbTable [ ] > ( [ ] ) ;
120+ const [ expanded , setExpanded ] = useState ( true ) ;
121+ const [ refreshing , setRefreshing ] = useState ( false ) ;
122+ const { stateDbTable, navigate } = useHashRoute ( ) ;
123+
124+ const refresh = useCallback ( ( ) => {
125+ setRefreshing ( true ) ;
126+ getStateDbStatus ( )
127+ . then ( ( { exists } ) => {
128+ if ( ! exists ) {
129+ onDbMissing ( ) ;
130+ return ;
131+ }
132+ return getStateDbTables ( ) . then ( setTables ) ;
133+ } )
134+ . catch ( console . error )
135+ . finally ( ( ) => setRefreshing ( false ) ) ;
136+ } , [ onDbMissing ] ) ;
137+
138+ useEffect ( ( ) => { refresh ( ) ; } , [ refresh ] ) ;
139+
140+ return (
141+ < div >
142+ { /* Section header */ }
143+ < div className = "flex items-center" >
144+ < button
145+ onClick = { ( ) => setExpanded ( ! expanded ) }
146+ className = "flex-1 text-left flex items-center gap-1 py-[5px] text-[11px] uppercase tracking-wider font-semibold cursor-pointer"
147+ style = { { paddingLeft : "12px" , background : "none" , border : "none" , color : "var(--text-muted)" } }
148+ >
149+ < svg
150+ width = "10"
151+ height = "10"
152+ viewBox = "0 0 10 10"
153+ fill = "currentColor"
154+ style = { { transform : expanded ? "rotate(90deg)" : "rotate(0deg)" , transition : "transform 0.15s" } }
155+ >
156+ < path d = "M3 1.5L7 5L3 8.5z" />
157+ </ svg >
158+ State Database
159+ </ button >
160+ < button
161+ onClick = { ( e ) => { e . stopPropagation ( ) ; refresh ( ) ; } }
162+ className = "shrink-0 flex items-center justify-center w-5 h-5 rounded cursor-pointer"
163+ style = { { background : "none" , border : "none" , color : "var(--text-muted)" , marginRight : "8px" } }
164+ onMouseEnter = { ( e ) => { e . currentTarget . style . color = "var(--text-primary)" ; } }
165+ onMouseLeave = { ( e ) => { e . currentTarget . style . color = "var(--text-muted)" ; } }
166+ title = "Refresh tables"
167+ >
168+ < svg
169+ width = "12"
170+ height = "12"
171+ viewBox = "0 0 24 24"
172+ fill = "none"
173+ stroke = "currentColor"
174+ strokeWidth = "2"
175+ strokeLinecap = "round"
176+ strokeLinejoin = "round"
177+ style = { refreshing ? { animation : "spin 0.6s linear infinite" } : undefined }
178+ >
179+ < polyline points = "23 4 23 10 17 10" />
180+ < path d = "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
181+ </ svg >
182+ </ button >
183+ </ div >
184+ { expanded && tables . map ( ( t ) => (
185+ < button
186+ key = { t . name }
187+ onClick = { ( ) => navigate ( `#/explorer/statedb/${ encodeURIComponent ( t . name ) } ` ) }
188+ className = "w-full text-left flex items-center gap-1 py-[3px] text-[13px] cursor-pointer transition-colors"
189+ style = { {
190+ paddingLeft : "28px" ,
191+ paddingRight : "8px" ,
192+ background : stateDbTable === t . name
193+ ? "color-mix(in srgb, var(--accent) 15%, var(--bg-primary))"
194+ : "transparent" ,
195+ color : stateDbTable === t . name ? "var(--text-primary)" : "var(--text-secondary)" ,
196+ border : "none" ,
197+ } }
198+ onMouseEnter = { ( e ) => {
199+ if ( stateDbTable !== t . name ) e . currentTarget . style . background = "var(--bg-hover)" ;
200+ } }
201+ onMouseLeave = { ( e ) => {
202+ if ( stateDbTable !== t . name ) e . currentTarget . style . background = "transparent" ;
203+ } }
204+ >
205+ < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "1.5" strokeLinecap = "round" strokeLinejoin = "round" style = { { color : "var(--accent)" } } className = "shrink-0" >
206+ < rect x = "3" y = "3" width = "18" height = "18" rx = "2" />
207+ < line x1 = "3" y1 = "9" x2 = "21" y2 = "9" />
208+ < line x1 = "3" y1 = "15" x2 = "21" y2 = "15" />
209+ < line x1 = "9" y1 = "3" x2 = "9" y2 = "21" />
210+ </ svg >
211+ < span className = "truncate flex-1" > { t . name } </ span >
212+ < span className = "text-[10px] shrink-0" style = { { color : "var(--text-muted)" } } >
213+ { t . row_count }
214+ </ span >
215+ </ button >
216+ ) ) }
217+ </ div >
218+ ) ;
219+ }
220+
116221export default function ExplorerSidebar ( ) {
117222 const rootChildren = useExplorerStore ( ( s ) => s . children [ "" ] ) ;
118223 const { setChildren } = useExplorerStore ( ) ;
224+ const [ hasStateDb , setHasStateDb ] = useState ( false ) ;
225+ const [ filesExpanded , setFilesExpanded ] = useState ( true ) ;
119226
120227 // Load root directory on mount
121228 useEffect ( ( ) => {
@@ -126,22 +233,51 @@ export default function ExplorerSidebar() {
126233 }
127234 } , [ rootChildren , setChildren ] ) ;
128235
236+ // Check if state.db exists
237+ useEffect ( ( ) => {
238+ getStateDbStatus ( )
239+ . then ( ( { exists } ) => setHasStateDb ( exists ) )
240+ . catch ( ( ) => setHasStateDb ( false ) ) ;
241+ } , [ ] ) ;
242+
129243 return (
130244 < div className = "flex-1 overflow-y-auto py-1" >
131- { rootChildren ? (
132- rootChildren . map ( ( entry ) => (
133- < FileTreeNode
134- key = { entry . path }
135- path = { entry . path }
136- name = { entry . name }
137- type = { entry . type }
138- depth = { 0 }
139- />
140- ) )
141- ) : (
142- < p className = "text-[11px] px-3 py-2" style = { { color : "var(--text-muted)" } } >
143- Loading...
144- </ p >
245+ { hasStateDb && (
246+ < StateDbSection onDbMissing = { ( ) => setHasStateDb ( false ) } />
247+ ) }
248+ { /* Collapsible FILES section */ }
249+ < button
250+ onClick = { ( ) => setFilesExpanded ( ! filesExpanded ) }
251+ className = "w-full text-left flex items-center gap-1 py-[5px] text-[11px] uppercase tracking-wider font-semibold cursor-pointer"
252+ style = { { paddingLeft : "12px" , paddingRight : "8px" , background : "none" , border : "none" , color : "var(--text-muted)" } }
253+ >
254+ < svg
255+ width = "10"
256+ height = "10"
257+ viewBox = "0 0 10 10"
258+ fill = "currentColor"
259+ style = { { transform : filesExpanded ? "rotate(90deg)" : "rotate(0deg)" , transition : "transform 0.15s" } }
260+ >
261+ < path d = "M3 1.5L7 5L3 8.5z" />
262+ </ svg >
263+ Files
264+ </ button >
265+ { filesExpanded && (
266+ rootChildren ? (
267+ rootChildren . map ( ( entry ) => (
268+ < FileTreeNode
269+ key = { entry . path }
270+ path = { entry . path }
271+ name = { entry . name }
272+ type = { entry . type }
273+ depth = { 0 }
274+ />
275+ ) )
276+ ) : (
277+ < p className = "text-[11px] px-3 py-2" style = { { color : "var(--text-muted)" } } >
278+ Loading...
279+ </ p >
280+ )
145281 ) }
146282 </ div >
147283 ) ;
0 commit comments