diff --git a/applications/virtual-fly-brain/frontend/src/components/Layout.jsx b/applications/virtual-fly-brain/frontend/src/components/Layout.jsx index 5785d70d..bbce5db1 100644 --- a/applications/virtual-fly-brain/frontend/src/components/Layout.jsx +++ b/applications/virtual-fly-brain/frontend/src/components/Layout.jsx @@ -18,6 +18,7 @@ import { removeAllInstances } from './../reducers/actions/instances'; import { Box, Button,Modal, useMediaQuery, useTheme, Typography, CircularProgress, Link } from "@mui/material"; import { activateCircuits, activateImages } from "../reducers/actions/layout"; import { widgetsIDs } from "./layout/widgets"; +import { bottomNavClearAll, bottomNavDownload, bottomNavLayers, bottomNavQuery, bottomNavSearch, bottomNavSnapshot, bottomNavUpload } from "../utils/constants"; const { secondaryBg, @@ -80,14 +81,14 @@ const MainLayout = ({ bottomNav, setBottomNav }) => { const queryComponentOpened = useSelector( state => state.globalInfo?.queryComponentOpened ); useEffect( () => { - if ( queryComponentOpened && bottomNav !== 2 ){ - setBottomNav(2) + if ( queryComponentOpened && bottomNav !== bottomNavQuery ){ + setBottomNav(bottomNavQuery) } }, [bottomNav, queryComponentOpened, setBottomNav]); // Handle Clear All functionality useEffect(() => { - if (bottomNav === 4) { + if (bottomNav === bottomNavClearAll) { if (allLoadedInstances?.length > 1) { removeAllInstances(); } @@ -97,7 +98,7 @@ const MainLayout = ({ bottomNav, setBottomNav }) => { }, [bottomNav, allLoadedInstances?.length, setBottomNav]); useEffect( () => { - if ( bottomNav === 3 ){ + if ( bottomNav === bottomNavLayers ){ const layoutManager = getLayoutManagerInstance(); if (!layoutManager.model.getNodeById("listViewerWidget").isVisible()) { const newWidget = { ...widgets[widgetsIDs.listViewerWidgetID] } @@ -252,22 +253,22 @@ const MainLayout = ({ bottomNav, setBottomNav }) => { {desktopScreen ? ( <> {tabContent} - {bottomNav === 0 && < VFBSnapshot open={true} setBottomNav={setBottomNav} />} - {bottomNav === 1 && < VFBUploader open={true} setBottomNav={setBottomNav} />} - {bottomNav === 2 && } - {bottomNav === 3 && } - {bottomNav === 6 && } + {bottomNav === bottomNavSnapshot && < VFBSnapshot open={true} setBottomNav={setBottomNav} />} + {bottomNav === bottomNavUpload && < VFBUploader open={true} setBottomNav={setBottomNav} />} + {bottomNav === bottomNavDownload && } + {bottomNav === bottomNavQuery && } + {bottomNav === bottomNavSearch && } ) : ( <> { - bottomNav != 3 && tabContent + bottomNav != bottomNavQuery && tabContent } - {bottomNav === 0 && } - {bottomNav === 1 && } - {bottomNav === 2 && } - {bottomNav === 3 && } - {bottomNav === 6 && } + {bottomNav === bottomNavSnapshot && } + {bottomNav === bottomNavUpload && } + {bottomNav === bottomNavDownload && } + {bottomNav === bottomNavQuery && } + {bottomNav === bottomNavSearch && } )} diff --git a/applications/virtual-fly-brain/frontend/src/components/NeuroglassViewer.jsx b/applications/virtual-fly-brain/frontend/src/components/NeuroglassViewer.jsx index b2ce5c05..6283c49b 100644 --- a/applications/virtual-fly-brain/frontend/src/components/NeuroglassViewer.jsx +++ b/applications/virtual-fly-brain/frontend/src/components/NeuroglassViewer.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { Box, Typography, useMediaQuery } from '@mui/material'; import { useTheme } from '@mui/material/styles'; @@ -8,6 +8,7 @@ const NEUROGLASS_URL = import.meta.env.NEUROGLASS_URL ?? ''; export default function NeuroglassViewer() { const [debouncedSrc, setDebouncedSrc] = useState(''); + const [iframeSrc, setIframeSrc] = useState(''); const allLoadedInstances = useSelector(state => state.instances?.allLoadedInstances); const focusedInstance = useSelector(state => state.instances?.focusedInstance); @@ -16,16 +17,43 @@ export default function NeuroglassViewer() { const theme = useTheme(); const isMobile = !useMediaQuery(theme.breakpoints.up('lg')); - // Rebuilds whenever instances, focused item, layout preference, or viewport size changes. - const iframeSrc = useMemo(() => { - const layout = resolveNeuroglassLayout(neuroglassView, isMobile); - const state = buildNeuroglassState( - allLoadedInstances, - focusedInstance?.metadata?.Id, - layout, - ); - if (!state || !NEUROGLASS_URL) return ''; - return `${NEUROGLASS_URL}/embed#!${encodeURIComponent(JSON.stringify(state))}`; + useEffect(() => { + let cancelled = false; + const abortController = new AbortController(); + async function buildSrc() { + const layout = resolveNeuroglassLayout(neuroglassView, isMobile); + try{ + const state = await buildNeuroglassState( + allLoadedInstances, + focusedInstance?.metadata?.Id, + layout, + abortController.signal + ); + + if (cancelled || abortController.signal.aborted) return; + + if (!state || !NEUROGLASS_URL) { + setIframeSrc(''); + return; + } + + setIframeSrc( + `${NEUROGLASS_URL}/embed#!${encodeURIComponent(JSON.stringify(state))}` + ); + } catch (error) { + if (error?.name === 'AbortError' || abortController.signal.aborted) { + return; + } + throw error; + } + } + + buildSrc(); + + return () => { + cancelled = true; + abortController.abort(); + }; }, [allLoadedInstances, focusedInstance?.metadata?.Id, neuroglassView, isMobile]); useEffect(() => { diff --git a/applications/virtual-fly-brain/frontend/src/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.jsx b/applications/virtual-fly-brain/frontend/src/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.jsx index df02622b..fcca0bf8 100644 --- a/applications/virtual-fly-brain/frontend/src/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.jsx +++ b/applications/virtual-fly-brain/frontend/src/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.jsx @@ -1,6 +1,7 @@ import React from 'react'; import vars from "../../../theme/variables"; import { widgets } from "../../layout/widgets"; +import { bottomNavDownload, bottomNavQuery, bottomNavSearch, bottomNavUpload } from '../../../utils/constants'; const { primaryFont, whiteColor, tabActiveColor, primaryBg } = vars; const ACTIONS = { @@ -199,7 +200,7 @@ export const toolbarMenu = (autoSaveLayout) => { return { icon: "fa fa-search", action: { handlerAction: ACTIONS.SHOW_COMPONENT, - parameters: [5] + parameters: [bottomNavSearch] } }, { @@ -207,7 +208,7 @@ export const toolbarMenu = (autoSaveLayout) => { return { icon: "fa fa-clipboard-question", action: { handlerAction: ACTIONS.SHOW_COMPONENT, - parameters: [2] + parameters: [bottomNavQuery] } }, { @@ -279,7 +280,7 @@ export const toolbarMenu = (autoSaveLayout) => { return { icon: "fa fa-download", action: { handlerAction: ACTIONS.SHOW_COMPONENT, - parameters: [1] + parameters: [bottomNavDownload] } }, { @@ -287,7 +288,7 @@ export const toolbarMenu = (autoSaveLayout) => { return { icon: "fa fa-upload", action: { handlerAction: ACTIONS.SHOW_COMPONENT, - parameters: [0] + parameters: [bottomNavUpload] } }, { diff --git a/applications/virtual-fly-brain/frontend/src/components/queryBuilder/Query.jsx b/applications/virtual-fly-brain/frontend/src/components/queryBuilder/Query.jsx index 46959eb8..fe1f7d23 100644 --- a/applications/virtual-fly-brain/frontend/src/components/queryBuilder/Query.jsx +++ b/applications/virtual-fly-brain/frontend/src/components/queryBuilder/Query.jsx @@ -26,15 +26,15 @@ const getQueries = (newQueries, searchTerm) => { if (!newQueries || newQueries.length === 0) return []; let updatedQueries = []; - const term = searchTerm ? searchTerm.toLowerCase() : undefined; + const term = searchTerm ? searchTerm.toLowerCase() : ""; newQueries.forEach((query) => { if (query.queries) { Object.keys(query.queries).forEach((key) => { if (query.queries[key]?.active) { - let rows = query.queries[key]?.rows || []; + let rows = query.queries[key]?.rows || query.queries[key]?.preview_results?.rows || []; - if (term) { + if (term != undefined) { rows = rows.filter(row => { // Cache the string conversion for performance const values = Object.values(row); @@ -95,20 +95,41 @@ const Query = forwardRef(({ fullWidth, queries, searchTerm }, ref) => { const [sortDirection, setSortDirection] = useState(1); // 1 for ascending, -1 for descending // Memoize filtered searches - const filteredSearches = useMemo(() => getQueries(queries, searchTerm), [queries, searchTerm]); + const filteredSearches = useMemo(() => { + return getQueries(queries, searchTerm); + }, [queries, searchTerm]); + + const getRows = (item) => { + if (item?.rows?.length > 0) { + return item.rows; + } + + if (item?.preview_results?.rows?.length > 0) { + return item.preview_results.rows; + } + + return null; + }; // Memoize the final filtered results based on both search term and active chip tags const finalFilteredResults = useMemo(() => { if (!filteredSearches || filteredSearches.length === 0) return []; - const activeChipLabels = chipTags.filter(f => f.active).map(tag => tag.label); + const rows = filteredSearches.flatMap(item => { + const nestedRows = getRows(item); + return nestedRows || [item]; + }); - // Filter by active chip tags + const activeChipLabels = chipTags + .filter(f => f.active) + .map(tag => tag.label); + let filtered; + if (activeChipLabels.length === 0) { - filtered = filteredSearches; + filtered = rows; } else { - filtered = filteredSearches.filter(row => { + filtered = rows.filter(row => { const tags = getTags(row.tags); return activeChipLabels.some(label => tags.includes(label)); }); @@ -146,13 +167,22 @@ const Query = forwardRef(({ fullWidth, queries, searchTerm }, ref) => { if (!queries || queries.length === 0) return []; const tagMap = new Map(); + queries.forEach(query => { if (query.queries) { Object.keys(query.queries).forEach(q => { - if (query.queries[q]?.active) { - query.queries[q]?.rows?.forEach(row => { + const currentQuery = query.queries[q]; + + if (currentQuery?.active) { + const rows = + currentQuery?.rows?.length > 0 + ? currentQuery.rows + : currentQuery?.preview_results?.rows || []; + + rows.forEach(row => { if (row.tags) { const rowTags = getTags(row.tags); + rowTags.forEach(rowTag => { if (!tagMap.has(rowTag)) { tagMap.set(rowTag, { label: rowTag, active: true }); @@ -249,8 +279,8 @@ const Query = forwardRef(({ fullWidth, queries, searchTerm }, ref) => { return acc; }, {}) })); - updateQueries(clearQueries); - }, [queries]); + dispatch(updateQueries(clearQueries)); + }, [queries, dispatch]); const clearAllTags = useCallback(() => { setChipTags(prevTags => prevTags.map(tag => ({ ...tag, active: true }))); @@ -343,7 +373,7 @@ const Query = forwardRef(({ fullWidth, queries, searchTerm }, ref) => { onClick={() => null} disabled={!tag.active} onDelete={() => handleChipDelete(tag.label)} - key={tag} + key={tag?.label} deleteIcon={ { } // Open the query component panel - setBottomNav(2); + setBottomNav(bottomNavQuery); } } @@ -140,13 +141,13 @@ const Header = ({ setBottomNav }) => { Object.keys(query.queries)?.forEach(q => query.queries[q].active = false); } }); - if (matchQuery?.queries?.[action?.parameters[1]]) { - matchQuery.queries[action.parameters[1]].active = true; + if (matchQuery?.short_form == action?.parameters[0]) { + Object.keys(matchQuery.queries)?.forEach(q => matchQuery.queries[q].active = true); updateQueries(updatedQueries); - setBottomNav(2) + setBottomNav(bottomNavQuery) } else { - getQueries(action.parameters[0], action.parameters[1]) - setBottomNav(2) + getQueries(action.parameters[0]) + setBottomNav(bottomNavQuery) } break; } @@ -310,7 +311,7 @@ const Header = ({ setBottomNav }) => { {focusedInstance?.metadata?.Id && (focusedInstance?.metadata?.Queries?.length > 0 || queries?.find(q => q.short_form === focusedInstance.metadata.Id)) && (