@@ -20,6 +20,12 @@ import {
2020 DialogContent ,
2121 DialogTitle ,
2222} from "@/components/ui/dialog" ;
23+ import {
24+ Accordion ,
25+ AccordionContent ,
26+ AccordionItem ,
27+ AccordionTrigger ,
28+ } from "@/components/ui/accordion" ;
2329import { ZarrDataset } from "@/components/zarr/ZarrLoaderLRU" ;
2430
2531const Variables = ( {
@@ -44,42 +50,105 @@ const Variables = ({
4450 ) ;
4551 const { currentStore } = useZarrStore ( useShallow ( state => ( {
4652 currentStore : state . currentStore ,
47- } ) ) )
48- const ZarrDS = useMemo ( ( ) => new ZarrDataset ( currentStore ) , [ currentStore ] )
53+ } ) ) ) ;
54+ const ZarrDS = useMemo ( ( ) => new ZarrDataset ( currentStore ) , [ currentStore ] ) ;
4955
50- const [ dimArrays , setDimArrays ] = useState ( [ [ 0 ] , [ 0 ] , [ 0 ] ] )
51- const [ dimUnits , setDimUnits ] = useState ( [ null , null , null ] )
52- const [ dimNames , setDimNames ] = useState < string [ ] > ( [ "Default" ] )
56+ const [ dimArrays , setDimArrays ] = useState ( [ [ 0 ] , [ 0 ] , [ 0 ] ] ) ;
57+ const [ dimUnits , setDimUnits ] = useState ( [ null , null , null ] ) ;
58+ const [ dimNames , setDimNames ] = useState < string [ ] > ( [ "Default" ] ) ;
5359
5460 const [ selectedIndex , setSelectedIndex ] = useState < number | null > ( null ) ;
5561 const [ selectedVar , setSelectedVar ] = useState < string | null > ( null ) ;
5662 const [ meta , setMeta ] = useState < any > ( null ) ;
5763 const [ query , setQuery ] = useState ( "" ) ;
64+ // root *open by default* (collapsible but starts open)
65+ const [ openAccordionItems , setOpenAccordionItems ] = useState < string [ ] > ( [ "root" ] ) ;
5866
59- const filtered = useMemo ( ( ) => {
67+ // Build nested variable tree
68+ const tree = useMemo ( ( ) => {
6069 const q = query . toLowerCase ( ) . trim ( ) ;
61- if ( ! q ) return variables ;
62- return variables . filter ( ( variable ) =>
63- variable . toLowerCase ( ) . includes ( q )
64- ) ;
70+ let filteredVars = variables ?? [ ] ;
71+
72+ if ( q ) {
73+ filteredVars = filteredVars . filter ( ( variable ) =>
74+ variable . toLowerCase ( ) . includes ( q )
75+ ) ;
76+ }
77+
78+ const buildTree = ( vars : string [ ] ) => {
79+ const t : any = { } ;
80+ vars . forEach ( ( v ) => {
81+ const parts = v . split ( "/" ) ;
82+ let current = t ;
83+ parts . forEach ( ( p , i ) => {
84+ if ( ! current [ p ] ) current [ p ] = i === parts . length - 1 ? null : { } ;
85+ current = current [ p ] ;
86+ } ) ;
87+ } ) ;
88+ return t ;
89+ } ;
90+
91+ return buildTree ( filteredVars ) ;
6592 } , [ query , variables ] ) ;
6693
94+ // Get all group paths (for auto-open when searching)
95+ const getGroupPaths = ( subtree : any , basePath = "" ) : string [ ] => {
96+ let paths : string [ ] = [ ] ;
97+ Object . entries ( subtree ) . forEach ( ( [ key , value ] ) => {
98+ const currentPath = basePath ? `${ basePath } /${ key } ` : key ;
99+ if ( value && typeof value === "object" ) {
100+ paths . push ( currentPath ) ;
101+ paths = paths . concat ( getGroupPaths ( value , currentPath ) ) ;
102+ }
103+ } ) ;
104+ return paths ;
105+ } ;
106+
107+ // Auto-open accordions that contain matches
108+ useEffect ( ( ) => {
109+ if ( query . trim ( ) ) {
110+ const openPaths = getGroupPaths ( tree ) ;
111+ setOpenAccordionItems ( [ "root" , ...openPaths ] ) ;
112+ } else {
113+ // when not searching keep root open by default
114+ setOpenAccordionItems ( [ "root" ] ) ;
115+ }
116+ } , [ query , tree ] ) ;
117+
118+ // Handle variable selection
119+ const handleVariableSelect = ( val : string , idx : number ) => {
120+ setSelectedIndex ( idx ) ;
121+ setSelectedVar ( val ) ;
122+ GetDimInfo ( val ) . then ( e => {
123+ setDimNames ( e . dimNames ) ;
124+ setDimArrays ( e . dimArrays ) ;
125+ setDimUnits ( e . dimUnits ) ;
126+ } ) ;
127+
128+ if ( popoverSide === "left" ) {
129+ setOpenMetaPopover ( true ) ;
130+ } else {
131+ setShowMeta ( true ) ;
132+ }
133+ } ;
134+
67135 useEffect ( ( ) => {
68136 if ( variables && zMeta && selectedVar ) {
69137 const relevant = zMeta . find ( ( e : any ) => e . name === selectedVar ) ;
70138 if ( relevant ) {
71139 setMeta ( { ...relevant , dimInfo : { dimArrays, dimNames, dimUnits} } ) ;
72- ZarrDS . GetAttributes ( selectedVar ) . then ( e => setMetadata ( e ) )
140+ ZarrDS . GetAttributes ( selectedVar ) . then ( e => setMetadata ( e ) ) ;
73141 }
74142 }
143+ // eslint-disable-next-line react-hooks/exhaustive-deps
75144 } , [ selectedVar , variables , zMeta , dimArrays , dimNames , dimUnits ] ) ;
76145
77146 useEffect ( ( ) => {
78- setSelectedIndex ( null )
79- setSelectedVar ( null )
80- setMeta ( null )
81- setMetadata ( null )
82- } , [ initStore ] )
147+ setSelectedIndex ( null ) ;
148+ setSelectedVar ( null ) ;
149+ setMeta ( null ) ;
150+ setMetadata ( null ) ;
151+ } , [ initStore , setMetadata ] ) ;
83152
84153 useEffect ( ( ) => {
85154 const handleResize = ( ) => {
@@ -90,38 +159,99 @@ const Variables = ({
90159 return ( ) => window . removeEventListener ( "resize" , handleResize ) ;
91160 } , [ ] ) ;
92161
162+ // Variable item renderer (keeps separator between variables in same group)
163+ const VariableItem = ( { val, idx, arrayLength } : { val : string ; idx : number ; arrayLength : number } ) => {
164+ const variableName = val . split ( '/' ) . pop ( ) || val ;
165+ const isLastItem = idx === arrayLength - 1 ;
166+
167+ return (
168+ < React . Fragment key = { val } >
169+ < div
170+ className = "cursor-pointer pl-2 py-1 text-sm hover:bg-muted rounded"
171+ style = { {
172+ background : selectedVar === val ? "var(--muted-foreground)" : "" ,
173+ } }
174+ onClick = { ( ) => handleVariableSelect ( val , idx ) }
175+ >
176+ { variableName }
177+ </ div >
178+ { ! isLastItem && < Separator className = "my-1" /> }
179+ </ React . Fragment >
180+ ) ;
181+ } ;
182+
183+ // render a subtree inside an Accordion (ensures AccordionItem children are direct children of an Accordion)
184+ const renderSubtreeAccordion = ( subtree : any , basePath = "" ) => {
185+ // Determine entries (keep variables first then groups for clarity)
186+ const entries = Object . entries ( subtree ) ;
187+ const variableEntries = entries . filter ( ( [ _ , v ] ) => v === null ) ;
188+ const groupEntries = entries . filter ( ( [ _ , v ] ) => v && typeof v === "object" ) ;
189+
190+ return (
191+ < Accordion
192+ key = { basePath || "__root_inner__" }
193+ type = "multiple"
194+ value = { openAccordionItems }
195+ onValueChange = { setOpenAccordionItems }
196+ className = "w-full"
197+ >
198+ { /* variables at this level */ }
199+ { variableEntries . length > 0 && (
200+ < div className = "px-1" >
201+ { variableEntries . map ( ( [ name ] , idx ) => {
202+ const varPath = basePath ? `${ basePath } /${ name } ` : name ;
203+ return (
204+ < VariableItem
205+ key = { varPath }
206+ val = { varPath }
207+ idx = { idx }
208+ arrayLength = { variableEntries . length }
209+ />
210+ ) ;
211+ } ) }
212+ </ div >
213+ ) }
214+
215+ { /* groups at this level */ }
216+ { groupEntries . map ( ( [ name , subtreeValue ] ) => {
217+ const currentPath = basePath ? `${ basePath } /${ name } ` : name ;
218+ return (
219+ < AccordionItem key = { currentPath } value = { currentPath } >
220+ < AccordionTrigger className = "cursor-pointer pl-2" >
221+ { name }
222+ </ AccordionTrigger >
223+ < AccordionContent className = "flex flex-col pl-2" >
224+ { /* recursively render children inside their own Accordion */ }
225+ { renderSubtreeAccordion ( subtreeValue , currentPath ) }
226+ </ AccordionContent >
227+ </ AccordionItem >
228+ ) ;
229+ } ) }
230+ </ Accordion >
231+ ) ;
232+ } ;
233+
234+ // render full Variable list under "root" accordion item
93235 const VariableList = (
94236 < div className = "overflow-y-auto flex-1 [&::-webkit-scrollbar]:hidden" >
95- { filtered . length > 0 ? (
96- filtered . map ( ( val , idx ) => (
97- < React . Fragment key = { idx } >
98- < div
99- className = "cursor-pointer pl-2 py-1 text-sm hover:bg-muted rounded"
100- style = { {
101- background :
102- idx === selectedIndex ? "var(--muted-foreground)" : "" ,
103- } }
104- onClick = { ( ) => {
105- setSelectedIndex ( idx ) ;
106- setSelectedVar ( val ) ;
107- GetDimInfo ( val ) . then ( e => { setDimNames ( e . dimNames ) ; setDimArrays ( e . dimArrays ) ; setDimUnits ( e . dimUnits ) } )
108- if ( popoverSide === "left" ) {
109- setOpenMetaPopover ( true ) ;
110- } else {
111- setShowMeta ( true ) ;
112- }
113- } }
114- >
115- { val }
116- </ div >
117- { idx !== filtered . length - 1 && < Separator className = "my-1" /> }
118- </ React . Fragment >
119- ) )
237+ { Object . keys ( tree ) . length > 0 ? (
238+ < Accordion
239+ type = "multiple"
240+ className = "w-full"
241+ value = { openAccordionItems }
242+ onValueChange = { setOpenAccordionItems }
243+ >
244+ < AccordionItem key = "root" value = "root" >
245+ < AccordionTrigger className = "cursor-pointer" > /</ AccordionTrigger >
246+ < AccordionContent className = "flex flex-col" >
247+ { /* render the top-level subtree inside its own Accordion so nested AccordionItems are legal */ }
248+ { renderSubtreeAccordion ( tree , "" ) }
249+ </ AccordionContent >
250+ </ AccordionItem >
251+ </ Accordion >
120252 ) : (
121253 < div className = "text-center text-muted-foreground py-2" >
122- { query
123- ? "No variables found matching your search."
124- : "No variables available." }
254+ { query ? "No variables found matching your search." : "No variables available." }
125255 </ div >
126256 ) }
127257 </ div >
@@ -187,11 +317,11 @@ const Variables = ({
187317 < Popover open = { openMetaPopover } onOpenChange = { setOpenMetaPopover } >
188318 < PopoverTrigger asChild >
189319 < div
190- className = "absolute -top-8" // adjust top position as needed
191- style = { {
192- left : `-${ 280 } px` , // move left
193- } }
194- />
320+ className = "absolute -top-8" // adjust top position as needed
321+ style = { {
322+ left : `-${ 280 } px` , // move to left of variable popover
323+ } }
324+ />
195325 </ PopoverTrigger >
196326 < PopoverContent
197327 data-meta-popover
@@ -200,13 +330,13 @@ const Variables = ({
200330 className = "max-h-[80vh] overflow-y-auto w-[300px]"
201331 >
202332 { meta && (
203- < MetaDataInfo
204- meta = { meta }
205- metadata = { metadata ?? { } }
206- setShowMeta = { setOpenMetaPopover }
207- setOpenVariables = { setOpenVariables }
208- popoverSide = { "left" }
209- />
333+ < MetaDataInfo
334+ meta = { meta }
335+ metadata = { metadata ?? { } }
336+ setShowMeta = { setOpenMetaPopover }
337+ setOpenVariables = { setOpenVariables }
338+ popoverSide = { "left" }
339+ />
210340 ) }
211341 </ PopoverContent >
212342 </ Popover >
@@ -219,7 +349,7 @@ const Variables = ({
219349 { meta && (
220350 < MetaDataInfo
221351 meta = { meta }
222- metadata = { metadata ?? { } }
352+ metadata = { metadata ?? { } }
223353 setShowMeta = { setShowMeta }
224354 setOpenVariables = { setOpenVariables }
225355 popoverSide = { "top" }
0 commit comments