Skip to content
Draft
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
1 change: 1 addition & 0 deletions app/apis/catalog/brc-analytics-catalog/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface BRCDataCatalogGenome {
gcPercent: number | null;
geneModelUrl: string | null;
isRef: string;
jbrowseConfigUrl: string | null;
length: number;
level: string;
lineageTaxonomyIds: string[];
Expand Down
1 change: 1 addition & 0 deletions app/apis/catalog/ga2/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface GA2AssemblyEntity {
gcPercent: number | null;
geneModelUrl: string | null;
isRef: "No" | "Yes";
jbrowseConfigUrl: string | null;
length: number;
level: string;
lineageTaxonomyIds: string[];
Expand Down
88 changes: 88 additions & 0 deletions app/components/Entity/components/JBrowse/jbrowse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useEffect, useState, useMemo } from "react";
import { Alert, Skeleton, Stack, ThemeProvider } from "@mui/material";
import {
createViewState,
JBrowseLinearGenomeView,
} from "@jbrowse/react-linear-genome-view2";
import { createJBrowseTheme } from "@jbrowse/core/ui/theme";
import {
convertToLinearGenomeViewConfig,
getConfigErrorMessage,
isValidConfigUrl,
loadJBrowseConfig,
} from "./utils";
import { JBrowseProps } from "./types";
import { mergeAppTheme } from "app/theme/theme";

/**
* JBrowse genome browser component with isolated theme.
* Uses JBrowse's own theme wrapped in a ThemeProvider to ensure all
* required MUI palette colors are present for JBrowse components.
*
* @param props - Component props
* @param props.accession - Assembly accession ID
* @param props.configUrl - URL to JBrowse configuration JSON
* @returns Rendered JBrowse Linear Genome View component
*/
export const JBrowse = ({
accession,
configUrl,
}: JBrowseProps): JSX.Element => {
const [viewState, setViewState] = useState<unknown>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

// Create and memoize JBrowse theme to override parent theme
const jbrowseTheme = useMemo(() => createJBrowseTheme(), []);

useEffect(() => {
async function initializeJBrowse(): Promise<void> {
setLoading(true);
setError(null);
try {
if (!isValidConfigUrl(configUrl)) {
throw new Error("Invalid JBrowse configuration URL");
}

const rawConfig = await loadJBrowseConfig(configUrl);
const config = convertToLinearGenomeViewConfig(rawConfig);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JBrowse config type is complex and external
const state = createViewState(config as any);
setViewState(state);
} catch (err) {
const message = getConfigErrorMessage(
accession,
err instanceof Error ? err : undefined
);
setError(message);
console.error(message, err);
} finally {
setLoading(false);
}
}

initializeJBrowse();
}, [accession, configUrl]);

if (loading) {
return (
<Stack gap={2}>
<Skeleton height={48} variant="rectangular" />
<Skeleton height={400} variant="rectangular" />
</Stack>
);
}

if (error || !viewState) {
return <Alert severity="error">{error || "Failed to initialize"}</Alert>;
}

return (
<ThemeProvider
theme={(outerTheme) => mergeAppTheme(outerTheme, jbrowseTheme)}
>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- JBrowse viewState type is complex and external */}
<JBrowseLinearGenomeView viewState={viewState as any} />
</ThemeProvider>
);
};
82 changes: 82 additions & 0 deletions app/components/Entity/components/JBrowse/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* JBrowse component types.
*/

/**
* Props for the JBrowse component.
*/
export interface JBrowseProps {
/**
* Assembly accession identifier (e.g., GCA_000002825.3).
*/
accession: string;
/**
* URL or path to the JBrowse 2 configuration file.
* Can be a local path (e.g., /jbrowse-config/GCA_000002825.3/config.json)
* or a remote URL (e.g., https://example.com/jbrowse/config.json).
*/
configUrl: string;
}

/**
* JBrowse view state interface.
*/
export interface JBrowseViewState {
/**
* URL to the assembly FASTA file.
*/
assembly: {
/**
* Assembly name.
*/
name: string;
/**
* Path to sequence adapter configuration.
*/
sequence: {
adapter: {
faiLocation: {
uri: string;
};
fastaLocation: {
uri: string;
};
type: string;
};
type: string;
};
};
/**
* Default session configuration.
*/
defaultSession?: {
name: string;
view: {
id: string;
tracks: Array<{
configuration: string;
displays: Array<{
configuration: string;
type: string;
}>;
type: string;
}>;
type: string;
};
};
/**
* Tracks configuration.
*/
tracks: Array<{
adapter: {
gffLocation?: {
uri: string;
};
type: string;
};
assemblyNames: string[];
name: string;
trackId: string;
type: string;
}>;
}
124 changes: 124 additions & 0 deletions app/components/Entity/components/JBrowse/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Utility functions for JBrowse component.
*/

/**
* Check if a URL is a remote URL (http/https) or local path.
* @param url - URL or path to check.
* @returns True if the URL is remote (starts with http:// or https://), false otherwise.
*/
export function isRemoteUrl(url: string): boolean {
return url.startsWith("http://") || url.startsWith("https://");
}

/**
* Validate that a JBrowse config URL is properly formed.
* @param url - URL or path to validate.
* @returns True if URL is valid, false otherwise.
*/
export function isValidConfigUrl(url: string): boolean {
if (!url || typeof url !== "string") {
return false;
}

// Check if it's a remote URL
if (isRemoteUrl(url)) {
try {
new URL(url);
return true;
} catch {
return false;
}
}

// Check if it's a local path (should start with /)
return url.startsWith("/");
}

/**
* Load JBrowse configuration from a URL or local path.
* @param configUrl - URL or path to the JBrowse config file.
* @returns Promise resolving to the configuration object.
*/
export async function loadJBrowseConfig(
configUrl: string
): Promise<Record<string, unknown>> {
try {
const response = await fetch(configUrl);

if (!response.ok) {
throw new Error(
`Failed to load JBrowse config: ${response.status} ${response.statusText}`
);
}

const config = await response.json();
return config;
} catch (error) {
console.error("Error loading JBrowse config:", error);
throw error;
}
}

/**
* Generate a default error message for missing or invalid config.
* @param accession - Assembly accession.
* @param error - Optional error object.
* @returns Error message string.
*/
export function getConfigErrorMessage(
accession: string,
error?: Error
): string {
if (error) {
return `Failed to load JBrowse configuration for assembly ${accession}: ${error.message}`;
}
return `JBrowse configuration not available for assembly ${accession}`;
}

/**
* Convert full JBrowse config to React Linear Genome View format.
* Handles both formats: full JBrowse config and React LGV config.
* @param config - JBrowse configuration object.
* @returns Configuration in React Linear Genome View format.
*/
export function convertToLinearGenomeViewConfig(
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Config structure varies
config: any
): Record<string, unknown> {
// If it's already in React LGV format (has singular 'assembly'), return as-is
if (config.assembly && config.defaultSession?.view) {
return config;
}

// If it's a full JBrowse config (has plural 'assemblies'), convert it
if (config.assemblies && Array.isArray(config.assemblies)) {
const firstAssembly = config.assemblies[0];
const firstView = config.defaultSession?.views?.[0];

if (!firstAssembly) {
throw new Error("Config has no assemblies");
}

return {
assembly: firstAssembly,
configuration: config.configuration,
defaultSession: {
name: config.defaultSession?.name || "Default Session",
view: firstView
? {
id: "linearGenomeView",
type: "LinearGenomeView",
...firstView.init,
}
: {
id: "linearGenomeView",
type: "LinearGenomeView",
},
},
tracks: config.tracks || [],
};
}

throw new Error("Invalid JBrowse configuration format");
}
12 changes: 12 additions & 0 deletions app/components/Layout/components/Main/main.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ export const StyledPagesMain = styled(DXMain)`
background-color: ${PALETTE.COMMON_WHITE};
flex-direction: column;
`;

/**
* Full-width main container for browser pages.
* Removes max-width constraints and padding to allow full viewport usage.
*/
export const StyledBrowserMain = styled(DXMain)`
background-color: ${PALETTE.COMMON_WHITE};
flex-direction: column;
max-width: 100% !important;
padding: 0 !important;
width: 100% !important;
`;
70 changes: 70 additions & 0 deletions app/views/BrowserView/browserView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Fragment } from "react";
import { Alert, Box, Stack, Typography } from "@mui/material";
import { BRCDataCatalogGenome } from "../../apis/catalog/brc-analytics-catalog/common/entities";
import dynamic from "next/dynamic";
import { GA2AssemblyEntity } from "app/apis/catalog/ga2/entities";

// Load JBrowse only on client-side to avoid SSR issues
const JBrowse = dynamic(
() =>
import("../../components/Entity/components/JBrowse/jbrowse").then(
(mod) => mod.JBrowse
),
{ ssr: false }
);

/**
* Props interface for the BrowserView component
* @interface BrowserViewProps
* @property {BRCDataCatalogGenome | GA2AssemblyEntity} assembly - The assembly object containing genome or assembly information
*/
export interface BrowserViewProps {
assembly: BRCDataCatalogGenome | GA2AssemblyEntity;
}

/**
* Browser view component for displaying JBrowse genome browser.
* @param props - Browser view props.
* @param props.assembly - Assembly entity with genome data.
* @returns Browser view JSX.
*/
export const BrowserView = ({ assembly }: BrowserViewProps): JSX.Element => {
// Check if assembly has JBrowse config
if (!assembly.jbrowseConfigUrl) {
return (
<Box sx={{ padding: 4 }}>
<Alert severity="info">
Genome browser is not available for this assembly.
</Alert>
</Box>
);
}

return (
<Fragment>
<Stack gap={2} sx={{ height: "100%", width: "100%" }}>
{/* JBrowse browser */}
<JBrowse
accession={assembly.accession}
configUrl={assembly.jbrowseConfigUrl}
/>

{/* Additional assembly info */}
{assembly.ucscBrowserUrl && (
<Box sx={{ paddingBottom: 2, paddingX: 2 }}>
<Typography variant="body2" color="text.secondary">
Alternative browsers:{" "}
<a
href={assembly.ucscBrowserUrl}
target="_blank"
rel="noopener noreferrer"
>
UCSC Genome Browser
</a>
</Typography>
</Box>
)}
</Stack>
</Fragment>
);
};
Loading
Loading