-
- {openGroups.test && (
-
- - Simple Tree (3)
- - Weather Structure
-
- )}
+ if (isCollapsed) {
+ return (
+
+ {nodes.map((node) => (
+
+
+ {node.type === 'folder' ? (
+ expanded.has(node.id) ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+ ))}
+ );
+ }
-
-
- {openGroups.weather && (
-
- )}
-
-
+ const indentPaddingLeftPx = Math.max(0, level) * 16; // 16px per level
+
+ return (
+
+ {nodes.map((node) => (
+
+
node.type === 'folder' && toggleNode(node.id)}
+ >
+ {/* Expand/Collapse arrow for folders; spacer for files to keep alignment */}
+ {node.type === 'folder' ? (
+
+ {expanded.has(node.id) ? (
+
+ ) : (
+
+ )}
+
+ ) : (
+
+ )}
+
+ {/* Icon - made larger and more prominent */}
+
+ {node.type === 'folder' ? (
+ expanded.has(node.id) ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+ {/* Node name - made larger and more readable */}
+
+ {node.name}
+
+
+ {/* Count badge for folders */}
+ {node.type === 'folder' && node.children && (
+
+ {node.children.length}
+
+ )}
+
+
+ {/* Children */}
+ {node.type === 'folder' && expanded.has(node.id) && node.children && (
+
+ )}
+
+ ))}
+
);
};
-
-export default TreeView;
-
-
diff --git a/app/databuk/dashboard/src/components/sidebar/WebsiteInfo.tsx b/app/databuk/dashboard/src/components/sidebar/WebsiteInfo.tsx
deleted file mode 100644
index f5fa875d..00000000
--- a/app/databuk/dashboard/src/components/sidebar/WebsiteInfo.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-const WebsiteInfo: React.FC = () => {
- return (
-
-
- 🌐
- data-explorer.local
-
-
- 📅
- v1.0.0
-
-
- );
-};
-
-export default WebsiteInfo;
-
-
diff --git a/app/databuk/dashboard/src/components/sidebar/data/mockData.ts b/app/databuk/dashboard/src/components/sidebar/data/mockData.ts
new file mode 100644
index 00000000..8d81aaf0
--- /dev/null
+++ b/app/databuk/dashboard/src/components/sidebar/data/mockData.ts
@@ -0,0 +1,73 @@
+import type { TreeNode } from '../types/sidebar';
+
+export const mockTreeData: TreeNode[] = [
+ {
+ id: '1',
+ name: 'Sources',
+ type: 'folder',
+ children: [
+ {
+ id: '1-1',
+ name: 'Test Data',
+ type: 'folder',
+ children: [
+ {
+ id: '1-1-1',
+ name: 'Simple Tree',
+ type: 'folder',
+ children: [
+ { id: '1-1-1-1', name: 'Structure Data', type: 'file' },
+ { id: '1-1-1-2', name: 'Transport Data', type: 'file' },
+ { id: '1-1-1-3', name: 'Tensors Data', type: 'file' }
+ ]
+ },
+ {
+ id: '1-1-2',
+ name: 'Weather Structure',
+ type: 'folder',
+ children: [
+ { id: '1-1-2-1', name: 'Temperature', type: 'file' },
+ { id: '1-1-2-2', name: 'Humidity', type: 'file' },
+ { id: '1-1-2-3', name: 'Pressure', type: 'file' }
+ ]
+ }
+ ]
+ },
+ {
+ id: '1-2',
+ name: 'Weather Data',
+ type: 'folder',
+ children: [
+ {
+ id: '1-2-1',
+ name: 'yr.no',
+ type: 'folder',
+ children: [
+ { id: '1-2-1-1', name: 'Temperature', type: 'file' },
+ { id: '1-2-1-2', name: 'Longitude', type: 'file' },
+ { id: '1-2-1-3', name: 'Latitude', type: 'file' },
+ { id: '1-2-1-4', name: 'Wind Speed', type: 'file' },
+ { id: '1-2-1-5', name: 'Wind Direction', type: 'file' },
+ { id: '1-2-1-6', name: 'Precipitation', type: 'file' },
+ { id: '1-2-1-7', name: 'Cloud Cover', type: 'file' },
+ { id: '1-2-1-8', name: 'Visibility', type: 'file' },
+ { id: '1-2-1-9', name: 'Dew Point', type: 'file' },
+ { id: '1-2-1-10', name: 'UV Index', type: 'file' },
+ { id: '1-2-1-11', name: 'Air Quality', type: 'file' }
+ ]
+ },
+ {
+ id: '1-2-2',
+ name: 'OpenWeather',
+ type: 'folder',
+ children: [
+ { id: '1-2-2-1', name: 'Current Weather', type: 'file' },
+ { id: '1-2-2-2', name: 'Forecast', type: 'file' },
+ { id: '1-2-2-3', name: 'Historical Data', type: 'file' }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/app/databuk/dashboard/src/components/sidebar/hooks/useSidebar.ts b/app/databuk/dashboard/src/components/sidebar/hooks/useSidebar.ts
new file mode 100644
index 00000000..70b829c0
--- /dev/null
+++ b/app/databuk/dashboard/src/components/sidebar/hooks/useSidebar.ts
@@ -0,0 +1,18 @@
+import { useState } from 'react';
+
+export const useSidebar = () => {
+ const [isCollapsed, setIsCollapsed] = useState(false);
+ const [isVisible, setIsVisible] = useState(true);
+
+ const toggleCollapse = () => setIsCollapsed(!isCollapsed);
+ const hide = () => setIsVisible(false);
+ const show = () => setIsVisible(true);
+
+ return {
+ isCollapsed,
+ isVisible,
+ toggleCollapse,
+ hide,
+ show,
+ };
+};
diff --git a/app/databuk/dashboard/src/components/sidebar/index.ts b/app/databuk/dashboard/src/components/sidebar/index.ts
index 1ab0b443..0585de68 100644
--- a/app/databuk/dashboard/src/components/sidebar/index.ts
+++ b/app/databuk/dashboard/src/components/sidebar/index.ts
@@ -1,3 +1,6 @@
-export { default } from './Sidebar';
+export { default as Sidebar } from './Sidebar';
+export { TreeView } from './TreeView';
+export { useSidebar } from './hooks/useSidebar';
+export type { SidebarProps, TreeNode, TreeViewProps } from './types/sidebar';
diff --git a/app/databuk/dashboard/src/components/sidebar/sidebar.css b/app/databuk/dashboard/src/components/sidebar/sidebar.css
deleted file mode 100644
index 5722d7a5..00000000
--- a/app/databuk/dashboard/src/components/sidebar/sidebar.css
+++ /dev/null
@@ -1,73 +0,0 @@
-:root {
- --zf-bg: #ffffff;
- --zf-fg: #111827;
- --zf-fg-muted: #374151;
- --zf-border: #e5e7eb;
- --zf-accent: #2563eb;
- --zf-hover: #f3f4f6;
- --zf-sidebar-w: 260px;
- --zf-sidebar-w-collapsed: 60px;
-}
-
-.zf-sidebar {
- width: var(--zf-sidebar-w);
- height: 100vh;
- background: var(--zf-bg);
- border-right: 1px solid var(--zf-border);
- display: flex;
- flex-direction: column;
- transition: width 200ms ease;
-}
-
-.zf-sidebar--collapsed {
- width: var(--zf-sidebar-w-collapsed);
-}
-
-.zf-sidebar__header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px;
- border-bottom: 1px solid var(--zf-border);
-}
-
-.zf-sidebar__brand {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.zf-logo { font-size: 16px; }
-.zf-title { font-size: 16px; margin: 0; color: var(--zf-fg); }
-.zf-subtitle { font-size: 12px; margin: 0; color: var(--zf-fg-muted); }
-
-.zf-sidebar__controls { display: flex; gap: 6px; }
-.zf-btn { border: 1px solid var(--zf-border); background: var(--zf-bg); color: var(--zf-fg); padding: 2px 6px; border-radius: 6px; cursor: pointer; }
-.zf-btn--icon { width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; }
-.zf-btn:hover { background: var(--zf-hover); }
-
-.zf-sidebar__section { padding: 12px; }
-.zf-sidebar__divider { height: 1px; background: var(--zf-border); }
-
-.zf-website-info { display: grid; gap: 6px; }
-.zf-info-item { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--zf-fg-muted); }
-.zf-info-icon { width: 16px; text-align: center; }
-
-.zf-tree { display: block; }
-.zf-section-title { font-size: 12px; color: var(--zf-fg-muted); margin-bottom: 8px; }
-.zf-tree__group { margin-bottom: 6px; }
-.zf-tree__group-btn { width: 100%; display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: none; background: transparent; cursor: pointer; border-radius: 6px; text-align: left; }
-.zf-tree__group-btn:hover { background: var(--zf-hover); }
-.zf-caret { width: 14px; text-align: center; color: var(--zf-fg-muted); }
-.zf-tree__label { flex: 1; text-align: left; color: var(--zf-fg); font-size: 14px; }
-.zf-badge { background: #eef2ff; color: #4338ca; font-size: 11px; padding: 2px 6px; border-radius: 999px; }
-.zf-tree__icon { width: 16px; text-align: center; }
-.zf-tree__list { list-style: none; margin: 4px 0 8px 22px; padding: 0; }
-.zf-tree__item { padding: 4px 6px; border-radius: 6px; font-size: 13px; color: var(--zf-fg-muted); display: flex; align-items: center; gap: 8px; }
-.zf-tree__item:hover { background: var(--zf-hover); color: var(--zf-fg); }
-
-@media (max-width: 640px) {
- .zf-sidebar { position: fixed; z-index: 40; top: 0; left: 0; width: 80%; max-width: 320px; height: 100dvh; box-shadow: 0 10px 30px rgba(0,0,0,0.08); }
-}
-
-
diff --git a/app/databuk/dashboard/src/components/sidebar/types/sidebar.ts b/app/databuk/dashboard/src/components/sidebar/types/sidebar.ts
new file mode 100644
index 00000000..7cb62b22
--- /dev/null
+++ b/app/databuk/dashboard/src/components/sidebar/types/sidebar.ts
@@ -0,0 +1,17 @@
+export interface TreeNode {
+ id: string;
+ name: string;
+ type: 'folder' | 'file';
+ children?: TreeNode[];
+}
+
+export interface SidebarProps {
+ isCollapsed: boolean;
+ onToggle: () => void;
+ onClose: () => void;
+}
+
+export interface TreeViewProps {
+ nodes: TreeNode[];
+ level?: number;
+}
diff --git a/app/databuk/dashboard/src/index.css b/app/databuk/dashboard/src/index.css
index 08a3ac9e..888759f3 100644
--- a/app/databuk/dashboard/src/index.css
+++ b/app/databuk/dashboard/src/index.css
@@ -1,16 +1,92 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Custom scrollbar styles */
+.scrollbar-thin {
+ scrollbar-width: thin;
+ scrollbar-color: #d1d5db #f3f4f6;
+}
+
+.scrollbar-thin::-webkit-scrollbar {
+ width: 8px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-track {
+ background: #f3f4f6;
+ border-radius: 4px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb {
+ background: #d1d5db;
+ border-radius: 4px;
+ transition: background-color 0.2s;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb:hover {
+ background: #9ca3af;
+}
+
+/* Smooth scrolling for the entire page */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Better focus states */
+*:focus {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+}
+
+/* Improved hover transitions */
+.transition-all {
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+/* Overflow handling */
+.overflow-safe {
+ overflow: hidden;
+}
+
+.overflow-safe:hover {
+ overflow: visible;
+}
+
+/* Text overflow prevention */
+.text-overflow-safe {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Better button interactions */
+.button-interactive {
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.button-interactive:hover {
+ transform: translateY(-1px);
+}
+
+.button-interactive:active {
+ transform: translateY(0);
+}
+
+/* Enhanced shadows */
+.shadow-enhanced {
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+.shadow-enhanced:hover {
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
}
a {
diff --git a/app/databuk/dashboard/tailwind.config.js b/app/databuk/dashboard/tailwind.config.js
new file mode 100644
index 00000000..1c3b7e1b
--- /dev/null
+++ b/app/databuk/dashboard/tailwind.config.js
@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
From 6a4d12755233ab7fbe2d1d65d0ad8d0d99d74ece Mon Sep 17 00:00:00 2001
From: mfatihakbas <119815601+mfatihakbas@users.noreply.github.com>
Date: Sun, 31 Aug 2025 21:50:30 +0200
Subject: [PATCH 03/12] Tree test integration to the interface
---
app/databuk/dashboard/backend/README.md | 81 ++++++
app/databuk/dashboard/backend/__init__.py | 1 +
.../dashboard/backend/core/__init__.py | 1 +
app/databuk/dashboard/backend/core/config.py | 40 +++
app/databuk/dashboard/backend/main.py | 66 +++++
.../dashboard/backend/models/__init__.py | 1 +
app/databuk/dashboard/backend/models/tree.py | 32 +++
.../dashboard/backend/requirements.txt | 7 +
.../dashboard/backend/routers/__init__.py | 1 +
app/databuk/dashboard/backend/routers/tree.py | 222 +++++++++++++++++
app/databuk/dashboard/backend/run.py | 30 +++
.../dashboard/backend/services/__init__.py | 1 +
.../backend/services/tree_service.py | 231 ++++++++++++++++++
.../dashboard/backend/services/zarr_reader.py | 139 +++++++++++
app/databuk/dashboard/src/App.tsx | 211 ++++++++++++++--
.../src/components/sidebar/Sidebar.tsx | 135 ++++++----
.../src/components/sidebar/TreeView.tsx | 45 +++-
.../src/components/sidebar/data/mockData.ts | 73 ------
.../src/components/sidebar/types/sidebar.ts | 7 +
19 files changed, 1180 insertions(+), 144 deletions(-)
create mode 100644 app/databuk/dashboard/backend/README.md
create mode 100644 app/databuk/dashboard/backend/__init__.py
create mode 100644 app/databuk/dashboard/backend/core/__init__.py
create mode 100644 app/databuk/dashboard/backend/core/config.py
create mode 100644 app/databuk/dashboard/backend/main.py
create mode 100644 app/databuk/dashboard/backend/models/__init__.py
create mode 100644 app/databuk/dashboard/backend/models/tree.py
create mode 100644 app/databuk/dashboard/backend/requirements.txt
create mode 100644 app/databuk/dashboard/backend/routers/__init__.py
create mode 100644 app/databuk/dashboard/backend/routers/tree.py
create mode 100644 app/databuk/dashboard/backend/run.py
create mode 100644 app/databuk/dashboard/backend/services/__init__.py
create mode 100644 app/databuk/dashboard/backend/services/tree_service.py
create mode 100644 app/databuk/dashboard/backend/services/zarr_reader.py
delete mode 100644 app/databuk/dashboard/src/components/sidebar/data/mockData.ts
diff --git a/app/databuk/dashboard/backend/README.md b/app/databuk/dashboard/backend/README.md
new file mode 100644
index 00000000..16708e4a
--- /dev/null
+++ b/app/databuk/dashboard/backend/README.md
@@ -0,0 +1,81 @@
+# ZARR FUSE Dashboard Backend
+
+Modular FastAPI backend for exploring Zarr stores and building tree structures.
+
+## Architecture
+
+```
+backend/
+├── core/ # Configuration and utilities
+├── models/ # Pydantic response models
+├── services/ # Business logic (Zarr operations)
+├── routers/ # HTTP API endpoints
+├── main.py # FastAPI application
+├── run.py # Server startup script
+└── requirements.txt # Python dependencies
+```
+
+## Setup
+
+1. **Install dependencies:**
+ ```bash
+ cd backend
+ pip install -r requirements.txt
+ ```
+
+2. **Run the server:**
+ ```bash
+ python run.py
+ ```
+
+ Or directly with uvicorn:
+ ```bash
+ uvicorn main:app --reload --host 0.0.0.0 --port 8000
+ ```
+
+## API Endpoints
+
+### Tree Structure
+- `GET /api/tree/structure` - Get complete tree hierarchy
+- `GET /api/tree/node` - Get specific node information
+- `GET /api/tree/store/summary` - Get store summary
+
+### Health & Info
+- `GET /` - API information
+- `GET /health` - Health check
+- `GET /docs` - Interactive API documentation (Swagger UI)
+
+## Configuration
+
+The backend automatically detects test stores from the project structure:
+- **structure_tree.zarr**: Located at `zarr_fuse/test/workdir/structure_tree.zarr`
+- **CORS**: Configured for frontend development (`localhost:5173`)
+
+## Data Flow
+
+1. **ZarrReader**: Opens and explores Zarr stores
+2. **TreeService**: Builds hierarchical tree structures
+3. **TreeRouter**: Exposes HTTP endpoints
+4. **Frontend**: Consumes tree data for sidebar rendering
+
+## Testing
+
+Test the API:
+```bash
+# Health check
+curl http://localhost:8000/health
+
+# Get tree structure
+curl http://localhost:8000/api/tree/structure
+
+# Get specific node
+curl "http://localhost:8000/api/tree/node?path=root"
+```
+
+## Next Steps
+
+- [ ] Add weather data endpoints
+- [ ] Implement data sampling for plotting
+- [ ] Add authentication/authorization
+- [ ] Implement caching for large trees
+- [ ] Add metrics and monitoring
diff --git a/app/databuk/dashboard/backend/__init__.py b/app/databuk/dashboard/backend/__init__.py
new file mode 100644
index 00000000..9c8f0df2
--- /dev/null
+++ b/app/databuk/dashboard/backend/__init__.py
@@ -0,0 +1 @@
+# Backend package for ZARR FUSE Dashboard
diff --git a/app/databuk/dashboard/backend/core/__init__.py b/app/databuk/dashboard/backend/core/__init__.py
new file mode 100644
index 00000000..fa7eabcb
--- /dev/null
+++ b/app/databuk/dashboard/backend/core/__init__.py
@@ -0,0 +1 @@
+# Core configuration and utilities
diff --git a/app/databuk/dashboard/backend/core/config.py b/app/databuk/dashboard/backend/core/config.py
new file mode 100644
index 00000000..978845c4
--- /dev/null
+++ b/app/databuk/dashboard/backend/core/config.py
@@ -0,0 +1,40 @@
+import os
+from pathlib import Path
+from typing import Optional
+
+class Settings:
+ """Backend configuration settings."""
+
+ # Base paths - Fix the path calculation
+ # Backend is in: app/databuk/dashboard/backend/
+ # Need to go up to: zarr_fuse/ (project root)
+ PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent.parent
+ TEST_STORES_DIR = PROJECT_ROOT / "zarr_fuse" / "test" / "workdir"
+
+ # Zarr store paths
+ STRUCTURE_TREE_STORE = TEST_STORES_DIR / "structure_tree.zarr"
+
+ # API settings
+ API_V1_STR: str = "/api"
+ PROJECT_NAME: str = "ZARR FUSE Dashboard API"
+ VERSION: str = "1.0.0"
+
+ # CORS settings
+ BACKEND_CORS_ORIGINS: list[str] = [
+ "http://localhost:5173", # Vite dev server
+ "http://localhost:3000", # Alternative dev port
+ ]
+
+ # Feature flags
+ ENABLE_TEST_STORES: bool = True
+ ENABLE_WEATHER_DATA: bool = False # Will be enabled later
+
+ @classmethod
+ def get_store_path(cls, store_name: str) -> Optional[Path]:
+ """Get the full path to a Zarr store by name."""
+ if store_name == "structure_tree":
+ return cls.STRUCTURE_TREE_STORE
+ # Add more stores here as needed
+ return None
+
+settings = Settings()
diff --git a/app/databuk/dashboard/backend/main.py b/app/databuk/dashboard/backend/main.py
new file mode 100644
index 00000000..79fd7dea
--- /dev/null
+++ b/app/databuk/dashboard/backend/main.py
@@ -0,0 +1,66 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from contextlib import asynccontextmanager
+
+# Use absolute imports
+from core.config import settings
+from routers import tree
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ """Application lifespan events."""
+ # Startup
+ print(f"🚀 Starting {settings.PROJECT_NAME} v{settings.VERSION}")
+ print(f"📁 Test stores directory: {settings.TEST_STORES_DIR}")
+ print(f"🌳 Structure tree store: {settings.STRUCTURE_TREE_STORE}")
+
+ yield
+
+ # Shutdown
+ print(f"🛑 Shutting down {settings.PROJECT_NAME}")
+
+# Create FastAPI app
+app = FastAPI(
+ title=settings.PROJECT_NAME,
+ version=settings.VERSION,
+ description="API for exploring Zarr stores and building tree structures",
+ lifespan=lifespan
+)
+
+# Add CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.BACKEND_CORS_ORIGINS,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Include routers
+app.include_router(tree.router, prefix=settings.API_V1_STR)
+
+@app.get("/")
+async def root():
+ """Root endpoint with API information."""
+ return {
+ "message": f"Welcome to {settings.PROJECT_NAME}",
+ "version": settings.VERSION,
+ "docs": "/docs",
+ "redoc": "/redoc",
+ "api_prefix": settings.API_V1_STR
+ }
+
+@app.get("/health")
+async def health_check():
+ """Health check endpoint."""
+ return {"status": "healthy", "service": settings.PROJECT_NAME}
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(
+ "main:app",
+ host="0.0.0.0",
+ port=8000,
+ reload=True,
+ log_level="info"
+ )
diff --git a/app/databuk/dashboard/backend/models/__init__.py b/app/databuk/dashboard/backend/models/__init__.py
new file mode 100644
index 00000000..49033646
--- /dev/null
+++ b/app/databuk/dashboard/backend/models/__init__.py
@@ -0,0 +1 @@
+# Pydantic models for API responses
diff --git a/app/databuk/dashboard/backend/models/tree.py b/app/databuk/dashboard/backend/models/tree.py
new file mode 100644
index 00000000..31bde9fb
--- /dev/null
+++ b/app/databuk/dashboard/backend/models/tree.py
@@ -0,0 +1,32 @@
+from typing import List, Literal, Optional
+from pydantic import BaseModel, Field
+
+class TreeNode(BaseModel):
+ """Represents a node in the tree structure."""
+ id: str = Field(..., description="Unique identifier for the node (usually the path)")
+ name: str = Field(..., description="Display name of the node")
+ type: Literal["folder", "file"] = Field(..., description="Type of the node")
+ children: Optional[List["TreeNode"]] = Field(None, description="Child nodes if this is a folder")
+ path: str = Field(..., description="Full path to the node in the store")
+
+ class Config:
+ # Allow recursive models
+ from_attributes = True
+
+class TreeStructureResponse(BaseModel):
+ """Response model for tree structure endpoint."""
+ nodes: List[TreeNode] = Field(..., description="Root level nodes")
+ store_name: str = Field(..., description="Name of the Zarr store")
+ total_nodes: int = Field(..., description="Total number of nodes in the tree")
+
+class NodeDataResponse(BaseModel):
+ """Response model for individual node data."""
+ id: str = Field(..., description="Node identifier")
+ path: str = Field(..., description="Full path to the node")
+ type: Literal["folder", "file"] = Field(..., description="Type of the node")
+ variables: Optional[List[str]] = Field(None, description="Available variables if file")
+ metadata: Optional[dict] = Field(None, description="Additional metadata")
+ children_count: Optional[int] = Field(None, description="Number of children if folder")
+
+# Update forward references
+TreeNode.model_rebuild()
diff --git a/app/databuk/dashboard/backend/requirements.txt b/app/databuk/dashboard/backend/requirements.txt
new file mode 100644
index 00000000..e11ffcb8
--- /dev/null
+++ b/app/databuk/dashboard/backend/requirements.txt
@@ -0,0 +1,7 @@
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+pydantic==2.5.0
+zarr==2.16.1
+numpy==1.24.3
+pandas==2.0.3
+python-multipart==0.0.6
diff --git a/app/databuk/dashboard/backend/routers/__init__.py b/app/databuk/dashboard/backend/routers/__init__.py
new file mode 100644
index 00000000..44a11c04
--- /dev/null
+++ b/app/databuk/dashboard/backend/routers/__init__.py
@@ -0,0 +1 @@
+# API route handlers
diff --git a/app/databuk/dashboard/backend/routers/tree.py b/app/databuk/dashboard/backend/routers/tree.py
new file mode 100644
index 00000000..e07e49a3
--- /dev/null
+++ b/app/databuk/dashboard/backend/routers/tree.py
@@ -0,0 +1,222 @@
+from fastapi import APIRouter, HTTPException, Query
+from typing import Optional
+from pathlib import Path
+
+from models.tree import TreeStructureResponse, NodeDataResponse
+from services.tree_service import TreeService
+from core.config import settings
+
+router = APIRouter(prefix="/tree", tags=["tree"])
+
+@router.get("/structure", response_model=TreeStructureResponse)
+async def get_tree_structure(store_name: str = Query("structure_tree", description="Name of the Zarr store")):
+ """
+ Get the complete tree structure for a Zarr store.
+
+ Args:
+ store_name: Name of the store (default: structure_tree)
+
+ Returns:
+ TreeStructureResponse with the complete hierarchy
+ """
+ try:
+ # Get the store path from configuration
+ store_path = settings.get_store_path(store_name)
+ if not store_path:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store '{store_name}' not found in configuration"
+ )
+
+ if not store_path.exists():
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store path does not exist: {store_path}"
+ )
+
+ # Create tree service and build structure
+ tree_service = TreeService(store_path)
+ nodes = tree_service.build_tree_structure()
+
+ if nodes is None:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to build tree structure"
+ )
+
+ # Count total nodes recursively
+ def count_nodes(node_list):
+ count = len(node_list)
+ for node in node_list:
+ if node.children:
+ count += count_nodes(node.children)
+ return count
+
+ total_nodes = count_nodes(nodes)
+
+ return TreeStructureResponse(
+ nodes=nodes,
+ store_name=store_name,
+ total_nodes=total_nodes
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Internal server error: {str(e)}"
+ )
+
+@router.get("/node", response_model=NodeDataResponse)
+async def get_node_info(
+ path: str = Query("", description="Path to the node in the store"),
+ store_name: str = Query("structure_tree", description="Name of the Zarr store")
+):
+ """
+ Get detailed information about a specific node.
+
+ Args:
+ path: Path to the node (empty string for root)
+ store_name: Name of the store
+
+ Returns:
+ NodeDataResponse with node details
+ """
+ try:
+ # Get the store path from configuration
+ store_path = settings.get_store_path(store_name)
+ if not store_path:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store '{store_name}' not found in configuration"
+ )
+
+ if not store_path.exists():
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store path does not exist: {store_path}"
+ )
+
+ # Create tree service and get node info
+ tree_service = TreeService(store_path)
+ node_info = tree_service.get_node_info(path)
+
+ if node_info is None:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Node not found: {path}"
+ )
+
+ return NodeDataResponse(
+ id=path or "root",
+ path=path,
+ type=node_info["type"],
+ variables=node_info.get("variables"),
+ metadata=node_info.get("metadata"),
+ children_count=node_info.get("children_count")
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Internal server error: {str(e)}"
+ )
+
+@router.get("/file/data")
+async def get_file_data(
+ path: str = Query(..., description="Path to the file in the store"),
+ store_name: str = Query("structure_tree", description="Name of the Zarr store")
+):
+ """
+ Get the actual data from a file (array) in the store.
+
+ Args:
+ path: Path to the file
+ store_name: Name of the store
+
+ Returns:
+ Dictionary with file data and metadata
+ """
+ try:
+ # Get the store path from configuration
+ store_path = settings.get_store_path(store_name)
+ if not store_path:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store '{store_name}' not found in configuration"
+ )
+
+ if not store_path.exists():
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store path does not exist: {store_path}"
+ )
+
+ # Create tree service and get file data
+ tree_service = TreeService(store_path)
+ file_data = tree_service.get_file_data(path)
+
+ if file_data is None:
+ raise HTTPException(
+ status_code=404,
+ detail=f"File not found or not readable: {path}"
+ )
+
+ return file_data
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Internal server error: {str(e)}"
+ )
+
+@router.get("/store/summary")
+async def get_store_summary(store_name: str = Query("structure_tree", description="Name of the Zarr store")):
+ """
+ Get a summary of the store.
+
+ Args:
+ store_name: Name of the store
+
+ Returns:
+ Dictionary with store summary information
+ """
+ try:
+ # Get the store path from configuration
+ store_path = settings.get_store_path(store_name)
+ if not store_path:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store '{store_name}' not found in configuration"
+ )
+
+ if not store_path.exists():
+ raise HTTPException(
+ status_code=404,
+ detail=f"Store path does not exist: {store_path}"
+ )
+
+ # Create tree service and get summary
+ tree_service = TreeService(store_path)
+ summary = tree_service.get_store_summary()
+
+ if "error" in summary:
+ raise HTTPException(
+ status_code=500,
+ detail=summary["error"]
+ )
+
+ return summary
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Internal server error: {str(e)}"
+ )
diff --git a/app/databuk/dashboard/backend/run.py b/app/databuk/dashboard/backend/run.py
new file mode 100644
index 00000000..0343e3e3
--- /dev/null
+++ b/app/databuk/dashboard/backend/run.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+"""
+Simple script to run the ZARR FUSE Dashboard backend.
+"""
+
+import uvicorn
+import sys
+from pathlib import Path
+
+# Add the backend directory to Python path
+backend_dir = Path(__file__).parent
+sys.path.insert(0, str(backend_dir))
+
+if __name__ == "__main__":
+ print(f"🚀 Starting ZARR FUSE Dashboard Backend")
+ print(f"📁 Backend directory: {backend_dir}")
+ print(f"🌐 Server will be available at: http://localhost:8000")
+ print(f"📚 API documentation: http://localhost:8000/docs")
+ print(f"🔍 Health check: http://localhost:8000/health")
+ print()
+
+ # Run the server
+ uvicorn.run(
+ "main:app",
+ host="0.0.0.0",
+ port=8000,
+ reload=True,
+ log_level="info",
+ reload_dirs=[str(backend_dir)]
+ )
diff --git a/app/databuk/dashboard/backend/services/__init__.py b/app/databuk/dashboard/backend/services/__init__.py
new file mode 100644
index 00000000..6203184d
--- /dev/null
+++ b/app/databuk/dashboard/backend/services/__init__.py
@@ -0,0 +1 @@
+# Business logic services
diff --git a/app/databuk/dashboard/backend/services/tree_service.py b/app/databuk/dashboard/backend/services/tree_service.py
new file mode 100644
index 00000000..a2f5b654
--- /dev/null
+++ b/app/databuk/dashboard/backend/services/tree_service.py
@@ -0,0 +1,231 @@
+from pathlib import Path
+from typing import List, Dict, Any, Optional
+from models.tree import TreeNode
+from services.zarr_reader import ZarrReader
+import logging
+
+logger = logging.getLogger(__name__)
+
+class TreeService:
+ """Service for building tree structures from Zarr stores."""
+
+ def __init__(self, store_path: Path):
+ """Initialize with a Zarr store path."""
+ self.store_path = store_path
+ self._node_counter = 0
+
+ def _generate_node_id(self, path: str) -> str:
+ """Generate a unique node ID."""
+ self._node_counter += 1
+ return f"node_{self._node_counter}_{path.replace('/', '_')}"
+
+ def _get_node_name(self, path: str) -> str:
+ """Extract the display name from a path."""
+ if path == "":
+ return "root"
+ return path.split("/")[-1]
+
+ def _build_tree_recursive(self, reader: ZarrReader, group_path: str = "") -> List[TreeNode]:
+ """
+ Recursively build the tree structure for a group.
+
+ Args:
+ reader: ZarrReader instance
+ group_path: Path to the current group
+
+ Returns:
+ List of TreeNode objects
+ """
+ try:
+ children = reader.list_children(group_path)
+ nodes = []
+
+ for name, item_type in children:
+ current_path = f"{group_path}/{name}" if group_path else name
+ node_id = self._generate_node_id(current_path)
+
+ if item_type == 'group':
+ # Recursively get children for this group
+ child_nodes = self._build_tree_recursive(reader, current_path)
+
+ node = TreeNode(
+ id=node_id,
+ name=name,
+ type="folder",
+ path=current_path,
+ children=child_nodes
+ )
+ else: # item_type == 'array'
+ node = TreeNode(
+ id=node_id,
+ name=name,
+ type="file",
+ path=current_path,
+ children=None
+ )
+
+ nodes.append(node)
+
+ return nodes
+ except Exception as e:
+ logger.error(f"Error building tree for {group_path}: {e}")
+ return []
+
+ def build_tree_structure(self) -> Optional[List[TreeNode]]:
+ """
+ Build the complete tree structure for the store.
+
+ Returns:
+ List of root-level TreeNode objects, or None if failed
+ """
+ try:
+ with ZarrReader(self.store_path) as reader:
+ if not reader.open_store():
+ logger.error(f"Failed to open store: {self.store_path}")
+ return None
+
+ # Build the tree starting from root
+ root_nodes = self._build_tree_recursive(reader, "")
+
+ # If we have a single root group, return its children
+ # Otherwise, return the root nodes as-is
+ if len(root_nodes) == 1 and root_nodes[0].type == "folder":
+ return root_nodes[0].children or []
+
+ return root_nodes
+
+ except Exception as e:
+ logger.error(f"Error building tree structure: {e}")
+ return None
+
+ def get_node_info(self, node_path: str) -> Optional[Dict[str, Any]]:
+ """
+ Get detailed information about a specific node.
+
+ Args:
+ node_path: Path to the node
+
+ Returns:
+ Dictionary with node information, or None if failed
+ """
+ try:
+ with ZarrReader(self.store_path) as reader:
+ if not reader.open_store():
+ return None
+
+ # Check if it's a group or array
+ children = reader.list_children(node_path)
+ if children: # It's a group
+ group_info = reader.get_group_info(node_path)
+ if group_info:
+ return {
+ "type": "folder",
+ "path": node_path,
+ "children_count": len(children),
+ "children": [{"name": name, "type": item_type} for name, item_type in children],
+ "metadata": group_info.get("attrs", {})
+ }
+ else: # It's an array
+ array_info = reader.get_array_info(node_path)
+ if array_info:
+ return {
+ "type": "file",
+ "path": node_path,
+ "variables": [node_path.split("/")[-1]],
+ "metadata": array_info
+ }
+
+ return None
+
+ except Exception as e:
+ logger.error(f"Error getting node info for {node_path}: {e}")
+ return None
+
+ def get_file_data(self, file_path: str) -> Optional[Dict[str, Any]]:
+ """
+ Get the actual data from a file (array) in the store.
+
+ Args:
+ file_path: Path to the file
+
+ Returns:
+ Dictionary with file data and metadata, or None if failed
+ """
+ try:
+ with ZarrReader(self.store_path) as reader:
+ if not reader.open_store():
+ return None
+
+ # Get array info first
+ array_info = reader.get_array_info(file_path)
+ if not array_info:
+ return None
+
+ # Read the actual data
+ try:
+ import zarr
+ import numpy as np
+
+ # Open the specific array
+ array = zarr.open(str(self.store_path / file_path), mode='r')
+
+ # Get the data (for small arrays, get all; for large ones, get sample)
+ if array.size <= 1000: # Small array, get all data
+ data = array[:]
+ else: # Large array, get sample
+ # Get first 1000 elements or first dimension if multi-dimensional
+ if len(array.shape) == 1:
+ data = array[:1000]
+ else:
+ # For multi-dimensional, get first slice
+ indices = [slice(0, min(1000, dim)) for dim in array.shape]
+ data = array[tuple(indices)]
+
+ return {
+ "path": file_path,
+ "name": file_path.split("/")[-1],
+ "type": "array",
+ "shape": array.shape,
+ "dtype": str(array.dtype),
+ "size": array.size,
+ "data": data.tolist() if hasattr(data, 'tolist') else data,
+ "metadata": array_info.get("attrs", {}),
+ "sample_size": len(data) if hasattr(data, '__len__') else 1
+ }
+
+ except Exception as e:
+ logger.error(f"Error reading array data for {file_path}: {e}")
+ # Return metadata only if data reading fails
+ return {
+ "path": file_path,
+ "name": file_path.split("/")[-1],
+ "type": "array",
+ "error": f"Failed to read data: {str(e)}",
+ "metadata": array_info
+ }
+
+ except Exception as e:
+ logger.error(f"Error getting file data for {file_path}: {e}")
+ return None
+
+ def get_store_summary(self) -> Dict[str, Any]:
+ """Get a summary of the store."""
+ try:
+ with ZarrReader(self.store_path) as reader:
+ if not reader.open_store():
+ return {"error": "Failed to open store"}
+
+ store_info = reader.get_store_info()
+ root_children = reader.list_children("")
+
+ return {
+ "name": store_info.get("name", "Unknown"),
+ "path": str(self.store_path),
+ "root_items": len(root_children),
+ "type": "zarr",
+ "status": "active"
+ }
+
+ except Exception as e:
+ logger.error(f"Error getting store summary: {e}")
+ return {"error": str(e)}
diff --git a/app/databuk/dashboard/backend/services/zarr_reader.py b/app/databuk/dashboard/backend/services/zarr_reader.py
new file mode 100644
index 00000000..98ebb41b
--- /dev/null
+++ b/app/databuk/dashboard/backend/services/zarr_reader.py
@@ -0,0 +1,139 @@
+import zarr
+from pathlib import Path
+from typing import List, Dict, Any, Optional, Tuple
+import logging
+
+logger = logging.getLogger(__name__)
+
+class ZarrReader:
+ """Service for reading Zarr stores and building tree structures."""
+
+ def __init__(self, store_path: Path):
+ """Initialize with a Zarr store path."""
+ self.store_path = store_path
+ self._store = None
+ self._root_group = None
+
+ def open_store(self) -> bool:
+ """Open the Zarr store and return success status."""
+ try:
+ if not self.store_path.exists():
+ logger.error(f"Store path does not exist: {self.store_path}")
+ return False
+
+ self._store = zarr.open(str(self.store_path), mode='r')
+ self._root_group = self._store
+ logger.info(f"Successfully opened Zarr store: {self.store_path}")
+ return True
+ except Exception as e:
+ logger.error(f"Failed to open Zarr store {self.store_path}: {e}")
+ return False
+
+ def close_store(self):
+ """Close the Zarr store."""
+ if self._store is not None:
+ try:
+ self._store.close()
+ self._store = None
+ self._root_group = None
+ logger.info("Zarr store closed")
+ except Exception as e:
+ logger.error(f"Error closing store: {e}")
+
+ def get_store_info(self) -> Dict[str, Any]:
+ """Get basic information about the store."""
+ if not self._store:
+ return {}
+
+ try:
+ return {
+ "name": self.store_path.name,
+ "path": str(self.store_path),
+ "type": "zarr",
+ "attrs": dict(self._store.attrs) if hasattr(self._store, 'attrs') else {}
+ }
+ except Exception as e:
+ logger.error(f"Error getting store info: {e}")
+ return {}
+
+ def list_children(self, group_path: str = "") -> List[Tuple[str, str]]:
+ """
+ List children of a group.
+
+ Args:
+ group_path: Path to the group (empty string for root)
+
+ Returns:
+ List of tuples: (name, type) where type is 'group' or 'array'
+ """
+ if not self._store:
+ return []
+
+ try:
+ if group_path == "":
+ group = self._store
+ else:
+ group = self._store[group_path]
+
+ children = []
+ for name in group.keys():
+ item = group[name]
+ if hasattr(item, 'keys'): # It's a group
+ children.append((name, 'group'))
+ else: # It's an array
+ children.append((name, 'array'))
+
+ return children
+ except Exception as e:
+ logger.error(f"Error listing children for {group_path}: {e}")
+ return []
+
+ def get_array_info(self, array_path: str) -> Optional[Dict[str, Any]]:
+ """Get information about a specific array."""
+ if not self._store:
+ return None
+
+ try:
+ array = self._store[array_path]
+ if not hasattr(array, 'shape'): # Not an array
+ return None
+
+ return {
+ "path": array_path,
+ "shape": array.shape,
+ "dtype": str(array.dtype),
+ "attrs": dict(array.attrs) if hasattr(array, 'attrs') else {}
+ }
+ except Exception as e:
+ logger.error(f"Error getting array info for {array_path}: {e}")
+ return None
+
+ def get_group_info(self, group_path: str) -> Optional[Dict[str, Any]]:
+ """Get information about a specific group."""
+ if not self._store:
+ return None
+
+ try:
+ group = self._store[group_path]
+ if not hasattr(group, 'keys'): # Not a group
+ return None
+
+ children = self.list_children(group_path)
+
+ return {
+ "path": group_path,
+ "children_count": len(children),
+ "attrs": dict(group.attrs) if hasattr(group, 'attrs') else {}
+ }
+ except Exception as e:
+ logger.error(f"Error getting group info for {group_path}: {e}")
+ return None
+
+ def __enter__(self):
+ """Context manager entry."""
+ self.open_store()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Context manager exit."""
+ self.close_store()
diff --git a/app/databuk/dashboard/src/App.tsx b/app/databuk/dashboard/src/App.tsx
index e5714cde..7c1a215b 100644
--- a/app/databuk/dashboard/src/App.tsx
+++ b/app/databuk/dashboard/src/App.tsx
@@ -1,15 +1,97 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { Sidebar } from './components/sidebar';
import { Database } from 'lucide-react';
+// Types for the tree data from backend
+interface TreeNode {
+ id: string;
+ name: string;
+ type: 'folder' | 'file';
+ path: string;
+ children?: TreeNode[];
+}
+
+interface TreeResponse {
+ nodes: TreeNode[];
+ store_name: string;
+ total_nodes: number;
+}
+
+interface FileData {
+ path: string;
+ name: string;
+ type: string;
+ shape: number[];
+ dtype: string;
+ size: number;
+ data: any[];
+ metadata: any;
+ sample_size: number;
+}
+
function App() {
const [isCollapsed, setIsCollapsed] = useState(false);
const [isVisible, setIsVisible] = useState(true);
+ const [treeData, setTreeData] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [fileLoading, setFileLoading] = useState(false);
- return (
-
- {/* Global opener when sidebar is hidden (same look/position as header icon) */}
- {!isVisible && (
+ // Fetch tree data from backend
+ useEffect(() => {
+ const fetchTreeData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const response = await fetch('http://localhost:8000/api/tree/structure');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data: TreeResponse = await response.json();
+ setTreeData(data.nodes);
+ console.log('Tree data loaded:', data);
+ } catch (err) {
+ console.error('Failed to fetch tree data:', err);
+ setError(err instanceof Error ? err.message : 'Failed to fetch data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTreeData();
+ }, []);
+
+ // Handle file click from sidebar
+ const handleFileClick = async (filePath: string, fileName: string) => {
+ try {
+ setFileLoading(true);
+ setSelectedFile(null);
+
+ console.log('Fetching file data for:', filePath);
+
+ const response = await fetch(`http://localhost:8000/api/tree/file/data?path=${encodeURIComponent(filePath)}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const fileData: FileData = await response.json();
+ setSelectedFile(fileData);
+ console.log('File data loaded:', fileData);
+ } catch (err) {
+ console.error('Failed to fetch file data:', err);
+ setError(err instanceof Error ? err.message : 'Failed to fetch file data');
+ } finally {
+ setFileLoading(false);
+ }
+ };
+
+ // Global opener when sidebar is hidden
+ if (!isVisible) {
+ return (
+
- )}
-
- {isVisible && (
-
setIsCollapsed(!isCollapsed)}
- onClose={() => setIsVisible(false)}
- />
- )}
+
+
+
+
ZARR FUSE Dashboard
+
+ Welcome to the Data Explorer Platform. This is a placeholder for the main content area.
+
+
+
+
Project Status
+
Basic setup completed successfully
+
+
+
Next Steps
+
Ready for sidebar development
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
setIsCollapsed(!isCollapsed)}
+ onClose={() => setIsVisible(false)}
+ treeData={treeData}
+ loading={loading}
+ error={error}
+ onFileClick={handleFileClick}
+ />
Content Area
-
This is a placeholder. We are focusing on the sidebar in checkpoint 1.
+
This is a placeholder. We are focusing on the sidebar in checkpoint 1.
+
+ {/* File Data Display */}
+ {selectedFile && (
+
+
📁 {selectedFile.name}
+
+
+
File Information
+
+
Path: {selectedFile.path}
+
Type: {selectedFile.type}
+
Shape: {selectedFile.shape.join(' × ')}
+
Data Type: {selectedFile.dtype}
+
Size: {selectedFile.size.toLocaleString()}
+
Sample Size: {selectedFile.sample_size.toLocaleString()}
+
+
+
+
Data Preview
+
+
+ {JSON.stringify(selectedFile.data, null, 2)}
+
+
+
+
+ {selectedFile.metadata && Object.keys(selectedFile.metadata).length > 0 && (
+
+
Metadata
+
+
+ {JSON.stringify(selectedFile.metadata, null, 2)}
+
+
+
+ )}
+
+ )}
+
+ {/* File Loading State */}
+ {fileLoading && (
+
+
+
+
Loading file data...
+
+
+ )}
+
+ {/* Instructions */}
+ {!selectedFile && !fileLoading && (
+
+
💡 How to use
+
+ Click on any file (📄) in the sidebar to view its data and metadata here.
+ Folders (📁) can be expanded/collapsed by clicking on them.
+
+
+ )}
+
+ {/* Debug info */}
+
+
Backend Status:
+ {loading &&
Loading tree data...
}
+ {error &&
Error: {error}
}
+ {!loading && !error && (
+
+ Tree data loaded successfully! Total nodes: {treeData.length}
+
+ )}
+
diff --git a/app/databuk/dashboard/src/components/sidebar/Sidebar.tsx b/app/databuk/dashboard/src/components/sidebar/Sidebar.tsx
index 2b6ec40b..68791a5f 100644
--- a/app/databuk/dashboard/src/components/sidebar/Sidebar.tsx
+++ b/app/databuk/dashboard/src/components/sidebar/Sidebar.tsx
@@ -1,18 +1,24 @@
import React from 'react';
-import { ChevronLeft, ChevronRight, X, Database, BarChart3, Globe} from 'lucide-react';
+import { ChevronLeft, ChevronRight, X, Database, BarChart3, Globe, Loader2, AlertCircle } from 'lucide-react';
import { TreeView } from './TreeView';
-import { mockTreeData } from './data/mockData';
import type { SidebarProps } from './types/sidebar';
-const Sidebar: React.FC = ({ isCollapsed, onToggle, onClose }) => {
+const Sidebar: React.FC = ({
+ isCollapsed,
+ onToggle,
+ onClose,
+ treeData,
+ loading,
+ error,
+ onFileClick
+}) => {
return (