Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test": "vitest"
},
"dependencies": {
"@bioimagetools/capability-manifest": "^0.1.0",
"@material-tailwind/react": "^3.0.0-beta.24",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
Expand Down
156 changes: 121 additions & 35 deletions frontend/src/hooks/useZarrMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,61 @@ import {
} from '@/omezarr-helper';
import { buildUrl } from '@/utils';
import * as zarr from 'zarrita';
import {
getCompatibleViewers,
type OmeZarrMetadata
} from '@bioimagetools/capability-manifest';

/**
* Convert Fileglancer's internal Metadata to the library's OmeZarrMetadata format
* Only converts if this is a valid OME-Zarr dataset (has version)
*/
function convertToOmeZarrMetadata(
metadata: ZarrMetadata
): OmeZarrMetadata | null {
if (!metadata?.multiscale) {
return null;
}

// If version is null/undefined, this is not a proper OME-Zarr dataset
// Return null to fall back to legacy logic
const version = metadata.multiscale.version;
if (!version) {
return null;
}

// Convert axes from multiscale to the expected format, ensuring no null values
const axes =
metadata.multiscale.axes?.map((axis: any) => ({
name: axis.name,
type: axis.type ?? undefined,
unit: axis.unit ?? undefined
})) || [];

// Build the OmeZarrMetadata object
const omeZarrMetadata: OmeZarrMetadata = {
version: version as '0.4' | '0.5',
axes,
multiscales: [
{
...metadata.multiscale,
version,
axes
}
]
};

// Add optional fields if they exist
if (metadata.omero) {
omeZarrMetadata.omero = metadata.omero;
}

if (metadata.labels && metadata.labels.length > 0) {
omeZarrMetadata.labels = metadata.labels;
}

return omeZarrMetadata;
}

export type { OpenWithToolUrls, ZarrMetadata };
export type PendingToolKey = keyof OpenWithToolUrls | null;
Expand Down Expand Up @@ -102,62 +157,93 @@ export default function useZarrMetadata() {
copy: url || ''
} as OpenWithToolUrls;

// Determine which tools should be available based on metadata type
if (metadata?.multiscale) {
// OME-Zarr - all urls for v2; no avivator for v3
// Convert metadata to OmeZarrMetadata format and get compatible viewers
const omeZarrMetadata = convertToOmeZarrMetadata(metadata);
let compatibleViewers: string[] = [];

if (omeZarrMetadata) {
try {
compatibleViewers = getCompatibleViewers(omeZarrMetadata);
log.debug('Compatible viewers from library:', compatibleViewers);
} catch (error) {
log.error('Error getting compatible viewers:', error);
// Fall back to assuming it's OME-Zarr if we have multiscale
compatibleViewers = metadata?.multiscale ? ['Neuroglancer'] : [];
}
}

// Determine which tools should be available based on compatible viewers
const isOmeZarr = omeZarrMetadata !== null;

if (isOmeZarr) {
// OME-Zarr dataset
const hasNeuroglancer = compatibleViewers.includes('Neuroglancer');
const hasAvivator =
compatibleViewers.includes('Vizarr') ||
compatibleViewers.includes('Avivator');

if (url) {
if (effectiveZarrVersion === 2) {
// Avivator/Vizarr - only for v2 and if compatible
if (effectiveZarrVersion === 2 && hasAvivator) {
openWithToolUrls.avivator = buildUrl(avivatorBaseUrl, null, {
image_url: url
});
} else {
openWithToolUrls.avivator = null;
}
// Populate with actual URLs when proxied path is available

// Validator - always available for OME-Zarr
openWithToolUrls.validator = buildUrl(validatorBaseUrl, null, {
source: url
});

// Vol-E - keep as-is for now (not in library)
openWithToolUrls.vole = buildUrl(voleBaseUrl, null, {
url
});
if (disableNeuroglancerStateGeneration) {
openWithToolUrls.neuroglancer =
neuroglancerBaseUrl +
generateNeuroglancerStateForDataURL(url, effectiveZarrVersion);
} else if (layerType) {
try {
openWithToolUrls.neuroglancer =
neuroglancerBaseUrl +
generateNeuroglancerStateForOmeZarr(
url,
effectiveZarrVersion,
layerType,
metadata.multiscale,
metadata.arr,
metadata.labels,
metadata.omero,
useLegacyMultichannelApproach
);
} catch (error) {
log.error(
'Error generating Neuroglancer state for OME-Zarr:',
error
);

// Neuroglancer - if compatible
if (hasNeuroglancer) {
if (disableNeuroglancerStateGeneration) {
openWithToolUrls.neuroglancer =
neuroglancerBaseUrl +
generateNeuroglancerStateForDataURL(url, effectiveZarrVersion);
} else if (layerType && metadata.multiscale) {
try {
openWithToolUrls.neuroglancer =
neuroglancerBaseUrl +
generateNeuroglancerStateForOmeZarr(
url,
effectiveZarrVersion,
layerType,
metadata.multiscale,
metadata.arr,
metadata.labels,
metadata.omero,
useLegacyMultichannelApproach
);
} catch (error) {
log.error(
'Error generating Neuroglancer state for OME-Zarr:',
error
);
openWithToolUrls.neuroglancer =
neuroglancerBaseUrl +
generateNeuroglancerStateForDataURL(url, effectiveZarrVersion);
}
} else {
openWithToolUrls.neuroglancer = '';
}
} else {
openWithToolUrls.neuroglancer = '';
}
} else {
// No proxied URL - show all tools as available but empty
// No proxied URL - show compatible tools as available but empty
openWithToolUrls.validator = '';
openWithToolUrls.vole = '';
// if this is a zarr version 2, then set the url to blank which will show
// the icon before a data link has been generated. Setting it to null for
// all other versions, eg zarr v3 means the icon will not be present before
// a data link is generated.
openWithToolUrls.avivator = effectiveZarrVersion === 2 ? '' : null;
openWithToolUrls.neuroglancer = '';
openWithToolUrls.avivator =
effectiveZarrVersion === 2 && hasAvivator ? '' : null;
openWithToolUrls.neuroglancer = hasNeuroglancer ? '' : '';
}
} else {
// Non-OME Zarr - only Neuroglancer available
Expand Down
32 changes: 25 additions & 7 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query';
import { initializeViewerManifests } from '@bioimagetools/capability-manifest';

import './index.css';
import App from './App';
import { queryClient } from './queryClient';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);
// Expose initialization function for tests
// This allows Playwright tests to call initializeViewerManifests via page.evaluate()
// since it needs to run in the browser context (uses fetch API), not Node.js context
declare global {
interface Window {
initializeViewerManifests?: typeof initializeViewerManifests;
}
}

window.initializeViewerManifests = initializeViewerManifests;

const startApp = async () => {
await initializeViewerManifests();

createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);
};

void startApp();
13 changes: 13 additions & 0 deletions frontend/ui-tests/fixtures/fileglancer-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ const openFileglancer = async (page: Page) => {
// Wait for the app to be ready
await page.waitForSelector('text=Log In', { timeout: 10000 });

// Initialize viewer manifests before login
// We call this via page.evaluate() because initializeViewerManifests uses browser APIs
// (fetch) and must run in the browser context, not the Node.js context where Playwright runs
await page.evaluate(async () => {
// @ts-ignore - accessing window-scoped function from main.tsx
if (window.initializeViewerManifests) {
await window.initializeViewerManifests();
console.log('[Test] Viewer manifests initialized');
} else {
console.warn('[Test] initializeViewerManifests not found on window');
}
});

// Perform login
const loginForm = page.getByRole('textbox', { name: 'Username' });
const loginSubmitBtn = page.getByRole('button', { name: 'Log In' });
Expand Down