From d2e6889a390e4bf000bb1e6f0ec72a3978b4bb48 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:29:00 -0500 Subject: [PATCH 01/32] feat: add reusable dialog button components --- .../components/ui/buttons/DialogIconBtn.tsx | 46 +++++++++++++++++++ .../components/ui/buttons/DialogTextBtn.tsx | 45 ++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 frontend/src/components/ui/buttons/DialogIconBtn.tsx create mode 100644 frontend/src/components/ui/buttons/DialogTextBtn.tsx diff --git a/frontend/src/components/ui/buttons/DialogIconBtn.tsx b/frontend/src/components/ui/buttons/DialogIconBtn.tsx new file mode 100644 index 00000000..2f81af6a --- /dev/null +++ b/frontend/src/components/ui/buttons/DialogIconBtn.tsx @@ -0,0 +1,46 @@ +import { useState, type ReactNode, type MouseEvent } from 'react'; +import type { IconType } from 'react-icons'; + +import FgTooltip from '@/components/ui/widgets/FgTooltip'; +import FgDialog from '@/components/ui/Dialogs/FgDialog'; + +type DialogIconBtnProps = { + readonly icon: IconType; + readonly label: string; + readonly triggerClasses: string; + readonly disabled?: boolean; + readonly children: ReactNode | ((closeDialog: () => void) => ReactNode); +}; + +export default function DialogIconBtn({ + icon, + label, + triggerClasses, + disabled = false, + children +}: DialogIconBtnProps) { + const [showDialog, setShowDialog] = useState(false); + + const closeDialog = () => setShowDialog(false); + + return ( + <> + ) => { + setShowDialog(true); + e.currentTarget.blur(); + }} + triggerClasses={triggerClasses} + /> + {showDialog ? ( + + {typeof children === 'function' ? children(closeDialog) : children} + + ) : null} + + ); +} diff --git a/frontend/src/components/ui/buttons/DialogTextBtn.tsx b/frontend/src/components/ui/buttons/DialogTextBtn.tsx new file mode 100644 index 00000000..29071a35 --- /dev/null +++ b/frontend/src/components/ui/buttons/DialogTextBtn.tsx @@ -0,0 +1,45 @@ +import { useState, type ReactNode, type MouseEvent } from 'react'; +import { Button } from '@material-tailwind/react'; + +import FgDialog from '@/components/ui/Dialogs/FgDialog'; + +type TextDialogBtnProps = { + readonly label: string; + readonly variant?: 'solid' | 'outline' | 'ghost' | 'gradient' | undefined; + readonly className?: string; + readonly disabled?: boolean; + readonly children: ReactNode | ((closeDialog: () => void) => ReactNode); +}; + +export default function TextDialogBtn({ + label, + variant = 'outline', + className = '!rounded-md w-fit', + disabled = false, + children +}: TextDialogBtnProps) { + const [showDialog, setShowDialog] = useState(false); + + const closeDialog = () => setShowDialog(false); + + return ( + <> + + {showDialog ? ( + + {typeof children === 'function' ? children(closeDialog) : children} + + ) : null} + + ); +} From 84dd4ee9818e227352bccced75899e074ae9fcf9 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:29:46 -0500 Subject: [PATCH 02/32] feat: add dialog with code snippets and instructions for data links --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx new file mode 100644 index 00000000..e01b3dd0 --- /dev/null +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -0,0 +1,194 @@ +import { Typography } from '@material-tailwind/react'; +import { HiOutlineClipboardCopy } from 'react-icons/hi'; + +import FgDialog from './FgDialog'; +import FgTooltip from '../widgets/FgTooltip'; +import useCopyTooltip from '@/hooks/useCopyTooltip'; +import { copy } from '@testing-library/user-event/dist/cjs/clipboard/copy.js'; + +type CodeSnippetItem = { + type: 'code'; + label: string; + code: string; + copyText: string; + copyLabel?: string; +}; + +type InstructionItem = { + type: 'instructions'; + label: string; + steps: string[]; + copyText: string; + copyLabel?: string; +}; + +type DialogItem = CodeSnippetItem | InstructionItem; + +type CodeSnippetBlockProps = { + readonly label: string; + readonly code: string; + readonly copyText: string; + readonly copyLabel?: string; +}; + +function CopyIconAndTooltip({ + copyLabel, + copyText +}: { + readonly copyLabel: string; + readonly copyText: string; +}) { + const { showCopiedTooltip, handleCopy } = useCopyTooltip(); + + return ( + await handleCopy(copyText)} + triggerClasses="text-foreground/50 hover:text-foreground" + variant="ghost" + > + {showCopiedTooltip ? ( +
+ {copyLabel === 'Copy data link' + ? 'Data link copied!' + : copyLabel === 'Copy code' + ? 'Code copied!' + : 'Copied!'} +
+ ) : null} +
+ ); +} + +function CodeSnippetBlock({ + label, + code, + copyText, + copyLabel = 'Copy' +}: CodeSnippetBlockProps) { + return ( +
+ {label} +
+
+          {code}
+        
+
+ +
+
+
+ ); +} + +type InstructionBlockProps = { + readonly label: string; + readonly steps: string[]; + readonly copyText: string; + readonly copyLabel?: string; +}; + +function InstructionBlock({ + label, + steps, + copyText, + copyLabel = 'Copy' +}: InstructionBlockProps) { + return ( +
+
+ + {label} + + +
+
+
    + {steps.map((step, index) => ( +
  1. + + {index + 1} + + {step} +
  2. + ))} +
+
+
+ ); +} + +type DataLinkUsageDialogProps = { + readonly dataLinkUrl: string; + readonly open: boolean; + readonly onClose: () => void; +}; + +export default function DataLinkUsageDialog({ + dataLinkUrl, + open, + onClose +}: DataLinkUsageDialogProps) { + const items: DialogItem[] = [ + { + type: 'instructions', + label: 'Napari', + steps: [ + 'Install napari-ome-zarr plugin', + 'Launch napari', + 'Open the data URL' + ], + copyText: dataLinkUrl, + copyLabel: 'Copy data link' + }, + { + type: 'code', + label: 'Python', + code: `import zarr +store = zarr.open("${dataLinkUrl}")`, + copyText: `import zarr +store = zarr.open("${dataLinkUrl}")`, + copyLabel: 'Copy code' + }, + { + type: 'code', + label: 'Java', + code: `String url = "${dataLinkUrl}";`, + copyText: `String url = "${dataLinkUrl}";`, + copyLabel: 'Copy code' + } + ]; + + return ( + +
+ + How to use your data link + + {items.map(item => { + if (item.type === 'code') { + return ( + + ); + } + return ( + + ); + })} +
+
+ ); +} From dde30209c540dde1bf97e7b8d52c0b6a1ee82057 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:31:34 -0500 Subject: [PATCH 03/32] refactor: convert navigation and folder btns to use DialogIconBtn --- .../ui/BrowsePage/NavigationButton.tsx | 41 ++---- .../ui/BrowsePage/NewFolderButton.tsx | 129 ++++++++---------- 2 files changed, 72 insertions(+), 98 deletions(-) diff --git a/frontend/src/components/ui/BrowsePage/NavigationButton.tsx b/frontend/src/components/ui/BrowsePage/NavigationButton.tsx index f2a56d58..85d9ad9d 100644 --- a/frontend/src/components/ui/BrowsePage/NavigationButton.tsx +++ b/frontend/src/components/ui/BrowsePage/NavigationButton.tsx @@ -1,10 +1,7 @@ -import { useState } from 'react'; -import type { MouseEvent } from 'react'; import { IoNavigateCircleSharp } from 'react-icons/io5'; -import FgTooltip from '@/components/ui/widgets/FgTooltip'; +import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn'; import NavigationInput from '@/components/ui/BrowsePage/NavigateInput'; -import FgDialog from '@/components/ui/Dialogs/FgDialog'; type NavigationButtonProps = { readonly triggerClasses: string; @@ -13,30 +10,18 @@ type NavigationButtonProps = { export default function NavigationButton({ triggerClasses }: NavigationButtonProps) { - const [showNavigationDialog, setShowNavigationDialog] = useState(false); - return ( - <> - ) => { - setShowNavigationDialog(true); - e.currentTarget.blur(); - }} - triggerClasses={triggerClasses} - /> - {showNavigationDialog ? ( - setShowNavigationDialog(false)} - open={showNavigationDialog} - > - - - ) : null} - + + {closeDialog => ( + + )} + ); } diff --git a/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx b/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx index e9984c86..0130fff8 100644 --- a/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx +++ b/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx @@ -1,11 +1,9 @@ -import { useState } from 'react'; -import type { ChangeEvent, MouseEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { Button, Typography } from '@material-tailwind/react'; import { HiFolderAdd } from 'react-icons/hi'; import toast from 'react-hot-toast'; -import FgTooltip from '@/components/ui/widgets/FgTooltip'; -import FgDialog from '@/components/ui/Dialogs/FgDialog'; +import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn'; import { Spinner } from '@/components/ui/widgets/Loaders'; import useNewFolderDialog from '@/hooks/useNewFolderDialog'; import { useFileBrowserContext } from '@/contexts/FileBrowserContext'; @@ -17,7 +15,6 @@ type NewFolderButtonProps = { export default function NewFolderButton({ triggerClasses }: NewFolderButtonProps) { - const [showNewFolderDialog, setShowNewFolderDialog] = useState(false); const { fspName, mutations } = useFileBrowserContext(); const { handleNewFolderSubmit, newName, setNewName, isDuplicateName } = useNewFolderDialog(); @@ -25,7 +22,10 @@ export default function NewFolderButton({ const isSubmitDisabled = !newName.trim() || isDuplicateName || mutations.createFolder.isPending; - const formSubmit = async (event: React.FormEvent) => { + const formSubmit = async ( + event: React.FormEvent, + closeDialog: () => void + ) => { event.preventDefault(); const result = await handleNewFolderSubmit(); if (result.success) { @@ -34,73 +34,62 @@ export default function NewFolderButton({ } else { toast.error(`Error creating folder: ${result.error}`); } - setShowNewFolderDialog(false); + closeDialog(); }; return ( - <> - ) => { - setShowNewFolderDialog(true); - e.currentTarget.blur(); - }} - triggerClasses={triggerClasses} - /> - {showNewFolderDialog ? ( - setShowNewFolderDialog(false)} - open={showNewFolderDialog} - > -
-
- - Create a New Folder + + {closeDialog => ( + formSubmit(e, closeDialog)}> +
+ + Create a New Folder + + ) => { + setNewName(event.target.value); + }} + placeholder="Folder name ..." + type="text" + value={newName} + /> +
+
+ + {!newName.trim() ? ( + + Please enter a folder name - ) => { - setNewName(event.target.value); - }} - placeholder="Folder name ..." - type="text" - value={newName} - /> -
-
- - {!newName.trim() ? ( - - Please enter a folder name - - ) : newName.trim() && isDuplicateName ? ( - - A file or folder with this name already exists - - ) : null} -
- - - ) : null} - + ) : newName.trim() && isDuplicateName ? ( + + A file or folder with this name already exists + + ) : null} +
+ + )} + ); } From ed5dc87f36035f079085c5e4a27d27269a7328d6 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:32:48 -0500 Subject: [PATCH 04/32] feat: add data link usage dialog across app --- .../components/ui/BrowsePage/ZarrPreview.tsx | 25 ++++++++-- .../ui/PropertiesDrawer/PropertiesDrawer.tsx | 48 +++++++++++++++---- .../src/components/ui/Table/linksColumns.tsx | 22 +++++++++ 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx index 403d31fc..4b68b978 100644 --- a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx +++ b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx @@ -5,6 +5,8 @@ import type { UseQueryResult } from '@tanstack/react-query'; import zarrLogo from '@/assets/zarr.jpg'; import ZarrMetadataTable from '@/components/ui/BrowsePage/ZarrMetadataTable'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import DataToolLinks from './DataToolLinks'; import type { OpenWithToolUrls, @@ -87,11 +89,24 @@ export default function ZarrPreview({ {openWithToolUrls ? ( - + <> + + {openWithToolUrls.copy ? ( + + {closeDialog => ( + + )} + + ) : null} + ) : null} {showDataLinkDialog ? ( diff --git a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index 570d32db..bcd40e1d 100644 --- a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -17,6 +17,8 @@ import OverviewTable from '@/components/ui/PropertiesDrawer/OverviewTable'; import TicketDetails from '@/components/ui/PropertiesDrawer/TicketDetails'; import FgTooltip from '@/components/ui/widgets/FgTooltip'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import { getPreferredPathForDisplay } from '@/utils'; import { copyToClipboard } from '@/utils/copyText'; import { useFileBrowserContext } from '@/contexts/FileBrowserContext'; @@ -276,15 +278,45 @@ export default function PropertiesDrawer({ {externalDataUrlQuery.data ? ( - + <> + + + {closeDialog => ( + + )} + + ) : proxiedPathByFspAndPathQuery.data?.url ? ( - + <> + + + {closeDialog => ( + + )} + + ) : null} ) : null} diff --git a/frontend/src/components/ui/Table/linksColumns.tsx b/frontend/src/components/ui/Table/linksColumns.tsx index c0ca1ab9..d9b7c7f9 100644 --- a/frontend/src/components/ui/Table/linksColumns.tsx +++ b/frontend/src/components/ui/Table/linksColumns.tsx @@ -4,6 +4,7 @@ import { Typography } from '@material-tailwind/react'; import type { ColumnDef } from '@tanstack/react-table'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; import DataLinksActionsMenu from '@/components/ui/Menus/DataLinksActions'; import { usePreferencesContext } from '@/contexts/PreferencesContext'; import { useZoneAndFspMapContext } from '@/contexts/ZonesAndFspMapContext'; @@ -37,6 +38,7 @@ type ProxiedPathRowActionProps = { handleCopyPath: (path: string) => Promise; handleCopyUrl: (item: ProxiedPath) => Promise; handleUnshare: () => void; + handleViewDataLinkUsage: () => void; item: ProxiedPath; displayPath: string; pathFsp: FileSharePath | undefined; @@ -94,6 +96,8 @@ function PathCell({ function ActionsCell({ item }: { readonly item: ProxiedPath }) { const [showDataLinkDialog, setShowDataLinkDialog] = useState(false); + const [showDataLinkUsageDialog, setShowDataLinkUsageDialog] = + useState(false); const { handleDeleteDataLink } = useDataToolLinks(setShowDataLinkDialog); const { pathPreference } = usePreferencesContext(); const { zonesAndFspQuery } = useZoneAndFspMapContext(); @@ -114,6 +118,10 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { item.path ); + const handleViewDataLinkUsage = () => { + setShowDataLinkUsageDialog(true); + }; + const menuItems: MenuItem[] = [ { name: 'Copy path', @@ -127,6 +135,11 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { await props.handleCopyUrl(props.item); } }, + { + name: 'Example code snippets', + action: (props: ProxiedPathRowActionProps) => + props.handleViewDataLinkUsage() + }, { name: 'Unshare', action: (props: ProxiedPathRowActionProps) => props.handleUnshare(), @@ -138,6 +151,7 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { handleCopyPath, handleCopyUrl, handleUnshare, + handleViewDataLinkUsage, item, displayPath, pathFsp @@ -169,6 +183,14 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { showDataLinkDialog={showDataLinkDialog} /> ) : null} + {/* Code snippets dialog */} + {showDataLinkUsageDialog ? ( + setShowDataLinkUsageDialog(false)} + open={showDataLinkUsageDialog} + /> + ) : null} ); } From 5b40748ab6b7e251de68d7f8c13375efbe372b67 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:33:07 -0500 Subject: [PATCH 05/32] fix: increase tooltip z-index to prevent overlap with dialogs --- frontend/src/components/ui/widgets/FgTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ui/widgets/FgTooltip.tsx b/frontend/src/components/ui/widgets/FgTooltip.tsx index 55d73896..35e5bc26 100644 --- a/frontend/src/components/ui/widgets/FgTooltip.tsx +++ b/frontend/src/components/ui/widgets/FgTooltip.tsx @@ -42,7 +42,7 @@ export default function FgTooltip({ > {Icon ? : null} {children} - + {label} From e657a1a52832bd40b634b269e57eba4d496e5558 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 15:05:11 -0500 Subject: [PATCH 06/32] style: organize example languages/tools in tabs --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 204 ++++++++---------- 1 file changed, 91 insertions(+), 113 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index e01b3dd0..730c0ba4 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -1,28 +1,10 @@ -import { Typography } from '@material-tailwind/react'; +import { useState } from 'react'; +import { Typography, Tabs } from '@material-tailwind/react'; import { HiOutlineClipboardCopy } from 'react-icons/hi'; import FgDialog from './FgDialog'; import FgTooltip from '../widgets/FgTooltip'; import useCopyTooltip from '@/hooks/useCopyTooltip'; -import { copy } from '@testing-library/user-event/dist/cjs/clipboard/copy.js'; - -type CodeSnippetItem = { - type: 'code'; - label: string; - code: string; - copyText: string; - copyLabel?: string; -}; - -type InstructionItem = { - type: 'instructions'; - label: string; - steps: string[]; - copyText: string; - copyLabel?: string; -}; - -type DialogItem = CodeSnippetItem | InstructionItem; type CodeSnippetBlockProps = { readonly label: string; @@ -62,60 +44,38 @@ function CopyIconAndTooltip({ } function CodeSnippetBlock({ - label, code, copyText, copyLabel = 'Copy' -}: CodeSnippetBlockProps) { +}: Omit) { return ( -
- {label} -
-
-          {code}
-        
-
- -
+
+
+        {code}
+      
+
+
); } type InstructionBlockProps = { - readonly label: string; readonly steps: string[]; - readonly copyText: string; - readonly copyLabel?: string; }; -function InstructionBlock({ - label, - steps, - copyText, - copyLabel = 'Copy' -}: InstructionBlockProps) { +function InstructionBlock({ steps }: InstructionBlockProps) { return ( -
-
- - {label} - - -
-
-
    - {steps.map((step, index) => ( -
  1. - - {index + 1} - - {step} -
  2. - ))} -
-
-
+
    + {steps.map((step, index) => ( +
  1. + + {index + 1} + + {step} +
  2. + ))} +
); } @@ -130,64 +90,82 @@ export default function DataLinkUsageDialog({ open, onClose }: DataLinkUsageDialogProps) { - const items: DialogItem[] = [ - { - type: 'instructions', - label: 'Napari', - steps: [ - 'Install napari-ome-zarr plugin', - 'Launch napari', - 'Open the data URL' - ], - copyText: dataLinkUrl, - copyLabel: 'Copy data link' - }, - { - type: 'code', - label: 'Python', - code: `import zarr -store = zarr.open("${dataLinkUrl}")`, - copyText: `import zarr -store = zarr.open("${dataLinkUrl}")`, - copyLabel: 'Copy code' - }, - { - type: 'code', - label: 'Java', - code: `String url = "${dataLinkUrl}";`, - copyText: `String url = "${dataLinkUrl}";`, - copyLabel: 'Copy code' - } - ]; + const [activeTab, setActiveTab] = useState('napari'); return ( -
- - How to use your data link - - {items.map(item => { - if (item.type === 'code') { - return ( - - ); - } - return ( +
+
+ + How to use your data link + + +
+ + + + Napari + + + + Python + + + + Java + + + + + {/* Napari panel */} + + + + {/* Python panel */} + + + + + + {/* Java panel */} + + - ); - })} + +
); From 169bb0b8a28ec3a18537716d4472e5d94c28350d Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 15:59:00 -0500 Subject: [PATCH 07/32] refactor: extract dark mode logic for code blocks --- .../components/ui/BrowsePage/FileViewer.tsx | 22 ++------------- frontend/src/hooks/useDarkMode.ts | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 frontend/src/hooks/useDarkMode.ts diff --git a/frontend/src/components/ui/BrowsePage/FileViewer.tsx b/frontend/src/components/ui/BrowsePage/FileViewer.tsx index a4c16ebf..8feaec50 100644 --- a/frontend/src/components/ui/BrowsePage/FileViewer.tsx +++ b/frontend/src/components/ui/BrowsePage/FileViewer.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import { Typography } from '@material-tailwind/react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { @@ -10,6 +9,7 @@ import { useFileBrowserContext } from '@/contexts/FileBrowserContext'; import { formatFileSize, formatUnixTimestamp } from '@/utils'; import type { FileOrFolder } from '@/shared.types'; import { useFileContentQuery } from '@/queries/fileContentQueries'; +import useDarkMode from '@/hooks/useDarkMode'; type FileViewerProps = { readonly file: FileOrFolder; @@ -76,27 +76,9 @@ const getLanguageFromExtension = (filename: string): string => { export default function FileViewer({ file }: FileViewerProps) { const { fspName } = useFileBrowserContext(); - - const [isDarkMode, setIsDarkMode] = useState(false); - + const isDarkMode = useDarkMode(); const contentQuery = useFileContentQuery(fspName, file.path); - // Detect dark mode from document - useEffect(() => { - const checkDarkMode = () => { - setIsDarkMode(document.documentElement.classList.contains('dark')); - }; - - checkDarkMode(); - const observer = new MutationObserver(checkDarkMode); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['class'] - }); - - return () => observer.disconnect(); - }, []); - const renderViewer = () => { if (contentQuery.isLoading) { return ( diff --git a/frontend/src/hooks/useDarkMode.ts b/frontend/src/hooks/useDarkMode.ts new file mode 100644 index 00000000..83187a9e --- /dev/null +++ b/frontend/src/hooks/useDarkMode.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +/** + * Hook to detect dark mode from the document element's class list. + * Observes changes to the document element's class attribute. + * @returns boolean indicating if dark mode is active + */ +export default function useDarkMode(): boolean { + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + const checkDarkMode = () => { + setIsDarkMode(document.documentElement.classList.contains('dark')); + }; + + checkDarkMode(); + const observer = new MutationObserver(checkDarkMode); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }); + + return () => observer.disconnect(); + }, []); + + return isDarkMode; +} From 2b5eaf619796b882d3c3937a2df614c793e60c02 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 16:00:05 -0500 Subject: [PATCH 08/32] feat: add SyntaxHighlighter to data link usage dialog --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 113 ++++++++++++++---- 1 file changed, 90 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 730c0ba4..bf15205e 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -1,17 +1,16 @@ import { useState } from 'react'; import { Typography, Tabs } from '@material-tailwind/react'; import { HiOutlineClipboardCopy } from 'react-icons/hi'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { + materialDark, + coy +} from 'react-syntax-highlighter/dist/esm/styles/prism'; import FgDialog from './FgDialog'; import FgTooltip from '../widgets/FgTooltip'; import useCopyTooltip from '@/hooks/useCopyTooltip'; - -type CodeSnippetBlockProps = { - readonly label: string; - readonly code: string; - readonly copyText: string; - readonly copyLabel?: string; -}; +import useDarkMode from '@/hooks/useDarkMode'; function CopyIconAndTooltip({ copyLabel, @@ -43,20 +42,87 @@ function CopyIconAndTooltip({ ); } -function CodeSnippetBlock({ +type CodeBlockProps = { + readonly code: string; + readonly language?: string; + readonly showLineNumbers?: boolean; + readonly wrapLines?: boolean; + readonly wrapLongLines?: boolean; + readonly copyable?: boolean; + readonly copyLabel?: string; + readonly customStyle?: React.CSSProperties; +}; + +function CodeBlock({ code, - copyText, - copyLabel = 'Copy' -}: Omit) { + language = 'text', + showLineNumbers = false, + wrapLines = true, + wrapLongLines = true, + copyable = false, + copyLabel = 'Copy code', + customStyle = { + margin: 0, + marginTop: 0, + marginRight: 0, + marginBottom: 0, + marginLeft: 0, + paddingTop: '1em', + paddingRight: '1em', + paddingBottom: '0', + paddingLeft: '1em', + fontSize: '14px', + lineHeight: '1.5' + } +}: CodeBlockProps) { + const isDarkMode = useDarkMode(); + const { showCopiedTooltip, handleCopy } = useCopyTooltip(); + + // Get the theme's code styles and merge with custom codeTagProps + const theme = isDarkMode ? materialDark : coy; + const themeCodeStyles = theme['code[class*="language-"]'] || {}; + const mergedCodeTagProps = { + style: { + ...themeCodeStyles, + paddingBottom: '1em' + } + }; + return ( -
-
+    <>
+      
         {code}
-      
-
- -
-
+ + {copyable ? ( +
+ await handleCopy(code)} + triggerClasses="text-foreground/50 hover:text-foreground" + variant="ghost" + > + {showCopiedTooltip ? ( +
+ {copyLabel === 'Copy data link' + ? 'Data link copied!' + : copyLabel === 'Copy code' + ? 'Code copied!' + : 'Copied!'} +
+ ) : null} +
+
+ ) : null} + ); } @@ -145,12 +211,12 @@ export default function DataLinkUsageDialog({ value="python" > - @@ -159,10 +225,11 @@ store = zarr.open("${dataLinkUrl}")`} className="flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface" value="java" > - From 207d1d636cbaed0e8e005c201376fe4f1b5a81be Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 16:00:23 -0500 Subject: [PATCH 09/32] style: add padding to FileViewer so scroll bar doesn't cover last line of code --- .../src/components/ui/BrowsePage/FileViewer.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ui/BrowsePage/FileViewer.tsx b/frontend/src/components/ui/BrowsePage/FileViewer.tsx index 8feaec50..8c94a4e9 100644 --- a/frontend/src/components/ui/BrowsePage/FileViewer.tsx +++ b/frontend/src/components/ui/BrowsePage/FileViewer.tsx @@ -103,11 +103,25 @@ export default function FileViewer({ file }: FileViewerProps) { const language = getLanguageFromExtension(file.name); const content = contentQuery.data ?? ''; + // Get the theme's code styles and merge with padding bottom for scrollbar + const theme = isDarkMode ? materialDark : coy; + const themeCodeStyles = theme['code[class*="language-"]'] || {}; + const mergedCodeTagProps = { + style: { + ...themeCodeStyles, + paddingBottom: '1em' + } + }; + return ( Date: Fri, 12 Dec 2025 16:06:06 -0500 Subject: [PATCH 10/32] chore: change text for data link usage btn in properties panel --- .../src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index bcd40e1d..c139ac9d 100644 --- a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -303,7 +303,7 @@ export default function PropertiesDrawer({ path={proxiedPathByFspAndPathQuery.data.url} /> {closeDialog => ( From 0cfb5ef7fe08e0a1968ac2417aa6ba295ca5e105 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 17:10:25 -0500 Subject: [PATCH 11/32] refactor: map over data to remove duplicate styling on tabs - also fixes location of the copy icon for the code blocks --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 119 ++++++++++-------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index bf15205e..3a49fb0e 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -67,7 +67,7 @@ function CodeBlock({ marginRight: 0, marginBottom: 0, marginLeft: 0, - paddingTop: '1em', + paddingTop: '2em', paddingRight: '1em', paddingBottom: '0', paddingLeft: '1em', @@ -89,7 +89,7 @@ function CodeBlock({ }; return ( - <> +
) : null} - +
); } @@ -158,6 +158,53 @@ export default function DataLinkUsageDialog({ }: DataLinkUsageDialogProps) { const [activeTab, setActiveTab] = useState('napari'); + const tabs = [ + { + id: 'napari', + label: 'Napari', + content: ( + + ) + }, + { + id: 'python', + label: 'Python', + content: ( + <> + + + + ) + }, + { + id: 'java', + label: 'Java', + content: ( + + ) + } + ]; + + const TRIGGER_CLASSES = '!text-foreground h-full'; + const PANEL_CLASSES = + 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light'; + return (
@@ -177,61 +224,23 @@ export default function DataLinkUsageDialog({ value={activeTab} > - - Napari - - - - Python - - - - Java - + {tabs.map(tab => ( + + {tab.label} + + ))} - {/* Napari panel */} - - - - - {/* Python panel */} - - - - - - {/* Java panel */} - - - + {tabs.map(tab => ( + + {tab.content} + + ))}
From 033d82e04cb4b7f5da8b30a44b316e6729b12546 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Mon, 5 Jan 2026 16:01:19 -0500 Subject: [PATCH 12/32] fix: show 'copied' msg for data link copy tooltip - in the DataLinkUsageDialog, the copied message was not showing due to a CSS issue. However, in addressing this, I realized that this component was recreating logic from the CopyTooltip component and reworked it to use that component --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 76 +++++-------------- 1 file changed, 20 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 3a49fb0e..5ef23393 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -8,39 +8,8 @@ import { } from 'react-syntax-highlighter/dist/esm/styles/prism'; import FgDialog from './FgDialog'; -import FgTooltip from '../widgets/FgTooltip'; -import useCopyTooltip from '@/hooks/useCopyTooltip'; import useDarkMode from '@/hooks/useDarkMode'; - -function CopyIconAndTooltip({ - copyLabel, - copyText -}: { - readonly copyLabel: string; - readonly copyText: string; -}) { - const { showCopiedTooltip, handleCopy } = useCopyTooltip(); - - return ( - await handleCopy(copyText)} - triggerClasses="text-foreground/50 hover:text-foreground" - variant="ghost" - > - {showCopiedTooltip ? ( -
- {copyLabel === 'Copy data link' - ? 'Data link copied!' - : copyLabel === 'Copy code' - ? 'Code copied!' - : 'Copied!'} -
- ) : null} -
- ); -} +import CopyTooltip from '@/components/ui/widgets/CopyTooltip'; type CodeBlockProps = { readonly code: string; @@ -53,6 +22,9 @@ type CodeBlockProps = { readonly customStyle?: React.CSSProperties; }; +const TOOLTIP_TRIGGER_CLASSES = + 'text-foreground/50 hover:text-foreground py-1 px-2'; + function CodeBlock({ code, language = 'text', @@ -67,7 +39,7 @@ function CodeBlock({ marginRight: 0, marginBottom: 0, marginLeft: 0, - paddingTop: '2em', + paddingTop: '3em', paddingRight: '1em', paddingBottom: '0', paddingLeft: '1em', @@ -76,7 +48,6 @@ function CodeBlock({ } }: CodeBlockProps) { const isDarkMode = useDarkMode(); - const { showCopiedTooltip, handleCopy } = useCopyTooltip(); // Get the theme's code styles and merge with custom codeTagProps const theme = isDarkMode ? materialDark : coy; @@ -103,23 +74,13 @@ function CodeBlock({ {copyable ? (
- await handleCopy(code)} - triggerClasses="text-foreground/50 hover:text-foreground" - variant="ghost" + - {showCopiedTooltip ? ( -
- {copyLabel === 'Copy data link' - ? 'Data link copied!' - : copyLabel === 'Copy code' - ? 'Code copied!' - : 'Copied!'} -
- ) : null} -
+ +
) : null}
@@ -201,7 +162,7 @@ export default function DataLinkUsageDialog({ } ]; - const TRIGGER_CLASSES = '!text-foreground h-full'; + const TAB_TRIGGER_CLASSES = '!text-foreground h-full'; const PANEL_CLASSES = 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light'; @@ -212,10 +173,13 @@ export default function DataLinkUsageDialog({ How to use your data link - + + +
{tabs.map(tab => ( From f5ad35dd8074970556633081ee858a9a47fe071a Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Mon, 5 Jan 2026 16:07:06 -0500 Subject: [PATCH 13/32] feat: add fiji instructions to DataLinkUsageDialog --- .../components/ui/Dialogs/DataLinkUsageDialog.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 5ef23393..8ff16379 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -159,6 +159,20 @@ export default function DataLinkUsageDialog({ language="java" /> ) + }, + { + id: 'fiji', + label: 'Fiji', + content: ( + + ) } ]; From cdf17aaf14b662fa98b6db7adc1232bd949cb807 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Mon, 5 Jan 2026 16:10:53 -0500 Subject: [PATCH 14/32] feat: add VVDViewer instructions to DataLinkUsageDialog --- .../components/ui/Dialogs/DataLinkUsageDialog.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 8ff16379..deb42927 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -173,6 +173,20 @@ export default function DataLinkUsageDialog({ ]} /> ) + }, + { + id: 'vvdViewer', + label: 'VVDViewer', + content: ( + + ) } ]; From d7442f5091f18c311e07aeb15abc3868b3096cd5 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 20 Jan 2026 21:24:12 -0500 Subject: [PATCH 15/32] feat: add python sample code --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 5ef23393..ca45f921 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -140,7 +140,46 @@ export default function DataLinkUsageDialog({ <> Date: Thu, 22 Jan 2026 09:38:03 -0500 Subject: [PATCH 16/32] style: limit height of data link usage dialog --- frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index f399095f..14eff658 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -249,7 +249,7 @@ if '0' in root: Date: Thu, 22 Jan 2026 09:38:31 -0500 Subject: [PATCH 17/32] refactor: remove java code block until we have a working example --- .../components/ui/Dialogs/DataLinkUsageDialog.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 14eff658..4c03b09e 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -187,18 +187,6 @@ if '0' in root: ) }, - { - id: 'java', - label: 'Java', - content: ( - - ) - }, { id: 'fiji', label: 'Fiji', From c2681d6fea480a4558699d43150fedd8042778a0 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 27 Jan 2026 09:08:26 -0500 Subject: [PATCH 18/32] wip: initial Java example --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 4c03b09e..dbc7e524 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -187,6 +187,282 @@ if '0' in root: ) }, + { + id: 'java', + label: 'Java', + content: ( + 0 ? args[0] : URL; + String groupName = args.length > 1 ? args[1] : "r0"; + String arraySubpath = args.length > 2 ? args[2] : "0"; + runExampleUsingN5API(url, groupName, arraySubpath); + } + + + private static void runExampleUsingN5API(String url, String groupName, String arraySubpath) { + try { + N5URI n5URI = new N5URI(url).resolve(groupName); + // Open the zarr store using N5ZarrReader for HTTP access + N5Factory n5Factory = new N5Factory().cacheAttributes(true); + N5Reader reader = n5Factory.openReader(StorageFormat.ZARR, n5URI.getURI()); + // new N5Factory().openReader(n5URI.getContainerPath()); + + // Read OME-ZARR metadata from .zattrs + JsonObject zattrs = readZattrs(n5URI); + if (zattrs != null && zattrs.has("multiscales")) { + JsonArray multiscales = zattrs.getAsJsonArray("multiscales"); + JsonObject firstMultiscale = multiscales.get(0).getAsJsonObject(); + + // Print version + if (firstMultiscale.has("version")) { + System.out.println(" Version: " + firstMultiscale.get("version").getAsString()); + } + + // Print name + if (firstMultiscale.has("name")) { + System.out.println(" Name: " + firstMultiscale.get("name").getAsString()); + } + + // Print axes + if (firstMultiscale.has("axes")) { + JsonArray axes = firstMultiscale.getAsJsonArray("axes"); + System.out.print(" Axes: ["); + for (int i = 0; i < axes.size(); i++) { + JsonObject axis = axes.get(i).getAsJsonObject(); + String name = axis.get("name").getAsString(); + String type = axis.has("type") ? axis.get("type").getAsString() : "null"; + String unit = axis.has("unit") ? axis.get("unit").getAsString() : "null"; + System.out.print("(" + name + ", " + type + ", " + unit + ")"); + if (i < axes.size() - 1) System.out.print(", "); + } + System.out.println("]"); + } + + // Print datasets + if (firstMultiscale.has("datasets")) { + JsonArray datasets = firstMultiscale.getAsJsonArray("datasets"); + System.out.print(" Datasets: ["); + for (int i = 0; i < datasets.size(); i++) { + JsonObject ds = datasets.get(i).getAsJsonObject(); + System.out.print(ds.get("path").getAsString()); + if (i < datasets.size() - 1) System.out.print(", "); + } + System.out.println("]"); + + // Print coordinate transforms for each scale level + for (int i = 0; i < datasets.size(); i++) { + JsonObject ds = datasets.get(i).getAsJsonObject(); + String path = ds.get("path").getAsString(); + if (ds.has("coordinateTransformations")) { + JsonArray transforms = ds.getAsJsonArray("coordinateTransformations"); + for (JsonElement t : transforms) { + JsonObject transform = t.getAsJsonObject(); + if (transform.has("scale")) { + JsonArray scale = transform.getAsJsonArray("scale"); + System.out.println(" " + path + " scale: " + scale); + } + if (transform.has("translation")) { + JsonArray translation = transform.getAsJsonArray("translation"); + System.out.println(" " + path + " translation: " + translation); + } + } + } + } + } + } + + // Access the highest resolution array (dataset "0") + if (reader.exists(arraySubpath)) { + DatasetAttributes attrs = reader.getDatasetAttributes(arraySubpath); + long[] dimensions = attrs.getDimensions(); + int[] blockSize = attrs.getBlockSize(); + + System.out.println("\\nHighest resolution array shape: " + Arrays.toString(dimensions)); + System.out.println("Array dtype: " + attrs.getDataType()); + System.out.println("Array chunks: " + Arrays.toString(blockSize)); + + // N5 reads whole chunks, so we read a block at a position that covers similar data + // Note that in java the order of the axes is: XYZCT + long[] blockGridPosition = new long[]{ + 1000 / blockSize[0], 1000 / blockSize[1], 500 / blockSize[2], 0, 0 + }; + double[] minMax = readBlockMinMax(reader, "0", blockGridPosition); + if (minMax != null) { + System.out.println("\\nBlock at grid position " + Arrays.toString(blockGridPosition) + ":"); + System.out.println("Data min: " + minMax[0] + ", max: " + minMax[1]); + } + + // Use imglib2 to read an arbitrary interval (similar to Python slice [0,0,500:600,1000:1100,1000:1100]) + // In Java XYZCT order: x=[1000,1100), y=[1000,1100), z=[500,600), c=0, t=0 + long[] intervalMin = new long[]{1000, 1000, 500, 0, 0}; + long[] intervalMax = new long[]{1099, 1099, 599, 0, 0}; + double[] intervalMinMax = readIntervalMinMax(reader, "0", intervalMin, intervalMax); + if (intervalMinMax != null) { + System.out.println("\\nInterval [" + Arrays.toString(intervalMin) + " - " + Arrays.toString(intervalMax) + "]:"); + System.out.println("Data min: " + intervalMinMax[0] + ", max: " + intervalMinMax[1]); + } + } + + reader.close(); + + } catch (Exception e) { + System.err.println("Error reading OME-ZARR: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Read the .zattrs file from the Zarr root to get OME-ZARR metadata. + */ + private static JsonObject readZattrs(N5URI baseUrl) { + try { + N5URI zattrsUri = baseUrl.resolve(".zattrs"); + URL url = zattrsUri.getURI().toURL(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { + String content = reader.lines().collect(Collectors.joining("\\n")); + return GSON.fromJson(content, JsonObject.class); + } + } catch (Exception e) { + System.err.println("Could not read .zattrs: " + e.getMessage()); + return null; + } + } + + /** + * Read a data block and compute its min and max values. + */ + private static double[] readBlockMinMax(N5Reader reader, String dataset, long[] gridPosition) { + try { + DataBlock block = reader.readBlock(dataset, reader.getDatasetAttributes(dataset), gridPosition); + if (block == null) { + System.err.println("Block not found at position " + Arrays.toString(gridPosition)); + return null; + } + + Object data = block.getData(); + double min = Double.MAX_VALUE; + double max = Double.MIN_VALUE; + + if (data instanceof short[]) { + short[] arr = (short[]) data; + for (short v : arr) { + int unsigned = v & 0xFFFF; + if (unsigned < min) min = unsigned; + if (unsigned > max) max = unsigned; + } + } else if (data instanceof int[]) { + int[] arr = (int[]) data; + for (int v : arr) { + if (v < min) min = v; + if (v > max) max = v; + } + } else if (data instanceof float[]) { + float[] arr = (float[]) data; + for (float v : arr) { + if (v < min) min = v; + if (v > max) max = v; + } + } else if (data instanceof double[]) { + double[] arr = (double[]) data; + for (double v : arr) { + if (v < min) min = v; + if (v > max) max = v; + } + } else if (data instanceof byte[]) { + byte[] arr = (byte[]) data; + for (byte v : arr) { + int unsigned = v & 0xFF; + if (unsigned < min) min = unsigned; + if (unsigned > max) max = unsigned; + } + } else if (data instanceof long[]) { + long[] arr = (long[]) data; + for (long v : arr) { + if (v < min) min = v; + if (v > max) max = v; + } + } else { + System.err.println("Unsupported data type: " + data.getClass().getSimpleName()); + return null; + } + + return new double[]{min, max}; + } catch (Exception e) { + System.err.println("Error reading block: " + e.getMessage()); + return null; + } + } + + /** + * Read an arbitrary interval using imglib2 and compute min/max values. + */ + private static & RealType> double[] readIntervalMinMax( + N5Reader reader, String dataset, long[] intervalMin, long[] intervalMax) { + try { + RandomAccessibleInterval img = N5Utils.open(reader, dataset); + RandomAccessibleInterval interval = Views.interval(img, intervalMin, intervalMax); + + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + + for (T pixel : Views.flatIterable(interval)) { + double val = pixel.getRealDouble(); + if (val < min) min = val; + if (val > max) max = val; + } + + return new double[]{min, max}; + } catch (Exception e) { + System.err.println("Error reading interval: " + e.getMessage()); + return null; + } + } +} + `} + copyLabel="Copy code" + copyable={true} + language="python" + /> + ) + }, { id: 'fiji', label: 'Fiji', From bfcd8ac55276e1f96e8acdf097e18f1a6801bc0a Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 27 Jan 2026 09:09:56 -0500 Subject: [PATCH 19/32] fix: put quotes around strings in code snippets --- frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index dbc7e524..bbeade03 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -144,7 +144,7 @@ export default function DataLinkUsageDialog({ from zarr.storage import FsspecStore from ome_zarr_models.v04.image import Image -url = ${dataLinkUrl} +url = '${dataLinkUrl}' # Open the zarr store using fsspec for HTTP access store = FsspecStore.from_url(url) @@ -225,7 +225,7 @@ import java.util.stream.Collectors; * Simple example to read OME-ZARR from an HTTP URL using N5-Zarr. */ public class ReadOmeZarr { - private static final String URL = ${dataLinkUrl}; + private static final String URL = '${dataLinkUrl}'; private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); public static void main(String[] args) { From 2ca628609475966afebff875cc2749935f0cd086 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 27 Jan 2026 09:31:56 -0500 Subject: [PATCH 20/32] fix: move scrollbars to inside tab panel instead of on overall dialog --- frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index bbeade03..517e6e21 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -44,7 +44,8 @@ function CodeBlock({ paddingBottom: '0', paddingLeft: '1em', fontSize: '14px', - lineHeight: '1.5' + lineHeight: '1.5', + width: 'max-content' } }: CodeBlockProps) { const isDarkMode = useDarkMode(); @@ -495,7 +496,7 @@ public class ReadOmeZarr { const TAB_TRIGGER_CLASSES = '!text-foreground h-full'; const PANEL_CLASSES = - 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light'; + 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light overflow-auto'; return ( From 978bb5536d008d3226077cca4be5fd0921723530 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Thu, 12 Feb 2026 15:20:03 -0500 Subject: [PATCH 21/32] feat: add link out to data link docs under "create data link" toggle in properties panel --- .../ui/PropertiesDrawer/PropertiesDrawer.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index e1eb313d..8e53f8ab 100644 --- a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -8,7 +8,12 @@ import { Tabs } from '@material-tailwind/react'; import toast from 'react-hot-toast'; -import { HiOutlineDocument, HiOutlineDuplicate, HiX } from 'react-icons/hi'; +import { + HiExternalLink, + HiOutlineDocument, + HiOutlineDuplicate, + HiX +} from 'react-icons/hi'; import { HiOutlineFolder } from 'react-icons/hi2'; import { useLocation } from 'react-router'; @@ -276,6 +281,20 @@ export default function PropertiesDrawer({ ? 'Deleting the data link will remove data access for collaborators with the link.' : 'Creating a data link allows you to share the data at this path with internal collaborators or use tools to view the data.'} + {!externalDataUrlQuery.data && + !proxiedPathByFspAndPathQuery.data && ( + + + Learn more about data links + + + + )} {externalDataUrlQuery.data ? ( <> From 12bb6cac1fb02f1dd3f70bcdd1d1d9ac63b310e1 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 13 Feb 2026 15:48:34 +0000 Subject: [PATCH 22/32] feat: make DataLinkUsageDialog context-aware with dataType prop --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 580 +++++++++--------- 1 file changed, 277 insertions(+), 303 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 517e6e21..51500ee7 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -11,6 +11,18 @@ import FgDialog from './FgDialog'; import useDarkMode from '@/hooks/useDarkMode'; import CopyTooltip from '@/components/ui/widgets/CopyTooltip'; +export type DataLinkType = 'directory' | 'zarr' | 'n5'; + +export function inferDataType(path: string): DataLinkType { + if (/\.zarr(\/|$)/.test(path)) { + return 'zarr'; + } + if (/\.n5(\/|$)/.test(path)) { + return 'n5'; + } + return 'directory'; +} + type CodeBlockProps = { readonly code: string; readonly language?: string; @@ -109,27 +121,239 @@ function InstructionBlock({ steps }: InstructionBlockProps) { type DataLinkUsageDialogProps = { readonly dataLinkUrl: string; + readonly dataType: DataLinkType; readonly open: boolean; readonly onClose: () => void; }; -export default function DataLinkUsageDialog({ - dataLinkUrl, - open, - onClose -}: DataLinkUsageDialogProps) { - const [activeTab, setActiveTab] = useState('napari'); +function getTabsForDataType(dataType: DataLinkType, dataLinkUrl: string) { + if (dataType === 'directory') { + return [ + { + id: 'python', + label: 'Python', + content: ( + <> + + + + ) + }, + { + id: 'java', + label: 'Java', + content: ( + response = client.send(request, + HttpResponse.BodyHandlers.ofString()); + + Document doc = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(new ByteArrayInputStream( + response.body().getBytes())); + + // Print files + NodeList contents = doc.getElementsByTagName("Contents"); + for (int i = 0; i < contents.getLength(); i++) { + Element el = (Element) contents.item(i); + String key = el.getElementsByTagName("Key") + .item(0).getTextContent(); + String size = el.getElementsByTagName("Size") + .item(0).getTextContent(); + System.out.println(key + " (" + size + " bytes)"); + } + + // Print subdirectories + NodeList prefixes = doc.getElementsByTagName("CommonPrefixes"); + for (int i = 0; i < prefixes.getLength(); i++) { + Element el = (Element) prefixes.item(i); + String prefix = el.getElementsByTagName("Prefix") + .item(0).getTextContent(); + System.out.println(prefix + " (directory)"); + } + } +}`} + copyLabel="Copy code" + copyable={true} + language="java" + /> + ) + } + ]; + } + + if (dataType === 'zarr') { + return [ + { + id: 'napari', + label: 'Napari', + content: ( + + ) + }, + { + id: 'python', + label: 'Python', + content: ( + <> + + + + ) + }, + { + id: 'java', + label: 'Java', + content: ( + block = reader.readBlock( + dataset, attrs, blockPosition); + if (block != null) { + Object data = block.getData(); + if (data instanceof short[]) { + System.out.println("Voxels: " + Arrays.toString((short[]) data)); + } else if (data instanceof float[]) { + System.out.println("Voxels: " + Arrays.toString((float[]) data)); + } else if (data instanceof byte[]) { + System.out.println("Voxels: " + Arrays.toString((byte[]) data)); + } + } - const tabs = [ + reader.close(); + } +}`} + copyLabel="Copy code" + copyable={true} + language="java" + /> + ) + }, + { + id: 'fiji', + label: 'Fiji', + content: ( + + ) + } + ]; + } + + // dataType === 'n5' + return [ { id: 'napari', label: 'Napari', content: ( ) @@ -143,44 +367,22 @@ export default function DataLinkUsageDialog({ 0 ? args[0] : URL; - String groupName = args.length > 1 ? args[1] : "r0"; - String arraySubpath = args.length > 2 ? args[2] : "0"; - runExampleUsingN5API(url, groupName, arraySubpath); - } - - - private static void runExampleUsingN5API(String url, String groupName, String arraySubpath) { - try { - N5URI n5URI = new N5URI(url).resolve(groupName); - // Open the zarr store using N5ZarrReader for HTTP access - N5Factory n5Factory = new N5Factory().cacheAttributes(true); - N5Reader reader = n5Factory.openReader(StorageFormat.ZARR, n5URI.getURI()); - // new N5Factory().openReader(n5URI.getContainerPath()); - - // Read OME-ZARR metadata from .zattrs - JsonObject zattrs = readZattrs(n5URI); - if (zattrs != null && zattrs.has("multiscales")) { - JsonArray multiscales = zattrs.getAsJsonArray("multiscales"); - JsonObject firstMultiscale = multiscales.get(0).getAsJsonObject(); - - // Print version - if (firstMultiscale.has("version")) { - System.out.println(" Version: " + firstMultiscale.get("version").getAsString()); - } - - // Print name - if (firstMultiscale.has("name")) { - System.out.println(" Name: " + firstMultiscale.get("name").getAsString()); - } - - // Print axes - if (firstMultiscale.has("axes")) { - JsonArray axes = firstMultiscale.getAsJsonArray("axes"); - System.out.print(" Axes: ["); - for (int i = 0; i < axes.size(); i++) { - JsonObject axis = axes.get(i).getAsJsonObject(); - String name = axis.get("name").getAsString(); - String type = axis.has("type") ? axis.get("type").getAsString() : "null"; - String unit = axis.has("unit") ? axis.get("unit").getAsString() : "null"; - System.out.print("(" + name + ", " + type + ", " + unit + ")"); - if (i < axes.size() - 1) System.out.print(", "); - } - System.out.println("]"); - } - - // Print datasets - if (firstMultiscale.has("datasets")) { - JsonArray datasets = firstMultiscale.getAsJsonArray("datasets"); - System.out.print(" Datasets: ["); - for (int i = 0; i < datasets.size(); i++) { - JsonObject ds = datasets.get(i).getAsJsonObject(); - System.out.print(ds.get("path").getAsString()); - if (i < datasets.size() - 1) System.out.print(", "); - } - System.out.println("]"); - - // Print coordinate transforms for each scale level - for (int i = 0; i < datasets.size(); i++) { - JsonObject ds = datasets.get(i).getAsJsonObject(); - String path = ds.get("path").getAsString(); - if (ds.has("coordinateTransformations")) { - JsonArray transforms = ds.getAsJsonArray("coordinateTransformations"); - for (JsonElement t : transforms) { - JsonObject transform = t.getAsJsonObject(); - if (transform.has("scale")) { - JsonArray scale = transform.getAsJsonArray("scale"); - System.out.println(" " + path + " scale: " + scale); - } - if (transform.has("translation")) { - JsonArray translation = transform.getAsJsonArray("translation"); - System.out.println(" " + path + " translation: " + translation); - } - } - } - } - } - } - - // Access the highest resolution array (dataset "0") - if (reader.exists(arraySubpath)) { - DatasetAttributes attrs = reader.getDatasetAttributes(arraySubpath); - long[] dimensions = attrs.getDimensions(); - int[] blockSize = attrs.getBlockSize(); - - System.out.println("\\nHighest resolution array shape: " + Arrays.toString(dimensions)); - System.out.println("Array dtype: " + attrs.getDataType()); - System.out.println("Array chunks: " + Arrays.toString(blockSize)); - - // N5 reads whole chunks, so we read a block at a position that covers similar data - // Note that in java the order of the axes is: XYZCT - long[] blockGridPosition = new long[]{ - 1000 / blockSize[0], 1000 / blockSize[1], 500 / blockSize[2], 0, 0 - }; - double[] minMax = readBlockMinMax(reader, "0", blockGridPosition); - if (minMax != null) { - System.out.println("\\nBlock at grid position " + Arrays.toString(blockGridPosition) + ":"); - System.out.println("Data min: " + minMax[0] + ", max: " + minMax[1]); - } - - // Use imglib2 to read an arbitrary interval (similar to Python slice [0,0,500:600,1000:1100,1000:1100]) - // In Java XYZCT order: x=[1000,1100), y=[1000,1100), z=[500,600), c=0, t=0 - long[] intervalMin = new long[]{1000, 1000, 500, 0, 0}; - long[] intervalMax = new long[]{1099, 1099, 599, 0, 0}; - double[] intervalMinMax = readIntervalMinMax(reader, "0", intervalMin, intervalMax); - if (intervalMinMax != null) { - System.out.println("\\nInterval [" + Arrays.toString(intervalMin) + " - " + Arrays.toString(intervalMax) + "]:"); - System.out.println("Data min: " + intervalMinMax[0] + ", max: " + intervalMinMax[1]); - } - } - - reader.close(); - - } catch (Exception e) { - System.err.println("Error reading OME-ZARR: " + e.getMessage()); - e.printStackTrace(); - } - } - - /** - * Read the .zattrs file from the Zarr root to get OME-ZARR metadata. - */ - private static JsonObject readZattrs(N5URI baseUrl) { - try { - N5URI zattrsUri = baseUrl.resolve(".zattrs"); - URL url = zattrsUri.getURI().toURL(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { - String content = reader.lines().collect(Collectors.joining("\\n")); - return GSON.fromJson(content, JsonObject.class); - } - } catch (Exception e) { - System.err.println("Could not read .zattrs: " + e.getMessage()); - return null; - } - } - - /** - * Read a data block and compute its min and max values. - */ - private static double[] readBlockMinMax(N5Reader reader, String dataset, long[] gridPosition) { - try { - DataBlock block = reader.readBlock(dataset, reader.getDatasetAttributes(dataset), gridPosition); - if (block == null) { - System.err.println("Block not found at position " + Arrays.toString(gridPosition)); - return null; - } + public static void main(String[] args) throws Exception { + N5URI n5URI = new N5URI(URL); + N5Factory n5Factory = new N5Factory().cacheAttributes(true); + N5Reader reader = n5Factory.openReader(StorageFormat.N5, n5URI.getURI()); + + String dataset = "s0"; + DatasetAttributes attrs = reader.getDatasetAttributes(dataset); + System.out.println("Shape: " + Arrays.toString(attrs.getDimensions())); + System.out.println("Dtype: " + attrs.getDataType()); + System.out.println("Block size: " + Arrays.toString(attrs.getBlockSize())); + + // Read a block and print voxel values + long[] blockPosition = new long[attrs.getBlockSize().length]; + DataBlock block = reader.readBlock( + dataset, attrs, blockPosition); + if (block != null) { Object data = block.getData(); - double min = Double.MAX_VALUE; - double max = Double.MIN_VALUE; - if (data instanceof short[]) { - short[] arr = (short[]) data; - for (short v : arr) { - int unsigned = v & 0xFFFF; - if (unsigned < min) min = unsigned; - if (unsigned > max) max = unsigned; - } - } else if (data instanceof int[]) { - int[] arr = (int[]) data; - for (int v : arr) { - if (v < min) min = v; - if (v > max) max = v; - } + System.out.println("Voxels: " + Arrays.toString((short[]) data)); } else if (data instanceof float[]) { - float[] arr = (float[]) data; - for (float v : arr) { - if (v < min) min = v; - if (v > max) max = v; - } - } else if (data instanceof double[]) { - double[] arr = (double[]) data; - for (double v : arr) { - if (v < min) min = v; - if (v > max) max = v; - } + System.out.println("Voxels: " + Arrays.toString((float[]) data)); } else if (data instanceof byte[]) { - byte[] arr = (byte[]) data; - for (byte v : arr) { - int unsigned = v & 0xFF; - if (unsigned < min) min = unsigned; - if (unsigned > max) max = unsigned; - } - } else if (data instanceof long[]) { - long[] arr = (long[]) data; - for (long v : arr) { - if (v < min) min = v; - if (v > max) max = v; - } - } else { - System.err.println("Unsupported data type: " + data.getClass().getSimpleName()); - return null; + System.out.println("Voxels: " + Arrays.toString((byte[]) data)); } - - return new double[]{min, max}; - } catch (Exception e) { - System.err.println("Error reading block: " + e.getMessage()); - return null; } - } - - /** - * Read an arbitrary interval using imglib2 and compute min/max values. - */ - private static & RealType> double[] readIntervalMinMax( - N5Reader reader, String dataset, long[] intervalMin, long[] intervalMax) { - try { - RandomAccessibleInterval img = N5Utils.open(reader, dataset); - RandomAccessibleInterval interval = Views.interval(img, intervalMin, intervalMax); - - double min = Double.MAX_VALUE; - double max = -Double.MAX_VALUE; - - for (T pixel : Views.flatIterable(interval)) { - double val = pixel.getRealDouble(); - if (val < min) min = val; - if (val > max) max = val; - } - return new double[]{min, max}; - } catch (Exception e) { - System.err.println("Error reading interval: " + e.getMessage()); - return null; - } + reader.close(); } -} - `} +}`} copyLabel="Copy code" copyable={true} - language="python" + language="java" /> ) }, @@ -477,22 +455,18 @@ public class ReadOmeZarr { ]} /> ) - }, - { - id: 'vvdViewer', - label: 'VVDViewer', - content: ( - - ) } ]; +} + +export default function DataLinkUsageDialog({ + dataLinkUrl, + dataType, + open, + onClose +}: DataLinkUsageDialogProps) { + const tabs = getTabsForDataType(dataType, dataLinkUrl); + const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? ''); const TAB_TRIGGER_CLASSES = '!text-foreground h-full'; const PANEL_CLASSES = From 4076fc3f462b4552eb045bce675af8dcca7630ac Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 13 Feb 2026 15:48:41 +0000 Subject: [PATCH 23/32] feat: pass dataType="zarr" to DataLinkUsageDialog in ZarrPreview --- frontend/src/components/ui/BrowsePage/ZarrPreview.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx index deba746d..fd7ea452 100644 --- a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx +++ b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx @@ -105,6 +105,7 @@ export default function ZarrPreview({ {closeDialog => ( From b355075fc249a7b1abc9c3f992537f50e241c790 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 13 Feb 2026 15:48:46 +0000 Subject: [PATCH 24/32] feat: pass dataType to DataLinkUsageDialog in PropertiesDrawer --- .../ui/PropertiesDrawer/PropertiesDrawer.tsx | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index 8e53f8ab..872100bf 100644 --- a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -22,7 +22,9 @@ import OverviewTable from '@/components/ui/PropertiesDrawer/OverviewTable'; import TicketDetails from '@/components/ui/PropertiesDrawer/TicketDetails'; import FgTooltip from '@/components/ui/widgets/FgTooltip'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; -import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import DataLinkUsageDialog, { + inferDataType +} from '@/components/ui/Dialogs/DataLinkUsageDialog'; import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import { getPreferredPathForDisplay } from '@/utils'; import { copyToClipboard } from '@/utils/copyText'; @@ -282,19 +284,19 @@ export default function PropertiesDrawer({ : 'Creating a data link allows you to share the data at this path with internal collaborators or use tools to view the data.'} {!externalDataUrlQuery.data && - !proxiedPathByFspAndPathQuery.data && ( - - - Learn more about data links - - - - )} + !proxiedPathByFspAndPathQuery.data ? ( + + + Learn more about data links + + + + ) : null} {externalDataUrlQuery.data ? ( <> @@ -309,6 +311,9 @@ export default function PropertiesDrawer({ {closeDialog => ( @@ -330,6 +335,9 @@ export default function PropertiesDrawer({ dataLinkUrl={ proxiedPathByFspAndPathQuery.data?.url ?? '' } + dataType={inferDataType( + fileBrowserState.propertiesTarget?.path ?? '' + )} onClose={closeDialog} open={true} /> From 985016a7cd0ce345e9b6ddd49e59ae8e304b2bfd Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 13 Feb 2026 15:48:53 +0000 Subject: [PATCH 25/32] feat: add DataLinkUsageDialog to N5Preview with dataType="n5" --- .../components/ui/BrowsePage/N5Preview.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/ui/BrowsePage/N5Preview.tsx b/frontend/src/components/ui/BrowsePage/N5Preview.tsx index ec1f5c55..8ab8e9e1 100644 --- a/frontend/src/components/ui/BrowsePage/N5Preview.tsx +++ b/frontend/src/components/ui/BrowsePage/N5Preview.tsx @@ -3,6 +3,8 @@ import type { UseQueryResult } from '@tanstack/react-query'; import N5MetadataTable from '@/components/ui/BrowsePage/N5MetadataTable'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import DataToolLinks from './DataToolLinks'; import type { N5Metadata, N5OpenWithToolUrls } from '@/queries/n5Queries'; import useDataToolLinks from '@/hooks/useDataToolLinks'; @@ -54,12 +56,26 @@ export default function N5Preview({ {openWithToolUrls ? ( - + <> + + {openWithToolUrls.copy ? ( + + {closeDialog => ( + + )} + + ) : null} + ) : null} {showDataLinkDialog ? ( From 268476c0c4a5e971fb7427c4981b4242d3ac7dba Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 13 Feb 2026 15:48:57 +0000 Subject: [PATCH 26/32] fix: add missing dataType prop to DataLinkUsageDialog in linksColumns --- .../src/components/ui/Table/TableCard.tsx | 1 - .../src/components/ui/Table/linksColumns.tsx | 20 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/ui/Table/TableCard.tsx b/frontend/src/components/ui/Table/TableCard.tsx index a367645a..f3c31fba 100644 --- a/frontend/src/components/ui/Table/TableCard.tsx +++ b/frontend/src/components/ui/Table/TableCard.tsx @@ -62,7 +62,6 @@ declare module '@tanstack/react-table' { data: CellContextMenuData ) => void; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { // Optional function to extract searchable values from a cell // Used by globalFilterFn to allow columns to define custom search behavior diff --git a/frontend/src/components/ui/Table/linksColumns.tsx b/frontend/src/components/ui/Table/linksColumns.tsx index 62b11a89..238cd1ec 100644 --- a/frontend/src/components/ui/Table/linksColumns.tsx +++ b/frontend/src/components/ui/Table/linksColumns.tsx @@ -4,7 +4,9 @@ import { Typography } from '@material-tailwind/react'; import type { ColumnDef } from '@tanstack/react-table'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; -import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import DataLinkUsageDialog, { + inferDataType +} from '@/components/ui/Dialogs/DataLinkUsageDialog'; import DataLinksActionsMenu from '@/components/ui/Menus/DataLinksActions'; import { usePreferencesContext } from '@/contexts/PreferencesContext'; import { useZoneAndFspMapContext } from '@/contexts/ZonesAndFspMapContext'; @@ -187,6 +189,7 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { {showDataLinkUsageDialog ? ( setShowDataLinkUsageDialog(false)} open={showDataLinkUsageDialog} /> @@ -263,12 +266,15 @@ export function useLinksColumns(): ColumnDef[] { enableSorting: true, meta: { // Allow searching by URL and all path formats (linux, mac, windows) - getSearchableValues: (value: PathCellValue, row: ProxiedPath) => [ - row.url, - value.pathMap.mac_path, - value.pathMap.linux_path, - value.pathMap.windows_path - ] + getSearchableValues: (value: unknown, row: ProxiedPath) => { + const pathValue = value as PathCellValue; + return [ + row.url, + pathValue.pathMap.mac_path, + pathValue.pathMap.linux_path, + pathValue.pathMap.windows_path + ]; + } } }, { From fa74db6784920910e1c851524b9a1a582b201739 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 13 Feb 2026 18:58:23 +0000 Subject: [PATCH 27/32] refactor: alphabetize tabs, add napari instructions, and fix code block styling --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 325 +++++++++++------- 1 file changed, 194 insertions(+), 131 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 51500ee7..9a8ee9dc 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Typography, Tabs } from '@material-tailwind/react'; -import { HiOutlineClipboardCopy } from 'react-icons/hi'; +import { HiExternalLink, HiOutlineClipboardCopy } from 'react-icons/hi'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { materialDark, @@ -47,17 +47,17 @@ function CodeBlock({ copyLabel = 'Copy code', customStyle = { margin: 0, - marginTop: 0, - marginRight: 0, - marginBottom: 0, - marginLeft: 0, paddingTop: '3em', - paddingRight: '1em', + paddingRight: '3em', paddingBottom: '0', paddingLeft: '1em', fontSize: '14px', lineHeight: '1.5', - width: 'max-content' + width: '100%', + boxSizing: 'border-box' as const, + whiteSpace: 'pre-wrap' as const, + wordBreak: 'break-word' as const, + overflowX: 'hidden' as const } }: CodeBlockProps) { const isDarkMode = useDarkMode(); @@ -68,12 +68,14 @@ function CodeBlock({ const mergedCodeTagProps = { style: { ...themeCodeStyles, - paddingBottom: '1em' + paddingBottom: '1em', + whiteSpace: 'pre-wrap' as const, + wordBreak: 'break-word' as const } }; return ( -
+
void; }; -function getTabsForDataType(dataType: DataLinkType, dataLinkUrl: string) { - if (dataType === 'directory') { - return [ - { - id: 'python', - label: 'Python', - content: ( - <> - +function getNapariZarrTab(dataLinkUrl: string) { + return { + id: 'napari', + label: 'Napari', + content: ( +
    +
  1. + + 1 + + +
  2. +
  3. + + 2 + +
    + + Install the napari-ome-zarr plugin. Assuming you are in the conda + environment, from the command line: + + +
    +
  4. +
  5. + + 3 + +
    + + Launch napari and open the data link using the napari-ome-zarr + plugin. From the command line: + + + Or in Python: + - - ) - }, +
    +
  6. +
+ ) + }; +} + +function getFijiTab() { + return { + id: 'fiji', + label: 'Fiji', + content: ( + + ) + }; +} + +function getTabsForDataType(dataType: DataLinkType, dataLinkUrl: string) { + if (dataType === 'directory') { + return [ { id: 'java', label: 'Java', @@ -224,57 +289,49 @@ public class ListFiles { language="java" /> ) - } - ]; - } - - if (dataType === 'zarr') { - return [ - { - id: 'napari', - label: 'Napari', - content: ( - - ) }, { id: 'python', label: 'Python', content: ( <> - + ) - }, + } + ]; + } + + if (dataType === 'zarr') { + return [ + getFijiTab(), { id: 'java', label: 'Java', @@ -327,69 +384,45 @@ public class ReadZarr { /> ) }, + getNapariZarrTab(dataLinkUrl), { - id: 'fiji', - label: 'Fiji', + id: 'python', + label: 'Python', content: ( - - ) - } - ]; - } - - // dataType === 'n5' - return [ - { - id: 'napari', - label: 'Napari', - content: ( - - ) - }, - { - id: 'python', - label: 'Python', - content: ( - <> - - + + - - ) - }, + copyLabel="Copy code" + copyable={true} + language="python" + /> + + ) + } + ]; + } + + // dataType === 'n5' + return [ + getFijiTab(), { id: 'java', label: 'Java', @@ -443,18 +476,48 @@ public class ReadN5 { ) }, { - id: 'fiji', - label: 'Fiji', + id: 'napari', + label: 'Napari', content: ( ) + }, + { + id: 'python', + label: 'Python', + content: ( + <> + + + + ) } ]; } @@ -470,12 +533,12 @@ export default function DataLinkUsageDialog({ const TAB_TRIGGER_CLASSES = '!text-foreground h-full'; const PANEL_CLASSES = - 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light overflow-auto'; + 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light overflow-y-auto overflow-x-hidden'; return ( - +
-
+
How to use your data link @@ -488,7 +551,7 @@ export default function DataLinkUsageDialog({
Date: Fri, 13 Feb 2026 19:44:33 +0000 Subject: [PATCH 28/32] style: widen dialog, increase step spacing, and expand tab max height --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 9a8ee9dc..de4af4d2 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -45,22 +45,28 @@ function CodeBlock({ wrapLongLines = true, copyable = false, copyLabel = 'Copy code', - customStyle = { - margin: 0, - paddingTop: '3em', - paddingRight: '3em', - paddingBottom: '0', - paddingLeft: '1em', + customStyle +}: CodeBlockProps) { + const isDarkMode = useDarkMode(); + + const defaultStyle = { + marginTop: '0px', + marginBottom: '0px', + padding: '3em 3em 1em 1em', fontSize: '14px', lineHeight: '1.5', width: '100%', boxSizing: 'border-box' as const, whiteSpace: 'pre-wrap' as const, wordBreak: 'break-word' as const, - overflowX: 'hidden' as const - } -}: CodeBlockProps) { - const isDarkMode = useDarkMode(); + overflowX: 'hidden' as const, + borderRadius: '0.5rem', + background: isDarkMode ? '#2f2f2f' : '#fdfdfd' + }; + + const mergedCustomStyle = customStyle + ? { ...defaultStyle, ...customStyle } + : defaultStyle; // Get the theme's code styles and merge with custom codeTagProps const theme = isDarkMode ? materialDark : coy; @@ -68,17 +74,17 @@ function CodeBlock({ const mergedCodeTagProps = { style: { ...themeCodeStyles, - paddingBottom: '1em', + paddingBottom: '1.5em', whiteSpace: 'pre-wrap' as const, wordBreak: 'break-word' as const } }; return ( -
+
+
    {steps.map((step, index) => (
  1. @@ -133,7 +139,7 @@ function getNapariZarrTab(dataLinkUrl: string) { id: 'napari', label: 'Napari', content: ( -
      +
      1. 1 @@ -533,11 +539,15 @@ export default function DataLinkUsageDialog({ const TAB_TRIGGER_CLASSES = '!text-foreground h-full'; const PANEL_CLASSES = - 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light overflow-y-auto overflow-x-hidden'; + 'flex flex-col gap-4 max-w-full max-h-[65vh] p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light overflow-y-auto overflow-x-hidden'; return ( - -
        + +
        How to use your data link @@ -551,7 +561,7 @@ export default function DataLinkUsageDialog({
        Date: Tue, 17 Feb 2026 12:25:02 -0500 Subject: [PATCH 29/32] style: redesign data tool links and "more way to open" to circular icons --- .../ui/BrowsePage/DataToolLinks.tsx | 247 ++++++++++-------- .../components/ui/BrowsePage/N5Preview.tsx | 31 +-- .../components/ui/BrowsePage/ZarrPreview.tsx | 31 +-- 3 files changed, 156 insertions(+), 153 deletions(-) diff --git a/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx b/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx index b4e34d0c..8063f5fe 100644 --- a/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx +++ b/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx @@ -1,155 +1,184 @@ -import { Button, ButtonGroup, Typography } from '@material-tailwind/react'; +import { Typography } from '@material-tailwind/react'; import { Link } from 'react-router'; +import { HiOutlineClipboardCopy } from 'react-icons/hi'; +import { HiOutlineEllipsisHorizontalCircle } from 'react-icons/hi2'; import neuroglancer_logo from '@/assets/neuroglancer.png'; import validator_logo from '@/assets/ome-ngff-validator.png'; import volE_logo from '@/assets/aics_website-3d-cell-viewer.png'; import avivator_logo from '@/assets/vizarr_logo.png'; -import copy_logo from '@/assets/copy-link-64.png'; import type { OpenWithToolUrls, PendingToolKey } from '@/hooks/useZarrMetadata'; import FgTooltip from '@/components/ui/widgets/FgTooltip'; +import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import type { DataLinkType } from '@/components/ui/Dialogs/DataLinkUsageDialog'; + +const CIRCLE_CLASSES = + 'rounded-full bg-surface-light dark:bg-surface-light hover:bg-surface dark:hover:bg-surface w-12 h-12 flex items-center justify-center cursor-pointer transform active:scale-90 transition-all duration-75'; + +const LABEL_CLASSES = 'text-xs text-center text-foreground mt-1'; export default function DataToolLinks({ + compact = false, + dataLinkUrl, + dataType, onToolClick, showCopiedTooltip, title, urls }: { + readonly compact?: boolean; + readonly dataLinkUrl?: string; + readonly dataType?: DataLinkType; readonly onToolClick: (toolKey: PendingToolKey) => Promise; readonly showCopiedTooltip: boolean; readonly title: string; readonly urls: OpenWithToolUrls | null; }) { - const tooltipTriggerClasses = - 'rounded-sm m-0 p-0 transform active:scale-90 transition-transform duration-75'; - if (!urls) { return null; } return (
        - + {title} - +
        {urls.neuroglancer !== null ? ( - - { - e.preventDefault(); - await onToolClick('neuroglancer'); - }} - rel="noopener noreferrer" - target="_blank" - to={urls.neuroglancer} +
        + - Neuroglancer logo - - + { + e.preventDefault(); + await onToolClick('neuroglancer'); + }} + rel="noopener noreferrer" + target="_blank" + to={urls.neuroglancer} + > + Neuroglancer logo + + + Neuroglancer +
        ) : null} {urls.vole !== null ? ( - - { - e.preventDefault(); - await onToolClick('vole'); - }} - rel="noopener noreferrer" - target="_blank" - to={urls.vole} - > - Vol-E logo - - +
        + + { + e.preventDefault(); + await onToolClick('vole'); + }} + rel="noopener noreferrer" + target="_blank" + to={urls.vole} + > + Vol-E logo + + + Vol-E +
        ) : null} {urls.avivator !== null ? ( - - { - e.preventDefault(); - await onToolClick('avivator'); - }} - rel="noopener noreferrer" - target="_blank" - to={urls.avivator} - > - Avivator logo - - +
        + + { + e.preventDefault(); + await onToolClick('avivator'); + }} + rel="noopener noreferrer" + target="_blank" + to={urls.avivator} + > + Avivator logo + + + Avivator +
        ) : null} {urls.validator !== null ? ( +
        + + { + e.preventDefault(); + await onToolClick('validator'); + }} + rel="noopener noreferrer" + target="_blank" + to={urls.validator} + > + OME-Zarr Validator logo + + + Validator +
        + ) : null} + +
        { + await onToolClick('copy'); + }} + openCondition={showCopiedTooltip ? true : undefined} + triggerClasses={CIRCLE_CLASSES} > - { - e.preventDefault(); - await onToolClick('validator'); - }} - rel="noopener noreferrer" - target="_blank" - to={urls.validator} - > - OME-Zarr Validator logo - + - ) : null} + Copy +
        - { - await onToolClick('copy'); - }} - openCondition={showCopiedTooltip ? true : undefined} - triggerClasses={tooltipTriggerClasses} - variant="ghost" - > - Copy URL icon - - + {dataLinkUrl && dataType ? ( +
        + + {closeDialog => ( + + )} + + More... +
        + ) : null} +
        ); } diff --git a/frontend/src/components/ui/BrowsePage/N5Preview.tsx b/frontend/src/components/ui/BrowsePage/N5Preview.tsx index 8ab8e9e1..9e598ca8 100644 --- a/frontend/src/components/ui/BrowsePage/N5Preview.tsx +++ b/frontend/src/components/ui/BrowsePage/N5Preview.tsx @@ -3,8 +3,6 @@ import type { UseQueryResult } from '@tanstack/react-query'; import N5MetadataTable from '@/components/ui/BrowsePage/N5MetadataTable'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; -import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; -import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import DataToolLinks from './DataToolLinks'; import type { N5Metadata, N5OpenWithToolUrls } from '@/queries/n5Queries'; import useDataToolLinks from '@/hooks/useDataToolLinks'; @@ -56,26 +54,15 @@ export default function N5Preview({ {openWithToolUrls ? ( - <> - - {openWithToolUrls.copy ? ( - - {closeDialog => ( - - )} - - ) : null} - + ) : null} {showDataLinkDialog ? ( diff --git a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx index fd7ea452..0d952418 100644 --- a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx +++ b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx @@ -5,8 +5,6 @@ import type { UseQueryResult } from '@tanstack/react-query'; import zarrLogo from '@/assets/zarr.jpg'; import ZarrMetadataTable from '@/components/ui/BrowsePage/ZarrMetadataTable'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; -import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; -import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import DataToolLinks from './DataToolLinks'; import type { OpenWithToolUrls, @@ -93,26 +91,15 @@ export default function ZarrPreview({
        {openWithToolUrls ? ( - <> - - {openWithToolUrls.copy ? ( - - {closeDialog => ( - - )} - - ) : null} - + ) : null} {showDataLinkDialog ? ( From 17867bfc7486b4b64bf3d6cdad9b2dfd77da07a9 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 17 Feb 2026 12:26:37 -0500 Subject: [PATCH 30/32] style: make copy data url more obvious in data link usage dialog --- .../components/ui/Dialogs/DataLinkUsageDialog.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index de4af4d2..a998a723 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -548,16 +548,19 @@ export default function DataLinkUsageDialog({ open={open} >
        -
        - - How to use your data link - + + How to use your data link + +
        + + {dataLinkUrl} + - +
        Date: Tue, 17 Feb 2026 12:27:08 -0500 Subject: [PATCH 31/32] fix: improve data mode and code block styles in DataLinkUsageDialog --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index a998a723..8f37a70a 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -49,9 +49,11 @@ function CodeBlock({ }: CodeBlockProps) { const isDarkMode = useDarkMode(); + // Note: margin and marginBottom need to be defined separately because the coy theme in react-syntax-highlighter defines both. + // If we only set margin, the coy theme's marginBottom value will override ours and cause extra space at the bottom of the code block. const defaultStyle = { - marginTop: '0px', - marginBottom: '0px', + margin: '0 0', + marginBottom: '0', padding: '3em 3em 1em 1em', fontSize: '14px', lineHeight: '1.5', @@ -81,13 +83,13 @@ function CodeBlock({ }; return ( -
        +
        @@ -225,8 +227,8 @@ function getFijiTab() { steps={[ 'Launch Fiji', 'Navigate to Plugins \u2192 BigDataViewer \u2192 HDF5/N5/Zarr/OME-NGFF Viewer', - 'Paste data link and click "Detect datasets"', - 'Select the multiscale image and click "OK"' + 'Paste your data link into the text input area located at the top of the "Main" tab of the resulting dialog. Then click "Detect datasets"', + 'In the text area under where you pasted the data link, you should now see the image file name, followed by "multiscale". Click on this entry, then click "OK"' ]} /> ) @@ -373,11 +375,14 @@ public class ReadZarr { if (block != null) { Object data = block.getData(); if (data instanceof short[]) { - System.out.println("Voxels: " + Arrays.toString((short[]) data)); + short[] arr = (short[]) data; + System.out.println("First 10 voxels: " + Arrays.toString(Arrays.copyOf(arr, Math.min(10, arr.length)))); } else if (data instanceof float[]) { - System.out.println("Voxels: " + Arrays.toString((float[]) data)); + float[] arr = (float[]) data; + System.out.println("First 10 voxels: " + Arrays.toString(Arrays.copyOf(arr, Math.min(10, arr.length)))); } else if (data instanceof byte[]) { - System.out.println("Voxels: " + Arrays.toString((byte[]) data)); + byte[] arr = (byte[]) data; + System.out.println("First 10 voxels: " + Arrays.toString(Arrays.copyOf(arr, Math.min(10, arr.length)))); } } @@ -464,11 +469,14 @@ public class ReadN5 { if (block != null) { Object data = block.getData(); if (data instanceof short[]) { - System.out.println("Voxels: " + Arrays.toString((short[]) data)); + short[] arr = (short[]) data; + System.out.println("First 10 voxels: " + Arrays.toString(Arrays.copyOf(arr, Math.min(10, arr.length)))); } else if (data instanceof float[]) { - System.out.println("Voxels: " + Arrays.toString((float[]) data)); + float[] arr = (float[]) data; + System.out.println("First 10 voxels: " + Arrays.toString(Arrays.copyOf(arr, Math.min(10, arr.length)))); } else if (data instanceof byte[]) { - System.out.println("Voxels: " + Arrays.toString((byte[]) data)); + byte[] arr = (byte[]) data; + System.out.println("First 10 voxels: " + Arrays.toString(Arrays.copyOf(arr, Math.min(10, arr.length)))); } } @@ -539,11 +547,11 @@ export default function DataLinkUsageDialog({ const TAB_TRIGGER_CLASSES = '!text-foreground h-full'; const PANEL_CLASSES = - 'flex flex-col gap-4 max-w-full max-h-[65vh] p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light overflow-y-auto overflow-x-hidden'; + 'flex flex-col gap-4 max-w-full max-h-[65vh] p-4 rounded-b-lg border border-t-0 border-surface dark:border-foreground/30 bg-surface-light dark:bg-surface overflow-y-auto overflow-x-hidden'; return ( @@ -569,7 +577,7 @@ export default function DataLinkUsageDialog({ onValueChange={setActiveTab} value={activeTab} > - + {tabs.map(tab => ( Date: Tue, 17 Feb 2026 12:46:29 -0500 Subject: [PATCH 32/32] fix: add button role back to data tool link copy icon --- frontend/src/components/ui/BrowsePage/DataToolLinks.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx b/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx index 8063f5fe..87e4cbb4 100644 --- a/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx +++ b/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx @@ -147,6 +147,7 @@ export default function DataToolLinks({
        { await onToolClick('copy');