From ba545b0fc073ce22c01a5ffd5012340a3263c0ed Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Thu, 18 Dec 2025 09:06:48 -0500 Subject: [PATCH 01/10] feat: Adds alignment file download link to ImageAlignmentStep --- src/TestHotReload.jsx | 7 ++++++ .../CustomSearch/ImageAlignmentStep.jsx | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/TestHotReload.jsx diff --git a/src/TestHotReload.jsx b/src/TestHotReload.jsx new file mode 100644 index 00000000..c7530a8b --- /dev/null +++ b/src/TestHotReload.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +const TestComponent = () =>
Test
; + +export default TestComponent; +// test polling 1765983077 +// test after podman restart 1765983592 diff --git a/src/components/CustomSearch/ImageAlignmentStep.jsx b/src/components/CustomSearch/ImageAlignmentStep.jsx index 7549fc94..29085a47 100644 --- a/src/components/CustomSearch/ImageAlignmentStep.jsx +++ b/src/components/CustomSearch/ImageAlignmentStep.jsx @@ -10,6 +10,19 @@ import HelpButton from "../Help/HelpButton"; export default function ImageAlignmentStep({ state, search }) { const [thumbnailUrl, setThumbnailUrl] = useState(null); const [movieUrl, setMovieUrl] = useState(null); + const [alignmentFile, setAlignmentFile] = useState(null); + + + useEffect(() =>{ + if (search.alignmentFile) { + const alignmentFileUrl = `${search.searchDir}/${search.alignmentFile}`; + signedLink(alignmentFileUrl).then((result) => { + setAlignmentFile(result); + }); + } else { + setAlignmentFile(null); + } + }, [search.searchDir, search.alignmentFile]); useEffect(() => { if (search.alignmentMovie) { @@ -80,6 +93,16 @@ export default function ImageAlignmentStep({ state, search }) { ) : ( "" )} + + {alignmentFile ? ( +
+ + Download Alignment File + +
+ ) : ( + "" + )} ); } else if (search.alignmentErrorMessage || search.errorMessage) { From f6be93fef2908231ce5778587b5f6c39920294a2 Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Tue, 13 Jan 2026 13:24:20 -0500 Subject: [PATCH 02/10] feat: display searched libraries with counts in ColorDepthSearchStep Adds librariesCountsMap field to GraphQL queries and displays the searched libraries with their match counts in the Color Depth Search step. Each library is shown on its own row with the format "LibraryName (count)". --- .../CustomSearch/ColorDepthSearchStep.jsx | 18 ++++++++++++++++-- src/components/CustomSearch/SearchSteps.jsx | 10 ++++++++++ src/graphql/queries.js | 2 ++ src/graphql/subscriptions.js | 1 + 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/components/CustomSearch/ColorDepthSearchStep.jsx b/src/components/CustomSearch/ColorDepthSearchStep.jsx index 67626cab..a7811f68 100644 --- a/src/components/CustomSearch/ColorDepthSearchStep.jsx +++ b/src/components/CustomSearch/ColorDepthSearchStep.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { formatRelative, formatDistanceStrict } from "date-fns"; import StepTitle from "./StepTitle"; -export default function ColorDepthSearchStep({ started, finished, state }) { +export default function ColorDepthSearchStep({ started, finished, state, librariesCountsMap }) { let searchEnd = ""; let duration = ""; @@ -14,11 +14,23 @@ export default function ColorDepthSearchStep({ started, finished, state }) { } } + const hasLibraries = librariesCountsMap && Object.keys(librariesCountsMap).length > 0; + return ( <>

{searchEnd}

{duration}

+ {hasLibraries ? ( + <> +

Libraries searched:

+ {Object.entries(librariesCountsMap).map(([library, count]) => ( +

+ {library} ({count}) +

+ ))} + + ) : null} ); } @@ -27,9 +39,11 @@ ColorDepthSearchStep.propTypes = { state: PropTypes.string.isRequired, finished: PropTypes.string, started: PropTypes.string, + librariesCountsMap: PropTypes.object, }; ColorDepthSearchStep.defaultProps = { finished: null, - started: null + started: null, + librariesCountsMap: null, }; diff --git a/src/components/CustomSearch/SearchSteps.jsx b/src/components/CustomSearch/SearchSteps.jsx index 1b37dfc9..b597af6b 100644 --- a/src/components/CustomSearch/SearchSteps.jsx +++ b/src/components/CustomSearch/SearchSteps.jsx @@ -34,6 +34,15 @@ export default function SearchSteps({ search }) { currentStep = 5; } + let librariesCountsMap = null; + if (search.librariesCountsMap) { + try { + librariesCountsMap = JSON.parse(search.librariesCountsMap); + } catch (error) { + console.error("Failed to parse librariesCountsMap:", error); + } + } + return (
@@ -67,6 +76,7 @@ export default function SearchSteps({ search }) { started={search.cdsStarted} finished={search.cdsFinished} state={stepState(3, currentStep, errorMessage)} + librariesCountsMap={librariesCountsMap} /> diff --git a/src/graphql/queries.js b/src/graphql/queries.js index e2db2746..dfd850d1 100644 --- a/src/graphql/queries.js +++ b/src/graphql/queries.js @@ -70,6 +70,7 @@ export const listSearches = /* GraphQL */ ` alignFinished alignStarted anatomicalRegion + librariesCountsMap } nextToken } @@ -109,6 +110,7 @@ export const listItemsByOwner = ` alignFinished alignStarted anatomicalRegion + librariesCountsMap } nextToken diff --git a/src/graphql/subscriptions.js b/src/graphql/subscriptions.js index 2becd8a7..1b98961d 100644 --- a/src/graphql/subscriptions.js +++ b/src/graphql/subscriptions.js @@ -62,6 +62,7 @@ export const onUpdateSearch = /* GraphQL */ ` alignmentMovie alignFinished alignStarted + librariesCountsMap } } `; From 39b743136f629c485ea184999593fa4a067ab045 Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Tue, 13 Jan 2026 13:28:21 -0500 Subject: [PATCH 03/10] fix(uploads): Correctly formats search libraries. Shows the name that we want to show users instead of the internal name(key). --- src/components/CustomSearch/ColorDepthSearchStep.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/CustomSearch/ColorDepthSearchStep.jsx b/src/components/CustomSearch/ColorDepthSearchStep.jsx index a7811f68..540d7420 100644 --- a/src/components/CustomSearch/ColorDepthSearchStep.jsx +++ b/src/components/CustomSearch/ColorDepthSearchStep.jsx @@ -2,6 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import { formatRelative, formatDistanceStrict } from "date-fns"; import StepTitle from "./StepTitle"; +import LibraryFormatter from "../LibraryFormatter"; export default function ColorDepthSearchStep({ started, finished, state, librariesCountsMap }) { let searchEnd = ""; @@ -26,7 +27,7 @@ export default function ColorDepthSearchStep({ started, finished, state, librari

Libraries searched:

{Object.entries(librariesCountsMap).map(([library, count]) => (

- {library} ({count}) + ({count})

))} From 16a169dd65e55ad33aa3f2f565414ff4df330cf4 Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Fri, 20 Mar 2026 16:15:33 -0400 Subject: [PATCH 04/10] chore: remove TestHotReload throwaway file --- src/TestHotReload.jsx | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/TestHotReload.jsx diff --git a/src/TestHotReload.jsx b/src/TestHotReload.jsx deleted file mode 100644 index c7530a8b..00000000 --- a/src/TestHotReload.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const TestComponent = () =>
Test
; - -export default TestComponent; -// test polling 1765983077 -// test after podman restart 1765983592 From 6868d286c5429fd44c4d21ba3b1c6600898514eb Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Fri, 20 Mar 2026 16:15:41 -0400 Subject: [PATCH 05/10] fix: add null check for error.response in Results catch handler --- src/components/Results.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Results.jsx b/src/components/Results.jsx index 8944ac1a..baa05e75 100644 --- a/src/components/Results.jsx +++ b/src/components/Results.jsx @@ -95,7 +95,7 @@ export default function Results({ match }) { fr.readAsText(results.Body); }) .catch(error => { - if (error.response.status === 404) { + if (error.response && error.response.status === 404) { setSearchResults({ results: [] }); } else { message.error({ From 7c2a2ccfd16cb65eee74e13f07b4014b68b0e54e Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Fri, 20 Mar 2026 16:15:46 -0400 Subject: [PATCH 06/10] feat: rename alignmentFile to alignedVolume and add to GraphQL queries --- src/components/CustomSearch/ImageAlignmentStep.jsx | 14 +++++++------- src/graphql/queries.js | 7 +++++-- src/graphql/subscriptions.js | 3 +++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/CustomSearch/ImageAlignmentStep.jsx b/src/components/CustomSearch/ImageAlignmentStep.jsx index 29085a47..22a74cf1 100644 --- a/src/components/CustomSearch/ImageAlignmentStep.jsx +++ b/src/components/CustomSearch/ImageAlignmentStep.jsx @@ -10,19 +10,19 @@ import HelpButton from "../Help/HelpButton"; export default function ImageAlignmentStep({ state, search }) { const [thumbnailUrl, setThumbnailUrl] = useState(null); const [movieUrl, setMovieUrl] = useState(null); - const [alignmentFile, setAlignmentFile] = useState(null); + const [alignedVolume, setAlignmentFile] = useState(null); useEffect(() =>{ - if (search.alignmentFile) { - const alignmentFileUrl = `${search.searchDir}/${search.alignmentFile}`; - signedLink(alignmentFileUrl).then((result) => { + if (search.alignedVolume) { + const alignedVolumeUrl = `${search.searchDir}/${search.alignedVolume}`; + signedLink(alignedVolumeUrl).then((result) => { setAlignmentFile(result); }); } else { setAlignmentFile(null); } - }, [search.searchDir, search.alignmentFile]); + }, [search.searchDir, search.alignedVolume]); useEffect(() => { if (search.alignmentMovie) { @@ -94,9 +94,9 @@ export default function ImageAlignmentStep({ state, search }) { "" )} - {alignmentFile ? ( + {alignedVolume ? ( diff --git a/src/graphql/queries.js b/src/graphql/queries.js index dfd850d1..54c4863f 100644 --- a/src/graphql/queries.js +++ b/src/graphql/queries.js @@ -14,6 +14,7 @@ export const getSearch = /* GraphQL */ ` alignmentErrorMessage alignmentScore alignmentMovie + alignedVolume step anatomicalRegion algorithm @@ -56,7 +57,8 @@ export const listSearches = /* GraphQL */ ` alignmentErrorMessage alignmentScore alignmentMovie - anatomicalRegion + alignedVolume + anatomicalRegion cdsStarted cdsFinished step @@ -96,7 +98,8 @@ export const listItemsByOwner = ` alignmentErrorMessage alignmentScore alignmentMovie - anatomicalRegion + alignedVolume + anatomicalRegion cdsStarted cdsFinished step diff --git a/src/graphql/subscriptions.js b/src/graphql/subscriptions.js index 1b98961d..190d734f 100644 --- a/src/graphql/subscriptions.js +++ b/src/graphql/subscriptions.js @@ -24,8 +24,10 @@ export const onCreateSearch = /* GraphQL */ ` anatomicalRegion alignmentScore alignmentMovie + alignedVolume alignStarted alignFinished + librariesCountsMap } } `; @@ -60,6 +62,7 @@ export const onUpdateSearch = /* GraphQL */ ` anatomicalRegion alignmentScore alignmentMovie + alignedVolume alignFinished alignStarted librariesCountsMap From c3f7a575bce238015afe30f29a77b2bee5d84806 Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Fri, 20 Mar 2026 16:15:50 -0400 Subject: [PATCH 07/10] feat: show libraries missing debug message for legacy searches --- src/components/CustomSearch/ColorDepthSearchStep.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/CustomSearch/ColorDepthSearchStep.jsx b/src/components/CustomSearch/ColorDepthSearchStep.jsx index 540d7420..64c3317b 100644 --- a/src/components/CustomSearch/ColorDepthSearchStep.jsx +++ b/src/components/CustomSearch/ColorDepthSearchStep.jsx @@ -1,10 +1,12 @@ -import React from "react"; +import React, { useContext } from "react"; import PropTypes from "prop-types"; import { formatRelative, formatDistanceStrict } from "date-fns"; import StepTitle from "./StepTitle"; import LibraryFormatter from "../LibraryFormatter"; +import { AppContext } from "../../containers/AppContext"; export default function ColorDepthSearchStep({ started, finished, state, librariesCountsMap }) { + const { appState } = useContext(AppContext); let searchEnd = ""; let duration = ""; @@ -31,6 +33,8 @@ export default function ColorDepthSearchStep({ started, finished, state, librari

))} + ) : appState.debug ? ( +

Libraries data not available for this search

) : null} ); From c69916eba0f12dd29441fbc1d1bd83a40b70513d Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Fri, 20 Mar 2026 16:23:40 -0400 Subject: [PATCH 08/10] feat: show aligned volume download link on 3D download page for custom uploads --- src/components/MatchModal/Download3D.jsx | 34 +++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/MatchModal/Download3D.jsx b/src/components/MatchModal/Download3D.jsx index 4b711d2e..733343b9 100644 --- a/src/components/MatchModal/Download3D.jsx +++ b/src/components/MatchModal/Download3D.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { useParams } from "react-router-dom"; import { Row, Col, Typography } from "antd"; @@ -7,6 +7,7 @@ import FileExclamationOutlined from "@ant-design/icons/FileExclamationOutlined"; import { faExternalLink } from "@fortawesome/pro-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import ViewIn3DButton from "./ViewIn3DButton"; +import { signedLink } from "../../libs/awsLib"; const { Title, Text, Paragraph } = Typography; @@ -39,6 +40,18 @@ function getH5JLink(construct) { export default function Download3D(props) { const { selectedMatch, mask, isLM } = props; const { algorithm } = useParams(); + const [alignedVolumeUrl, setAlignedVolumeUrl] = useState(null); + + useEffect(() => { + if (mask.alignedVolume && mask.searchDir) { + const volumePath = `${mask.searchDir}/${mask.alignedVolume}`; + signedLink(volumePath).then((result) => { + setAlignedVolumeUrl(result); + }); + } else { + setAlignedVolumeUrl(null); + } + }, [mask.searchDir, mask.alignedVolume]); // If the match has either an AlignedBodySWC or a VisuallyLosslessStack // file, then we need to make it available for download, the same needs @@ -53,11 +66,20 @@ export default function Download3D(props) { const downloadLinks = ( <> {!mask.precomputed ? ( - - We don't have - a 3D representation of your uploaded file. You will need to generate - one from your original imagery. - + alignedVolumeUrl ? ( + + {" "} + + Download Aligned Volume (H5J) + + + ) : ( + + We don't have + a 3D representation of your uploaded file. You will need to generate + one from your original imagery. + + ) ) : ( "" )} From 5b34093c679556eec85851b089cf3e71f7843b53 Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Tue, 31 Mar 2026 14:55:36 -0400 Subject: [PATCH 09/10] feat: persist selected channel from mask selection to search record Add channel to GraphQL queries and updateSearch mutation so the user's channel selection is saved and available for vol-viewer links. Also fix selectedLibraries initial value to be an array for the multi-select component. Requires neuronbridge-services 5acc815 (fix: adds channel to UpdateSearchInput mutation) --- schema.graphql | 1 + src/components/MaskSelection.jsx | 4 ++-- src/graphql/mutations.js | 1 + src/graphql/queries.js | 3 +++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/schema.graphql b/schema.graphql index 997ac06e..1224ae92 100644 --- a/schema.graphql +++ b/schema.graphql @@ -214,6 +214,7 @@ input UpdateSearchInput { anatomicalRegion: String cdsFinished: AWSDateTime cdsStarted: AWSDateTime + channel: Int completedBatches: Int computedMIPs: [String] dataThreshold: Int diff --git a/src/components/MaskSelection.jsx b/src/components/MaskSelection.jsx index 640f24fa..2b158dea 100644 --- a/src/components/MaskSelection.jsx +++ b/src/components/MaskSelection.jsx @@ -127,7 +127,7 @@ export default function MaskSelection({ match }) { }); } - const maskDetails = { searchMask: maskName, id: searchMeta.id }; + const maskDetails = { searchMask: maskName, channel, id: searchMeta.id }; const searchParams = { ...searchFormData, ...maskDetails }; API.graphql( graphqlOperation(mutations.updateSearch, { input: searchParams }), @@ -216,7 +216,7 @@ export default function MaskSelection({ match }) { wrapperCol={{ span: 16 }} name="basic" initialValues={{ - selectedLibraries: searchMeta.searchType || getDefaultLibrary(appState?.dataConfig?.stores, searchMeta.anatomicalRegion) || undefined, + selectedLibraries: [searchMeta.searchType || getDefaultLibrary(appState?.dataConfig?.stores, searchMeta.anatomicalRegion)].filter(Boolean), dataThreshold: searchMeta.dataThreshold || 100, maskThreshold: searchMeta.maskThreshold || 100, xyShift: searchMeta.xyShift || 0, diff --git a/src/graphql/mutations.js b/src/graphql/mutations.js index 45256024..8c218b69 100644 --- a/src/graphql/mutations.js +++ b/src/graphql/mutations.js @@ -39,6 +39,7 @@ export const updateSearch = /* GraphQL */ ` identityId owner step + channel upload uploadThumbnail searchDir diff --git a/src/graphql/queries.js b/src/graphql/queries.js index 54c4863f..0966a18e 100644 --- a/src/graphql/queries.js +++ b/src/graphql/queries.js @@ -28,6 +28,7 @@ export const getSearch = /* GraphQL */ ` voxelX voxelY voxelZ + channel referenceChannel alignStarted alignFinished @@ -59,6 +60,7 @@ export const listSearches = /* GraphQL */ ` alignmentMovie alignedVolume anatomicalRegion + channel cdsStarted cdsFinished step @@ -100,6 +102,7 @@ export const listItemsByOwner = ` alignmentMovie alignedVolume anatomicalRegion + channel cdsStarted cdsFinished step From 72298b2c69c5a062b5a1d74a64f006d7c0d81417 Mon Sep 17 00:00:00 2001 From: Jody Clements Date: Tue, 31 Mar 2026 14:57:23 -0400 Subject: [PATCH 10/10] feat: support custom uploads in ViewIn3DButton and show 3D button when aligned volume exists Use signed private links for custom upload h5j volumes and the persisted channel from the search record. Show the View in 3D button in Summary and Download3D when an aligned volume is present. --- src/components/MatchModal/Download3D.jsx | 2 +- src/components/MatchModal/Summary.jsx | 2 +- src/components/MatchModal/ViewIn3DButton.jsx | 45 ++++++++++++++------ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/components/MatchModal/Download3D.jsx b/src/components/MatchModal/Download3D.jsx index 733343b9..ceac8c30 100644 --- a/src/components/MatchModal/Download3D.jsx +++ b/src/components/MatchModal/Download3D.jsx @@ -97,7 +97,7 @@ export default function Download3D(props) { return ( <> - {algorithm !== "pppm" && !mask.identityId ? ( + {algorithm !== "pppm" && (!mask.identityId || mask.alignedVolume) ? (

- View the match in our online volume viewer{" "} diff --git a/src/components/MatchModal/Summary.jsx b/src/components/MatchModal/Summary.jsx index d934a38b..5217e790 100644 --- a/src/components/MatchModal/Summary.jsx +++ b/src/components/MatchModal/Summary.jsx @@ -57,7 +57,7 @@ export default function Summary(props) { > {appState.compactMeta ? "Show" : "Hide"} Match Info - {algorithm !== "pppm" && !mask.identityId ? ( + {algorithm !== "pppm" && (!mask.identityId || mask.alignedVolume) ? ( { - const swc = isLM ? getSWCLink(mask) : getSWCLink(match.image); - signedPublicLink(swc).then((signed) => { - setSignedSwc(signed); - }); + const swc = isCustomUpload + ? getSWCLink(match.image) + : isLM ? getSWCLink(mask) : getSWCLink(match.image); - if (swc && h5j) { + if (swc) { + signedPublicLink(swc).then((signed) => { + setSignedSwc(signed); + }); + } + + if (isCustomUpload) { + const volumePath = `${mask.searchDir}/${mask.alignedVolume}`; + signedLink(volumePath).then((signed) => { + setSignedH5j(signed); + if (swc) { + setDisabled(false); + } + }); + } else if (swc && h5j) { setDisabled(false); } - }, [isLM, mask, match, h5j]); + }, [isLM, isCustomUpload, mask, match, h5j]); + + const h5jUrl = isCustomUpload ? signedH5j : h5j; // must encode the signedSWC, so that it makes it to the volume viewer in the // correct state for loading the swc const volViewerLink = `${config.volumeViewer}?ref=${encodeURIComponent( ref - )}&h5j=${encodeURIComponent(h5j)}&swc=${encodeURIComponent( + )}&h5j=${encodeURIComponent(h5jUrl)}&swc=${encodeURIComponent( signedSwc )}&ch=${channel}&mx=${mirrored}`;