-
Notifications
You must be signed in to change notification settings - Fork 1
Add dashboard for explore data #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- update minio endpoint url in: - readme - dashboard/data/prepare_data.ipynb - dashboard/streamlit-css.py - notebooks/footprints/footprints.ipynb - notebooks/how_to.ipynb - notebooks/modified-gravity-tests/01_lastberu_cosmo_ground.ipynb - notebooks/proposals/proposals.ipynb
- remove dashboard directory and all its contents - this includes streamlit app, data files, and requirements
- scaffold basic React/TypeScript application with Vite - configure ESLint for code quality and formatting - set up basic UI layout with MUI components - add React Query for data fetching - implement Recoil for state management - create initial components: App, DataTables, SkyMap, Gallery, FiltersDrawer, ObjectsTable - implement data loading from static JSON files - implement basic filtering and selection of objects - add basic sky map visualization with object positions - add cutout image gallery with placeholder images - implement basic UI theme and styling - add .gitignore file to exclude node_modules and other unnecessary files - add public directory with placeholder data and images - add types file for data structures
- read data from minio - process data - save data to public/data directory
- configures ci to automatically build and deploy the react dashboard to github pages - sets up python and node.js environments, installs dependencies, prepares data from minio, and builds the project - deploys the built dashboard to github pages on pushes to the main or streamlit branches
- install pyarrow and fastparquet for enhanced data processing capabilities
WalkthroughIntroduces a React + TypeScript dashboard under dashboard/ with data loading, filtering, sky map, and cutout gallery. Adds data preparation script pulling artifacts from MinIO. Sets up Vite, ESLint, theme, and CI to build/deploy to GitHub Pages. Adds a devcontainer. Updates notebook MinIO endpoints and a README link. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Dev as Developer
participant GH as GitHub
participant WF as Actions: deploy.yml
participant MinIO as MinIO (secrets)
participant Pages as GitHub Pages
Dev->>GH: push to dashboard/**
GH-->>WF: trigger build job
WF->>WF: setup Python/Node
WF->>MinIO: run prepare_data.py (env secrets)
MinIO-->>WF: data files (JSON)
WF->>WF: npm ci && vite build (BASE_PATH)
WF->>Pages: upload-pages-artifact
Note over WF,Pages: On main/streamlit only
GH-->>WF: trigger deploy job
WF->>Pages: deploy artifact
Pages-->>Dev: page URL output
sequenceDiagram
autonumber
actor User
participant App as Dashboard App
participant RQ as React Query
participant FS as /public/data/*.json
participant MinIO as MinIO Gateway
User->>App: open dashboard
App->>RQ: fetch db, consolidated, dictionary, cutouts
RQ->>FS: GET /data/database.json
RQ->>FS: GET /data/consolidated_database.json
RQ->>FS: GET /data/dictionary.json
RQ->>FS: GET /data/cutouts.json
FS-->>RQ: JSON datasets
RQ-->>App: resolved data
App->>User: render tables, sky map, filters
User->>App: open Cutouts/Gallery
App->>MinIO: resolve cutout URL (ngrok-aware)
MinIO-->>App: image or blob URL
App->>User: show thumbnails/modal
sequenceDiagram
autonumber
participant Script as prepare_data.py
participant MinIO as MinIO
participant FS as dashboard/public/data/
Script->>MinIO: download Processed_Cutouts.parquet
Script->>MinIO: download FITS.parquet
Script->>Script: merge/normalize cutouts (bands, paths)
Script->>FS: write cutouts.json
Script->>MinIO: download Consolidated_Data.csv
Script->>Script: type normalize
Script->>FS: write consolidated_database.json
Script->>MinIO: download Database.csv
Script->>Script: build dictionary, expand multi-values
Script->>FS: write dictionary.json
Script->>FS: write database.json
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- trigger deployment workflow only when changes occur in the dashboard directory
- introduce features, quick start guide, data files, theming, and scripts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 29
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
notebooks/how_to.ipynb (1)
64-72: Remove hardcoded MinIO credentials from all notebooks
HardcodedMINIO_ENDPOINT_URL,ACCESS_KEY, andSECRET_KEYare present in:
- notebooks/how_to.ipynb (lines 64–72)
- notebooks/proposals/proposals.ipynb (lines 38–44)
- notebooks/modified-gravity-tests/01_LaStBeRu_cosmo_ground.ipynb (lines 85–91)
- notebooks/footprints/footprints.ipynb (lines 54–60)
Load these via
os.getenv("MINIO_ENDPOINT_URL") os.getenv("MINIO_ACCESS_KEY") os.getenv("MINIO_SECRET_KEY")and raise an error if any are unset. Rotate exposed credentials and audit access.
rg -n -C1 -S '(MINIO_.*(KEY|SECRET)|slcomp)' notebooks/**/*.ipynbnotebooks/proposals/proposals.ipynb (3)
38-46: Remove hard-coded MinIO endpoint and credentials (secrets in repo).Endpoint, access key, and secret are committed in plain text. Move to environment variables and fail fast if missing.
Apply this diff:
- MINIO_ENDPOINT_URL = "nonarithmetically-undeliberating-janelle.ngrok-free.app" - ACCESS_KEY = "slcomp" - SECRET_KEY = "slcomp@data" - client = Minio( - MINIO_ENDPOINT_URL, - access_key=ACCESS_KEY, - secret_key=SECRET_KEY, - secure=True, - ) + MINIO_ENDPOINT_URL = os.environ["MINIO_ENDPOINT_URL"] + ACCESS_KEY = os.environ["MINIO_ACCESS_KEY"] + SECRET_KEY = os.environ["MINIO_SECRET_KEY"] + MINIO_SECURE = os.getenv("MINIO_SECURE", "1") not in {"0", "false", "False"} + client = Minio( + MINIO_ENDPOINT_URL, + access_key=ACCESS_KEY, + secret_key=SECRET_KEY, + secure=MINIO_SECURE, + )
534-536: Fix RA range logic (6h–20h → degrees).Current expression uses
8*20(160°), not20*15(300°). This incorrectly filters RA.Apply this diff:
- Database = Database.query( - "(RA>=6*15 and RA <=8*20) and (DEC >=-75 and DEC <= 15)" - ).reset_index(drop=True) + Database = Database.query( + "(RA >= 6*15 and RA <= 20*15) and (DEC >= -75 and DEC <= 15)" + ).reset_index(drop=True)
727-731: NaN handling in filter doesn’t work withisin([np.nan, ...]).
NaNnever matches viaisin. Useisna()for missing System_Type.Apply this diff:
- data_with_match = data_with_match[ - data_with_match.System_Type.isin([np.nan, "Single Lens Galaxy"]) - ].reset_index(drop=True) + data_with_match = data_with_match[ + data_with_match["System_Type"].isna() | (data_with_match["System_Type"] == "Single Lens Galaxy") + ].reset_index(drop=True)
🧹 Nitpick comments (62)
dashboard/src/components/VirtualizedList.tsx (7)
3-9: Ensure stable React keys; optionally add keyExtractor.As written, keys depend on renderItem providing them. To avoid “Each child should have a unique key” warnings, accept an optional keyExtractor and apply it where items are rendered.
interface VirtualizedListProps<T = unknown> { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; itemHeight: number; containerHeight: number; overscan?: number; + keyExtractor?: (item: T, index: number) => React.Key; } @@ - overscan = 5 + overscan = 5, + keyExtractor }: VirtualizedListProps<T>) => { @@ - {items.map((item, index) => renderItem(item, index))} + {items.map((item, index) => { + const node = renderItem(item, index); + const key = keyExtractor ? keyExtractor(item, index) : index; + return React.isValidElement(node) + ? React.cloneElement(node, { key }) + : <div key={key}>{node}</div>; + })} @@ - {visibleItems.map(({ item, index }) => - renderItem(item, index) - )} + {visibleItems.map(({ item, index }) => { + const node = renderItem(item, index); + const key = keyExtractor ? keyExtractor(item, index) : index; + return React.isValidElement(node) + ? React.cloneElement(node, { key }) + : <div key={key}>{node}</div>; + })}Also applies to: 12-17, 50-52, 75-77
27-29: Clamp overscan to a non-negative integer.Guards against negative or fractional overscan values.
- const start = Math.max(0, startIndex - overscan); - const end = Math.min(items.length - 1, endIndex + overscan); + const safeOverscan = Math.max(0, Math.floor(overscan)); + const start = Math.max(0, startIndex - safeOverscan); + const end = Math.min(items.length - 1, endIndex + safeOverscan);
41-43: Reduce scroll jank with rAF batching.Scrolling can fire dozens of events per frame; batch setState with requestAnimationFrame.
+ const rafRef = React.useRef<number | null>(null); - const handleScroll = React.useCallback((e: React.UIEvent<HTMLDivElement>) => { - setScrollTop(e.currentTarget.scrollTop); - }, []); + const handleScroll = React.useCallback((e: React.UIEvent<HTMLDivElement>) => { + const st = e.currentTarget.scrollTop; + if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(() => setScrollTop(st)); + }, []); + + React.useEffect(() => { + return () => { + if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); + }; + }, []);
45-47: Clamp scrollTop when data or sizes shrink to avoid stale window.If totalHeight decreases below current scrollTop, visible window can be off until the next user scroll.
const totalHeight = items.length * itemHeight; + React.useEffect(() => { + const maxScroll = Math.max(0, totalHeight - containerHeight); + setScrollTop((s) => Math.min(s, maxScroll)); + }, [totalHeight, containerHeight]);
61-62: Remove unused positioning on outer container.position: 'relative' isn’t needed here; the absolutely positioned layer is relative to its own parent container below.
- overflowY: 'auto', - position: 'relative' + overflowY: 'auto'
67-73: Hint GPU acceleration for smoother scrolling.Small win: willChange helps browsers plan for frequent transforms.
style={{ transform: `translateY(${visibleIndices.start * itemHeight}px)`, position: 'absolute', top: 0, left: 0, - right: 0 + right: 0, + willChange: 'transform' }}
47-54: Make the “20 items” heuristic configurable or data-driven.Consider a prop like minVirtualizeCount = 20 or auto-enable when items.length * itemHeight > containerHeight.
dashboard/public/data/.gitkeep (1)
1-2: Make the generator path explicit (minor clarity).Reference the exact script path to avoid ambiguity when browsing the repo.
-# Files are generated by prepare_data.py which fetches data from MinIO +# Files are generated by dashboard/prepare_data.py which fetches data from MinIOnotebooks/how_to.ipynb (7)
23-27: Importdisplayexplicitly for portability.Fixes the Ruff F821 and makes the notebook runnable after export.
import io +from IPython.display import display
333-338: MinIO client: prefer streaming/read() over.data(API compatibility + memory).
.dataisn’t guaranteed across minio client versions and loads entire objects into memory. Useread()or stream.Example refactor (apply similarly to other occurrences):
-Database_object = client.get_object("slcomp", "Data/Database.csv").data -Database = pd.read_csv(io.StringIO(Database_object.decode("utf-8")), low_memory=False, dtype=object) +resp = client.get_object("slcomp", "Data/Database.csv") +try: + Database = pd.read_csv(io.StringIO(resp.read().decode("utf-8")), low_memory=False, dtype=object) +finally: + resp.close() + resp.release_conn()Also applies to: 752-761, 1098-1107, 1436-1445, 1691-1694, 2008-2015
1882-1886: Ensure directories exist beforefget_objectwrites (robustness).Current code may fail if nested directories aren’t present.
-[ - client.fget_object("slcomp", "Cutouts/" + file_path, file_path) - for file_path in Cutouts_Catalog.query('JNAME=="J114833.1+193003.2"').file_path -] +for file_path in Cutouts_Catalog.query('JNAME=="J114833.1+193003.2"').file_path: + os.makedirs(os.path.dirname(file_path), exist_ok=True) + client.fget_object("slcomp", f"Cutouts/{file_path}", file_path)
2260-2266: Same: create parent dirs before saving processed cutouts.-[ - client.fget_object("slcomp", "Cutouts/" + file_path, file_path) - for file_path in Processed_Cutouts_Catalog.query('JNAME=="J114833.1+193003.2"').file_path -] +for file_path in Processed_Cutouts_Catalog.query('JNAME=="J114833.1+193003.2"').file_path: + os.makedirs(os.path.dirname(file_path), exist_ok=True) + client.fget_object("slcomp", f"Cutouts/{file_path}", file_path)
2978-2980: Boolean filter should not compare to string literal.If
is_rgbis boolean, compare as a boolean or filter directly.-len(Processed_Cutouts_Catalog.query('is_rgb=="True"')) +Processed_Cutouts_Catalog["is_rgb"].sum() # or: len(Processed_Cutouts_Catalog[Processed_Cutouts_Catalog.is_rgb])
2403-2404: Avoidnp.hstackon mixed list/scalar cells; useexplode.
hstackrisks splitting strings into characters if a cell isn’t a list.-pd.DataFrame(np.hstack(Database.Lens_Type), columns=["Lens_Type"]).value_counts() +tmp = Database[["Lens_Type"]].copy() +tmp["Lens_Type"] = tmp["Lens_Type"].apply(lambda v: v if isinstance(v, list) else [v]) +tmp.explode("Lens_Type")["Lens_Type"].value_counts() -pd.DataFrame(np.hstack(Database.Source_Type), columns=["Source_Type"]).value_counts() +tmp = Database[["Source_Type"]].copy() +tmp["Source_Type"] = tmp["Source_Type"].apply(lambda v: v if isinstance(v, list) else [v]) +tmp.explode("Source_Type")["Source_Type"].value_counts()Also applies to: 2437-2438
1-3045: Strip heavy notebook outputs before committing (repo hygiene).Outputs bloat diffs and repo size; keep notebooks lightweight or use nbdime/LFS.
README.md (1)
52-55: Endpoint drift: README MinIO link differs from notebook endpoint.README uses ruggedly-quaky-maricruz…, notebooks use nonarithmetically-undeliberating-janelle… Consolidate to a single canonical URL or document that endpoints are ephemeral.
Suggestion:
- Define MINIO_ENDPOINT_URL once (e.g., in an .env example) and reference it in README and notebooks.
- Consider a stable subdomain (e.g., minio.slcomp.org) that CNAMEs to the current tunnel.
dashboard/.gitignore (2)
15-15: Typo: TypeScript build info pattern.The file is typically
tsconfig.tsbuildinfo. Use a broader pattern.-tsbuildinfo.tsbuildinfo +*.tsbuildinfo
12-12: Consider ignoring parquet artifacts if added later.Future-proof the generated data rule.
public/data/*.json +public/data/*.parquetnotebooks/footprints/footprints.ipynb (2)
54-54: Make endpoint configurable; avoid hardcoding ephemeral ngrok URLBind MINIO_ENDPOINT_URL from env (optionally support a local default) to avoid future breakage and simplify CI/CD and local dev.
-import os +import os -MINIO_ENDPOINT_URL = "nonarithmetically-undeliberating-janelle.ngrok-free.app" +MINIO_ENDPOINT_URL = os.getenv( + "MINIO_ENDPOINT_URL", + # Optional local/dev fallback (adjust as appropriate) + "nonarithmetically-undeliberating-janelle.ngrok-free.app" +)Consider centralizing this in a small config module used by notebooks and dashboard/prepare_data.py, and document required env vars in README and GH Actions (use repository Secrets).
57-57: Be explicit about TLS when initializing Minio clientGiven the ngrok endpoint is HTTPS, pass secure=True explicitly to avoid ambiguity across client versions.
client = Minio( MINIO_ENDPOINT_URL, access_key=ACCESS_KEY, secret_key=SECRET_KEY, + secure=True, )notebooks/proposals/proposals.ipynb (1)
996-1058: Close image resources and simplify subplot loop.Use context managers to avoid file/resource leaks and potential warnings.
Apply this diff:
- data_io = io.BytesIO( - client.get_object("slcomp", "Cutouts/" + data.iloc[k].file_path).data - ) - # im = plt.imread(data_io) - im = Image.open(data_io) - im.load() - ax = plt.subplot(gs[i, j]) + data_io = io.BytesIO( + client.get_object("slcomp", "Cutouts/" + data.iloc[k].file_path).data + ) + ax = plt.subplot(gs[i, j]) + with Image.open(data_io) as im: + im.load() if data.iloc[k].survey == "CS82": - ax.imshow(im, interpolation="lanczos", aspect="auto", cmap="gray") + ax.imshow(im, interpolation="lanczos", aspect="auto", cmap="gray") else: - ax.imshow(im, interpolation="lanczos", aspect="auto") + ax.imshow(im, interpolation="lanczos", aspect="auto") ax.set_xticklabels([]) ax.set_yticklabels([]) ax.set_xticks([]) ax.set_yticks([]) @@ - im = Image.open(f"mosaics/{jname}.png") - im2 = im.crop(im.getbbox()) - im2.save(f"mosaics/{jname}.png") + with Image.open(f"mosaics/{jname}.png") as _im: + im2 = _im.crop(_im.getbbox()) + im2.save(f"mosaics/{jname}.png")dashboard/tsconfig.json (1)
2-21: Tighten TS config for Vite/React.Add Vite client types and convenient import aliasing.
Apply this diff:
"compilerOptions": { @@ - "jsx": "react-jsx" + "jsx": "react-jsx", + "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } },dashboard/src/hooks/useDebounce.ts (1)
6-14: Guard zero/negative delays.Immediate update when
delay <= 0avoids unnecessary timers.Apply this diff:
useEffect(() => { - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); + if (delay <= 0) { + setDebouncedValue(value); + return; + } + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay);dashboard/package.json (2)
6-11: Add a dedicated typecheck and fix script.Keeps build fast and enables quick autofixes.
Apply this diff:
"scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc --noEmit && vite build", "preview": "vite preview", - "lint": "eslint src --ext .ts,.tsx" + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "typecheck": "tsc --noEmit" },
1-5: Add engines for predictable CI/dev behavior.Pin Node to the version Vite/TS expect.
Apply this diff:
{ "name": "slcomp-dashboard", "version": "0.1.0", "private": true, "type": "module", + "engines": { + "node": ">=18.18.0" + },.devcontainer/devcontainer.json (1)
20-20: Security note: disabling CORS/XSRF.Okay for local dev, but avoid committing insecure defaults; add a comment or guard via env flag.
Apply this diff:
- "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; [ -f requirements.txt ] && pip3 install --user -r requirements.txt; pip3 install --user streamlit; echo '✅ Packages installed and Requirements met'", + "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; [ -f requirements.txt ] && pip3 install --user -r requirements.txt; pip3 install --user streamlit; echo '✅ Packages installed and Requirements met' # dev-only",dashboard/vite.config.ts (2)
4-7: Normalize BASE_PATH for safety.Ensures leading/trailing slashes so GH Pages routing is correct regardless of input.
Apply this diff:
-// For GitHub Pages deployment: set BASE_PATH environment variable to your repo name -// Example: export BASE_PATH=/slcomp/ before building -const basePath = process.env.BASE_PATH || '/'; +// For GitHub Pages deployment: set BASE_PATH to your repo name (with or without slashes) +const rawBase = process.env.BASE_PATH || '/'; +const basePath = (() => { + let b = rawBase.trim(); + if (!b.startsWith('/')) b = '/' + b; + if (!b.endsWith('/')) b = b + '/'; + return b; +})();
23-31: Consider strict port to avoid collision during dev.Small DX improvement.
Apply this diff:
server: { - port: 5173, + port: 5173, + strictPort: true,dashboard/src/types.ts (3)
1-4: Avoidanyin data records.Prefer
unknownand narrow at use sites to keep type-safety.Apply this diff:
export interface DataRecord { JNAME: string; - [key: string]: any; // dynamic attributes + [key: string]: unknown; // dynamic attributes }
6-9: Same here for ConsolidatedRecord.Apply this diff:
export interface ConsolidatedRecord { JNAME: string; - [key: string]: any; + [key: string]: unknown; }
18-21: Avoidanyin DictionaryEntry.Keeps downstream code safer.
Apply this diff:
export interface DictionaryEntry { JNAME?: string[] | string; - [key: string]: any; + [key: string]: unknown; }dashboard/eslint.config.js (2)
3-3: Ignore pattern may miss other build artifacts.If you want to fully avoid scanning generated/static files, consider also ignoring coverage/, .vite/, and public/data/*.json (though files: only targets src/, so this is optional).
21-26: Consider enabling unused-disable reporting.Helps keep config tidy by flagging stale // eslint-disable lines.
{ files: ['src/**/*.{ts,tsx}'], + linterOptions: { reportUnusedDisableDirectives: true }, languageOptions: {dashboard/prepare_data.py (2)
24-27: Make output path robust to working directory.Resolve DATA_PATH relative to this script so CI/local runs are consistent.
-# Data will be saved to public/data directory -DATA_PATH = "public/data" +# Data will be saved to dashboard/public/data (relative to this file) +DATA_PATH = str((pathlib.Path(__file__).parent / "public" / "data").resolve())
74-76: Consider ordered categories for band.If band sorting matters in the UI, set ordered=True to enforce intended order.
-cutouts.band = pd.Categorical( - cutouts.band, categories=["u", "g", "r", "i", "z", "y", "trilogy", "lsb"] -) +cutouts.band = pd.Categorical( + cutouts.band, + categories=["u", "g", "r", "i", "z", "y", "trilogy", "lsb"], + ordered=True, +).github/workflows/deploy.yml (2)
23-23: Clean trailing spaces to satisfy linters.Minor formatting to appease YAML linters and keep diffs clean.
- uses: actions/checkout@v4 - + uses: actions/checkout@v4 @@ - + @@ - + @@ - + @@ - + @@ - + @@ - +Also applies to: 28-28, 35-35, 40-40, 49-49, 54-54, 57-57, 63-63
36-54: Optional: set working-directory instead of cd for clarity.Reduces repetition and chances of path mistakes.
- - name: Install Python dependencies - run: | - cd dashboard - pip install pandas numpy minio pyarrow fastparquet + - name: Install Python dependencies + working-directory: dashboard + run: pip install pandas numpy minio pyarrow fastparquet @@ - - name: Prepare data from MinIO + - name: Prepare data from MinIO env: MINIO_ENDPOINT_URL: ${{ secrets.MINIO_ENDPOINT_URL }} MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }} MINIO_SECRET_KEY: ${{ secrets.MINIO_SECRET_KEY }} - run: | - cd dashboard - python prepare_data.py + working-directory: dashboard + run: python prepare_data.py @@ - - name: Install dependencies - run: | - cd dashboard - npm ci + - name: Install dependencies + working-directory: dashboard + run: npm ci @@ - - name: Build - run: | - cd dashboard - export BASE_PATH=/slcomp/ - npm run build + - name: Build + working-directory: dashboard + env: + BASE_PATH: /slcomp/ + run: npm run build @@ - - name: Upload artifact + - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: 'dashboard/dist'dashboard/src/main.tsx (2)
8-24: Harden ErrorBoundary types and add a lightweight resetTighten types and provide a simple “try again” without a full reload.
-class ErrorBoundary extends React.Component<{children: React.ReactNode}, {error: any}> { - constructor(props:any){ +class ErrorBoundary extends React.Component<React.PropsWithChildren, { error: Error | null }> { + constructor(props: React.PropsWithChildren){ super(props); this.state = { error: null }; } - static getDerivedStateFromError(error:any){ return { error }; } - componentDidCatch(err:any, info:any){ console.error('App crashed:', err, info); } + static getDerivedStateFromError(error: Error){ return { error }; } + componentDidCatch(err: Error, info: React.ErrorInfo){ console.error('App crashed:', err, info); } render(){ if(this.state.error){ return <div style={{padding:24,fontFamily:'monospace',color:'#eee'}}> <h2>Application Error</h2> - <pre>{String(this.state.error)}</pre> - <p>Check console for stack trace.</p> + <pre>{String(this.state.error.message || this.state.error)}</pre> + <p>Check console for stack trace.</p> + <button onClick={() => this.setState({ error: null })} style={{marginTop:12}}>Try again</button> </div>; } return this.props.children; } }
37-46: Guard #root existence and wrap in StrictModePrevents a cryptic null deref and enables extra checks in dev.
-ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - <QueryClientProvider client={qc}> - <ThemeProvider theme={darkAquaTheme}> - <CssBaseline /> - <ErrorBoundary> - <App /> - </ErrorBoundary> - </ThemeProvider> - </QueryClientProvider> -); +const rootEl = document.getElementById('root'); +if (!rootEl) throw new Error('Root element #root not found'); +ReactDOM.createRoot(rootEl).render( + <React.StrictMode> + <QueryClientProvider client={qc}> + <ThemeProvider theme={darkAquaTheme}> + <CssBaseline /> + <ErrorBoundary> + <App /> + </ErrorBoundary> + </ThemeProvider> + </QueryClientProvider> + </React.StrictMode> +);dashboard/README.md (2)
18-24: Fix markdownlint MD058: add blank lines around the tableAdds the required blank lines for table blocks.
-## Data Files (place in `public/data/`) -| File | Purpose | +## Data Files (place in `public/data/`) + +| File | Purpose | ... -| `dictionary.json` | Reference dictionary | +| `dictionary.json` | Reference dictionary | +
11-15: Optional: call out data preparation step to avoid 404sIf data is generated by prepare_data.py in CI, mention how to populate public/data locally.
I can propose a short “Prepare data locally” subsection if you confirm the intended local workflow.
dashboard/src/env.d.ts (1)
3-12: Add missing type for VITE_MINIO_SCHEME (if referenced)AI summary mentions usage; add it (optional) to avoid TS errors if used.
interface ImportMetaEnv { readonly VITE_MINIO_ENDPOINT: string; + readonly VITE_MINIO_SCHEME?: 'http' | 'https'; readonly VITE_MINIO_ACCESS_KEY: string; readonly VITE_MINIO_SECRET_KEY: string; readonly VITE_MINIO_BUCKET: string; }If you remove secrets per above, keep only ENDPOINT/BUCKET (and optional SCHEME).
dashboard/src/theme.ts (2)
1-1: Avoidas anyon theme.shadows; type it asShadowsKeeps TS safety while satisfying MUI’s 25-shadow tuple.
-import { createTheme, alpha } from '@mui/material/styles'; +import { createTheme, alpha } from '@mui/material/styles'; +import type { Shadows } from '@mui/material/styles'; @@ -const customShadows = [ +const customShadows = [ 'none', '0 2px 4px -2px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.02)', '0 4px 12px -2px rgba(0,0,0,0.65), 0 0 0 1px rgba(255,255,255,0.02)', '0 6px 18px -4px rgba(0,0,0,0.65), 0 0 0 1px rgba(255,255,255,0.025)', '0 10px 28px -6px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.025)', ...Array(20).fill('0 0 0 1px rgba(0,0,0,0.4)') as string[] -] as const; +] as unknown as Shadows; @@ - shadows: customShadows as any, + shadows: customShadows,Also applies to: 3-10, 38-38
42-52: Add Firefox scrollbar styling (WebKit-only at the moment)Small cross-browser polish.
body: { backgroundColor: '#03060a', backgroundImage: [ 'radial-gradient(circle at 20% 15%, rgba(0,200,255,0.09), rgba(0,0,0,0) 45%)', 'radial-gradient(circle at 80% 75%, rgba(120,60,255,0.10), rgba(0,0,0,0) 50%)', 'linear-gradient(135deg, #020409 0%, #040b14 45%, #020409 100%)' ].join(','), backgroundAttachment: 'fixed', overscrollBehavior: 'none', - WebkitFontSmoothing: 'antialiased' + WebkitFontSmoothing: 'antialiased', + scrollbarWidth: 'thin', + scrollbarColor: '#145566 #03060a' },dashboard/src/components/DataTables.tsx (1)
1-1: Memoize column definitions to avoid DataGrid state resets and extra work.Apply:
-import React from 'react'; +import React, { useMemo } from 'react';And:
- const dbCols = autoCols(database); - const consCols = autoCols(consolidated); + const dbCols = useMemo(() => autoCols(database), [database]); + const consCols = useMemo(() => autoCols(consolidated), [consolidated]);dashboard/src/components/Gallery.tsx (3)
90-96: Don’t open modal for missing images; add alt text.Improves UX and accessibility.
Apply:
- <Grid item key={img.key} xs={6} sm={4} md={3} lg={2} onClick={()=> openModal(idx)} style={{ cursor: img.url ? 'pointer':'default' }}> + <Grid item key={img.key} xs={6} sm={4} md={3} lg={2} onClick={()=> img.url && openModal(idx)} style={{ cursor: img.url ? 'pointer':'default' }}> @@ - {img.url ? (<img src={img.url} style={{ maxWidth:'100%', maxHeight:120, objectFit:'contain' }} />) : (<Typography variant="caption" color="text.secondary">No image</Typography>)} + {img.url ? (<img src={img.url} alt={`${img.survey} ${img.band} cutout for ${img.key}`} style={{ maxWidth:'100%', maxHeight:120, objectFit:'contain' }} />) : (<Typography variant="caption" color="text.secondary">No image</Typography>)}
120-123: Alt text for modal image.Apply:
- <img src={current.url} style={{ maxWidth:'100%', maxHeight:'70vh', objectFit:'contain', borderRadius:4 }} /> + <img src={current.url} alt={`${current.survey} ${current.band} cutout for ${current.key}`} style={{ maxWidth:'100%', maxHeight:'70vh', objectFit:'contain', borderRadius:4 }} />
37-51: Optional: Fetch in parallel with limited concurrency for large sets.Sequential
awaitper item will be slow; consider a small pool (e.g., 6–8) and update progress as promises settle.If helpful, I can provide a p-limit based diff.
dashboard/src/components/CutoutGrid.tsx (2)
1-1: ImportuseEffectfor cleanup.Apply:
-import React, { memo } from 'react'; +import React, { memo, useEffect } from 'react';
42-49: Add alt text for accessibility.Apply:
- {!isLoading && data && <img - src={data} + {!isLoading && data && <img + src={data} + alt={`${record.survey} ${record.band} cutout for ${record.JNAME}`} loading="lazy" decoding="async" crossOrigin="anonymous" style={{ width:'100%', height:'100%', objectFit:'contain', imageRendering:'auto' }}dashboard/src/components/SkyMap.tsx (4)
231-233: Keep marker radius constant in screen px under anisotropic scaling.Use the larger axis scale for px→world conversion; current code uses only baseScaleX, producing ellipses with zoom/aspect changes.
- const pxToWorld = 1 / (baseScaleX*zoom); - const rWorld = targetPx * pxToWorld; + const invS = 1 / (Math.max(baseScaleX, baseScaleY) * zoom); + const rWorld = targetPx * invS;and
- const pxToWorld = 1 / (baseScaleX*zoom); - const rWorld = targetPx * pxToWorld; + const invS = 1 / (Math.max(baseScaleX, baseScaleY) * zoom); + const rWorld = targetPx * invS;Also applies to: 246-247
362-377: Add pointercancel handler to avoid stuck “grabbing” state on gesture interruptions.canvas.addEventListener('pointerup', onUp); + canvas.addEventListener('pointercancel', onUp); canvas.addEventListener('pointerleave', onUp); @@ - canvas.removeEventListener('pointerup', onUp as any); + canvas.removeEventListener('pointerup', onUp as any); + canvas.removeEventListener('pointercancel', onUp as any); canvas.removeEventListener('pointerleave', onUp as any);
97-106: Remove unused refs (dead state).lastPtsRef, lastSelectedRef, lastPanRef, lastZoomRef are assigned but never read. Trim to reduce churn.
307-312: Redundant redraw effects.Both effects call draw(); a single effect keyed on draw is sufficient.
- useEffect(()=>{ draw(); }, [draw]); - - // Redraw selection change - useEffect(()=>{ draw(); }, [selected, draw]); + useEffect(()=>{ draw(); }, [draw]);dashboard/src/components/ObjectsTable.tsx (1)
31-33: Tighten types for handlers and row-className.Avoid any; use DataGrid types for better safety and editor help.
-import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { DataGrid, GridColDef, GridRowParams, GridPaginationModel, GridRowClassNameParams } from '@mui/x-data-grid'; @@ - const handleRowClick = useCallback((params: any) => { + const handleRowClick = useCallback((params: GridRowParams) => { onSelect(params.row.JNAME); }, [onSelect]); @@ - const handlePaginationChange = useCallback((model: any) => { + const handlePaginationChange = useCallback((model: GridPaginationModel) => { setPage(model.page); setPageSize(model.pageSize); }, []); @@ - getRowClassName: (params: any) => params.row.JNAME === selected ? 'selected-row' : '', + getRowClassName: (params: GridRowClassNameParams) => (params.row as any).JNAME === selected ? 'selected-row' : '',Also applies to: 35-38, 65-66, 22-24
dashboard/src/App.tsx (6)
170-171: Avoid mutating dependencies with in-place sort.
references.sort()mutates the array and can create subtle ordering bugs. Sort a copy instead.- const allReferences = useMemo(()=> references.sort(), [references]); + const allReferences = useMemo(()=> [...references].sort(), [references]);
53-66: Stream min/max instead of building large arrays.
Math.min(...vals)with spreads is memory-heavy for big datasets. Compute min/max in one pass.- const domain = useMemo(()=>{ - const acc: Record<string,{min:number;max:number}> = {}; - numericFields.forEach(f=>{ - const vals: number[] = []; - for(const r of database){ - if(!r) continue; - let v: unknown = r[f.key]; - if(typeof v === 'string'){ const parsed = parseFloat(v); if(!isNaN(parsed)) v = parsed; } - if(typeof v === 'number' && !isNaN(v)) vals.push(v); - } - if(vals.length){ acc[f.key] = { min: Math.min(...vals), max: Math.max(...vals) }; } - }); - return acc; - }, [database, numericFields]); + const domain = useMemo(()=>{ + const acc: Record<string,{min:number;max:number}> = {}; + for (const f of numericFields) { + let min = Infinity, max = -Infinity; + for (const r of database) { + if(!r) continue; + let v: unknown = r[f.key]; + if (typeof v === 'string') { const p = parseFloat(v); if (!isNaN(p)) v = p; } + if (typeof v === 'number' && !isNaN(v)) { if (v < min) min = v; if (v > max) max = v; } + } + if (min !== Infinity) acc[f.key] = { min, max }; + } + return acc; + }, [database, numericFields]);
150-156: Reset should also clear current selection (optional).When resetting filters, keeping a stale JNAME selected can confuse users. Clear selection too.
- const resetFilters = useCallback(() => { - setFilters(initialFilters); - }, [initialFilters]); + const resetFilters = useCallback(() => { + setFilters(initialFilters); + setJName(''); + }, [initialFilters]);
28-33: Static JSON: make queries non-stale and avoid retries.These endpoints are static assets. Consider disabling retries and marking data perpetually fresh.
- const { data: database = [], isLoading: dbLoading, error: dbError } = useQuery({ queryKey: ['db'], queryFn: loadDatabase }); + const { data: database = [], isLoading: dbLoading, error: dbError } = + useQuery({ queryKey: ['db'], queryFn: loadDatabase, staleTime: Infinity, retry: false }); - const { data: consolidated = [], isLoading: consLoading, error: consError } = useQuery({ queryKey: ['cons'], queryFn: loadConsolidated }); + const { data: consolidated = [], isLoading: consLoading, error: consError } = + useQuery({ queryKey: ['cons'], queryFn: loadConsolidated, staleTime: Infinity, retry: false }); - const { data: dictionary = {} as Record<string, unknown>, isLoading: dictLoading, error: dictError } = useQuery({ queryKey: ['dict'], queryFn: loadDictionary }); + const { data: dictionary = {} as Record<string, unknown>, isLoading: dictLoading, error: dictError } = + useQuery({ queryKey: ['dict'], queryFn: loadDictionary, staleTime: Infinity, retry: false }); - const { data: cutouts = [], isLoading: cutoutsLoading, error: cutoutsError } = useQuery({ queryKey: ['cutouts'], queryFn: loadCutouts }); + const { data: cutouts = [], isLoading: cutoutsLoading, error: cutoutsError } = + useQuery({ queryKey: ['cutouts'], queryFn: loadCutouts, staleTime: Infinity, retry: false });
184-188: Disable “Reset Filters” based on actual state, not reference equality.
filters === initialFiltersonly catches the “just-reset” reference. Consider a shallow check on fields to better reflect user changes.Example:
- disabled={filters === initialFilters} + disabled={ + filters.jnameSearch === '' && + filters.references.length === 0 && + Object.values(filters.numeric).every(v => v == null) + }
198-203: Error panel: avoid leaking raw errors to end users.Render a friendly message and log details to console for diagnostics.
- <Typography variant="body2" color="text.secondary">{String(anyError)}</Typography> + <Typography variant="body2" color="text.secondary"> + One or more datasets failed to load. Please retry or check hosting configuration. + </Typography> + {console.error('[Explorer] Data load error:', anyError)}dashboard/src/components/FiltersDrawer.tsx (1)
31-31: Format numeric labels.Long floats can be noisy. Format to a few decimals.
-const pct = (min: number, max: number) => `${min} – ${max}`; +const fmt = (n: number) => (Number.isInteger(n) ? n : +n.toFixed(4)); +const pct = (min: number, max: number) => `${fmt(min)} – ${fmt(max)}`;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
dashboard/package-lock.jsonis excluded by!**/package-lock.jsondashboard/public/telescope.pngis excluded by!**/*.png
📒 Files selected for processing (30)
.devcontainer/devcontainer.json(1 hunks).github/workflows/deploy.yml(1 hunks)README.md(1 hunks)dashboard/.gitignore(1 hunks)dashboard/README.md(1 hunks)dashboard/eslint.config.js(1 hunks)dashboard/index.html(1 hunks)dashboard/package.json(1 hunks)dashboard/prepare_data.py(1 hunks)dashboard/public/data/.gitkeep(1 hunks)dashboard/src/App.tsx(1 hunks)dashboard/src/api.ts(1 hunks)dashboard/src/components/CutoutGrid.tsx(1 hunks)dashboard/src/components/DataTables.tsx(1 hunks)dashboard/src/components/FiltersDrawer.tsx(1 hunks)dashboard/src/components/Gallery.tsx(1 hunks)dashboard/src/components/ObjectsTable.tsx(1 hunks)dashboard/src/components/SkyMap.tsx(1 hunks)dashboard/src/components/VirtualizedList.tsx(1 hunks)dashboard/src/env.d.ts(1 hunks)dashboard/src/hooks/useDebounce.ts(1 hunks)dashboard/src/main.tsx(1 hunks)dashboard/src/theme.ts(1 hunks)dashboard/src/types.ts(1 hunks)dashboard/tsconfig.json(1 hunks)dashboard/vite.config.ts(1 hunks)notebooks/footprints/footprints.ipynb(1 hunks)notebooks/how_to.ipynb(1 hunks)notebooks/modified-gravity-tests/01_LaStBeRu_cosmo_ground.ipynb(1 hunks)notebooks/proposals/proposals.ipynb(1 hunks)
🔥 Files not summarized due to errors (1)
- notebooks/modified-gravity-tests/01_LaStBeRu_cosmo_ground.ipynb: Error: Server error: no LLM provider could handle the message
👮 Files not reviewed due to content moderation or server errors (1)
- notebooks/modified-gravity-tests/01_LaStBeRu_cosmo_ground.ipynb
🧰 Additional context used
🧬 Code graph analysis (6)
dashboard/src/api.ts (1)
dashboard/src/types.ts (4)
DataRecord(1-4)ConsolidatedRecord(6-9)Dictionary(23-23)CutoutRecord(11-16)
dashboard/src/main.tsx (1)
dashboard/src/theme.ts (1)
darkAquaTheme(13-137)
dashboard/src/components/Gallery.tsx (2)
dashboard/src/types.ts (1)
CutoutRecord(11-16)dashboard/src/api.ts (1)
getCutoutObject(52-104)
dashboard/src/App.tsx (8)
dashboard/src/components/DataTables.tsx (1)
DataTables(32-87)dashboard/src/components/CutoutGrid.tsx (1)
CutoutGrid(12-25)dashboard/src/api.ts (4)
loadDatabase(5-8)loadConsolidated(10-13)loadDictionary(15-20)loadCutouts(22-25)dashboard/src/components/FiltersDrawer.tsx (2)
FiltersState(12-16)FiltersDrawer(33-148)dashboard/src/hooks/useDebounce.ts (1)
useDebounce(3-17)dashboard/src/components/ObjectsTable.tsx (1)
ObjectsTable(18-109)dashboard/src/components/SkyMap.tsx (1)
SkyMap(41-398)dashboard/src/types.ts (1)
CutoutRecord(11-16)
dashboard/src/components/CutoutGrid.tsx (2)
dashboard/src/types.ts (1)
CutoutRecord(11-16)dashboard/src/api.ts (1)
getCutoutObject(52-104)
dashboard/src/components/DataTables.tsx (1)
dashboard/src/types.ts (2)
DataRecord(1-4)ConsolidatedRecord(6-9)
🪛 markdownlint-cli2 (0.17.2)
dashboard/README.md
18-18: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
🪛 Ruff (0.12.2)
notebooks/how_to.ipynb
64-64: Undefined name display
(F821)
dashboard/prepare_data.py
245-245: Do not use bare except
(E722)
🪛 actionlint (1.7.7)
.github/workflows/deploy.yml
25-25: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🪛 YAMLlint (1.37.1)
.github/workflows/deploy.yml
[error] 23-23: trailing spaces
(trailing-spaces)
[error] 28-28: trailing spaces
(trailing-spaces)
[error] 35-35: trailing spaces
(trailing-spaces)
[error] 40-40: trailing spaces
(trailing-spaces)
[error] 49-49: trailing spaces
(trailing-spaces)
[error] 54-54: trailing spaces
(trailing-spaces)
[error] 57-57: trailing spaces
(trailing-spaces)
[error] 63-63: trailing spaces
(trailing-spaces)
🔇 Additional comments (11)
dashboard/src/components/VirtualizedList.tsx (2)
1-82: Solid, minimal virtualization component.Clean math, correct index clamping, and nice fallback for small lists. Good reuse potential.
6-7: Validate heights > 0.Guard against itemHeight <= 0 or containerHeight <= 0 to prevent division edge cases.
Do you want me to add a small runtime assert (and story/test) to enforce positive heights?
notebooks/proposals/proposals.ipynb (1)
875-881: Verify and correct boolean comparison foris_rgb
- Comparing to string
"True"may mis-filter; confirmCutouts["is_rgb"].dtypeis boolean.- If it’s not, convert first:
Cutouts["is_rgb"] = Cutouts["is_rgb"].astype("boolean")- Then update the query:
- .query('cutout_size == "20asec" and is_rgb == "True"') + .query('cutout_size == "20asec" and is_rgb == True')dashboard/src/types.ts (1)
11-16: Align CutoutRecord with the JSON shape from prepare_dataRemove any fields dropped in data prep (cutout_size, file_name) and keep band required; add the two columns still present in
/data/cutouts.json:dashboard/src/types.ts export interface CutoutRecord { JNAME: string; survey: string; band: string; file_path: string; + processing: string; + is_rgb: string; }Likely an incorrect or invalid review comment.
dashboard/eslint.config.js (1)
10-19: ESLint top-level await is supported; no changes required. dashboard/package.json specifies eslint ^9.9.0 and CI uses Node 18, satisfying the flat-config ESM and top-level-await requirements. Optional: switch to static imports for broader environment compatibility.dashboard/prepare_data.py (1)
12-21: Validate MinIO vars early, strip scheme, and fail fast
Add a guard at the top of dashboard/prepare_data.py to ensure all three env vars are set, remove anyhttp(s)://scheme before passing to Minio, and raise a clear error. If you support Python < 3.9, replacestr.removeprefixwithurlparseor a regex. Also confirm that yourendpointis inhost[:port]form:MINIO_ENDPOINT_URL = os.getenv("MINIO_ENDPOINT_URL") ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY") SECRET_KEY = os.getenv("MINIO_SECRET_KEY") +if not (MINIO_ENDPOINT_URL and ACCESS_KEY and SECRET_KEY): + raise RuntimeError( + "MINIO_ENDPOINT_URL, MINIO_ACCESS_KEY, and MINIO_SECRET_KEY must be set" + ) +# strip any URL scheme (use urlparse or removeprefix on Python ≥3.9) +endpoint = MINIO_ENDPOINT_URL.removeprefix("https://").removeprefix("http://") client = Minio( - MINIO_ENDPOINT_URL, + endpoint, access_key=ACCESS_KEY, secret_key=SECRET_KEY, secure=True, )dashboard/src/main.tsx (2)
26-35: React Query defaults look goodSane cache policy for an interactive dashboard; no changes needed.
26-35: No changes needed: @tanstack/react-query v5.56.2 supportsgcTime.dashboard/src/components/CutoutGrid.tsx (1)
30-35: Optional: avoid caching blob URLs in React Query.If using @tanstack/react-query v5, set
gcTime: 0; if v4, setcacheTime: 0to reduce holding blob URLs in memory after unmount.Want me to propose the exact option based on your installed version?
dashboard/src/components/SkyMap.tsx (1)
380-397: Nice UX polish.Controls, zoom bounds, and DPR handling look solid. The selection glow is tasteful and performant with conditional RAF.
dashboard/src/components/ObjectsTable.tsx (1)
40-48: Auto-scroll to selection is a nice touch.Keeping the selected row visible improves ergonomics with large datasets.
Summary by CodeRabbit
New Features
Deployment
Documentation
Chores