diff --git a/package.json b/package.json
index d0f68614..f84dcc41 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"lint": "next lint --max-warnings 100"
},
"dependencies": {
+ "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-separator": "^1.1.7",
diff --git a/src/components/ui/MainPanel/MetaDataInfo.tsx b/src/components/ui/MainPanel/MetaDataInfo.tsx
index 66937af5..293ef87c 100644
--- a/src/components/ui/MainPanel/MetaDataInfo.tsx
+++ b/src/components/ui/MainPanel/MetaDataInfo.tsx
@@ -196,7 +196,7 @@ const MetaDataInfo = ({ meta, metadata, setShowMeta, setOpenVariables, popoverSi
{`${meta.long_name} `}
{ popoverSide=="left" ?
-
+
Attributes
@@ -209,7 +209,7 @@ const MetaDataInfo = ({ meta, metadata, setShowMeta, setOpenVariables, popoverSi
:
-
+
}
diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx
index f490ef1c..733b3add 100644
--- a/src/components/ui/MainPanel/Variables.tsx
+++ b/src/components/ui/MainPanel/Variables.tsx
@@ -20,6 +20,12 @@ import {
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
import { ZarrDataset } from "@/components/zarr/ZarrLoaderLRU";
const Variables = ({
@@ -44,42 +50,105 @@ const Variables = ({
);
const { currentStore } = useZarrStore(useShallow(state => ({
currentStore: state.currentStore,
- })))
- const ZarrDS = useMemo(() => new ZarrDataset(currentStore), [currentStore])
+ })));
+ const ZarrDS = useMemo(() => new ZarrDataset(currentStore), [currentStore]);
- const [dimArrays, setDimArrays] = useState([[0],[0],[0]])
- const [dimUnits, setDimUnits] = useState([null,null,null])
- const [dimNames, setDimNames] = useState (["Default"])
+ const [dimArrays, setDimArrays] = useState([[0],[0],[0]]);
+ const [dimUnits, setDimUnits] = useState([null,null,null]);
+ const [dimNames, setDimNames] = useState(["Default"]);
const [selectedIndex, setSelectedIndex] = useState(null);
const [selectedVar, setSelectedVar] = useState(null);
const [meta, setMeta] = useState(null);
const [query, setQuery] = useState("");
+ // root *open by default* (collapsible but starts open)
+ const [openAccordionItems, setOpenAccordionItems] = useState(["root"]);
- const filtered = useMemo(() => {
+ // Build nested variable tree
+ const tree = useMemo(() => {
const q = query.toLowerCase().trim();
- if (!q) return variables;
- return variables.filter((variable) =>
- variable.toLowerCase().includes(q)
- );
+ let filteredVars = variables ?? [];
+
+ if (q) {
+ filteredVars = filteredVars.filter((variable) =>
+ variable.toLowerCase().includes(q)
+ );
+ }
+
+ const buildTree = (vars: string[]) => {
+ const t: any = {};
+ vars.forEach((v) => {
+ const parts = v.split("/");
+ let current = t;
+ parts.forEach((p, i) => {
+ if (!current[p]) current[p] = i === parts.length - 1 ? null : {};
+ current = current[p];
+ });
+ });
+ return t;
+ };
+
+ return buildTree(filteredVars);
}, [query, variables]);
+ // Get all group paths (for auto-open when searching)
+ const getGroupPaths = (subtree: any, basePath = ""): string[] => {
+ let paths: string[] = [];
+ Object.entries(subtree).forEach(([key, value]) => {
+ const currentPath = basePath ? `${basePath}/${key}` : key;
+ if (value && typeof value === "object") {
+ paths.push(currentPath);
+ paths = paths.concat(getGroupPaths(value, currentPath));
+ }
+ });
+ return paths;
+ };
+
+ // Auto-open accordions that contain matches
+ useEffect(() => {
+ if (query.trim()) {
+ const openPaths = getGroupPaths(tree);
+ setOpenAccordionItems(["root", ...openPaths]);
+ } else {
+ // when not searching keep root open by default
+ setOpenAccordionItems(["root"]);
+ }
+ }, [query, tree]);
+
+ // Handle variable selection
+ const handleVariableSelect = (val: string, idx: number) => {
+ setSelectedIndex(idx);
+ setSelectedVar(val);
+ GetDimInfo(val).then(e => {
+ setDimNames(e.dimNames);
+ setDimArrays(e.dimArrays);
+ setDimUnits(e.dimUnits);
+ });
+
+ if (popoverSide === "left") {
+ setOpenMetaPopover(true);
+ } else {
+ setShowMeta(true);
+ }
+ };
+
useEffect(() => {
if (variables && zMeta && selectedVar) {
const relevant = zMeta.find((e: any) => e.name === selectedVar);
if (relevant){
setMeta({...relevant, dimInfo : {dimArrays, dimNames, dimUnits}});
- ZarrDS.GetAttributes(selectedVar).then(e=>setMetadata(e))
+ ZarrDS.GetAttributes(selectedVar).then(e=>setMetadata(e));
}
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedVar, variables, zMeta, dimArrays, dimNames, dimUnits]);
useEffect(()=>{
- setSelectedIndex(null)
- setSelectedVar(null)
- setMeta(null)
- setMetadata(null)
- },[initStore])
+ setSelectedIndex(null);
+ setSelectedVar(null);
+ setMeta(null);
+ setMetadata(null);
+ },[initStore, setMetadata]);
useEffect(() => {
const handleResize = () => {
@@ -90,38 +159,99 @@ const Variables = ({
return () => window.removeEventListener("resize", handleResize);
}, []);
+ // Variable item renderer (keeps separator between variables in same group)
+ const VariableItem = ({ val, idx, arrayLength }: { val: string; idx: number; arrayLength: number }) => {
+ const variableName = val.split('/').pop() || val;
+ const isLastItem = idx === arrayLength - 1;
+
+ return (
+
+ handleVariableSelect(val, idx)}
+ >
+ {variableName}
+
+ {!isLastItem && }
+
+ );
+ };
+
+ // render a subtree inside an Accordion (ensures AccordionItem children are direct children of an Accordion)
+ const renderSubtreeAccordion = (subtree: any, basePath = "") => {
+ // Determine entries (keep variables first then groups for clarity)
+ const entries = Object.entries(subtree);
+ const variableEntries = entries.filter(([_, v]) => v === null);
+ const groupEntries = entries.filter(([_, v]) => v && typeof v === "object");
+
+ return (
+
+ {/* variables at this level */}
+ {variableEntries.length > 0 && (
+
+ {variableEntries.map(([name], idx) => {
+ const varPath = basePath ? `${basePath}/${name}` : name;
+ return (
+
+ );
+ })}
+
+ )}
+
+ {/* groups at this level */}
+ {groupEntries.map(([name, subtreeValue]) => {
+ const currentPath = basePath ? `${basePath}/${name}` : name;
+ return (
+
+
+ {name}
+
+
+ {/* recursively render children inside their own Accordion */}
+ {renderSubtreeAccordion(subtreeValue, currentPath)}
+
+
+ );
+ })}
+
+ );
+ };
+
+ // render full Variable list under "root" accordion item
const VariableList = (
- {filtered.length > 0 ? (
- filtered.map((val, idx) => (
-
- {
- setSelectedIndex(idx);
- setSelectedVar(val);
- GetDimInfo(val).then(e=>{setDimNames(e.dimNames); setDimArrays(e.dimArrays); setDimUnits(e.dimUnits)})
- if (popoverSide === "left") {
- setOpenMetaPopover(true);
- } else {
- setShowMeta(true);
- }
- }}
- >
- {val}
-
- {idx !== filtered.length - 1 && }
-
- ))
+ {Object.keys(tree).length > 0 ? (
+
+
+ /
+
+ {/* render the top-level subtree inside its own Accordion so nested AccordionItems are legal */}
+ {renderSubtreeAccordion(tree, "")}
+
+
+
) : (
- {query
- ? "No variables found matching your search."
- : "No variables available."}
+ {query ? "No variables found matching your search." : "No variables available."}
)}
@@ -187,11 +317,11 @@ const Variables = ({
+ className="absolute -top-8" // adjust top position as needed
+ style={{
+ left: `-${280}px`, // move to left of variable popover
+ }}
+ />
{meta && (
-
+
)}
@@ -219,7 +349,7 @@ const Variables = ({
{meta && (
) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+
diff --git a/src/components/zarr/GetMetadata.tsx b/src/components/zarr/GetMetadata.tsx
index df8e8d03..74f81f5e 100644
--- a/src/components/zarr/GetMetadata.tsx
+++ b/src/components/zarr/GetMetadata.tsx
@@ -57,6 +57,11 @@ export async function GetZarrMetadata(groupStore: Promise 1 ? pathParts.slice(0, -1).join('/') : undefined;
+ // ? should we query the node type instead or in addition?
variables.push({
name: item.path.substring(1),
@@ -69,7 +74,8 @@ export async function GetZarrMetadata(groupStore: Promise