Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f4e4cb2
add useIndividualViewableExperimentResults hook
noctillion Oct 30, 2025
438dd21
centralize drs fetches in ExplorerIndividualContent
noctillion Oct 30, 2025
90cd292
rm child DRS dispatches
noctillion Oct 30, 2025
ad29462
rm child drs dispatches
noctillion Oct 30, 2025
83b30e3
lint
noctillion Oct 30, 2025
dbffca6
lint
noctillion Oct 30, 2025
0cb3de3
rm unused dispatch/imports post-DRS
noctillion Oct 30, 2025
440fdf0
rm unused code
noctillion Oct 31, 2025
796bce8
move and export file format constants
noctillion Nov 27, 2025
ccf5d1a
rf to deduplicate by id
noctillion Nov 27, 2025
10a1902
rm local definitions of file format
noctillion Nov 27, 2025
a0bab4b
rf allExperimentResults
noctillion Nov 27, 2025
4350358
lint
noctillion Nov 27, 2025
8ef71bf
refacto for isViewableInIgv
noctillion Nov 28, 2025
d6aac5c
lint
noctillion Nov 28, 2025
186a77c
refactor unify DRS fetch actions into single retrieveDrsUrls flow
noctillion Jan 22, 2026
a0c85fa
refactor IGV and download URL state into urlsByFilename
noctillion Jan 22, 2026
452211e
implement unified DRS action and remove redundant hooks
noctillion Jan 22, 2026
6b3ccc3
update selector to access new urlsByFilename state
noctillion Jan 22, 2026
cc3c497
update IGV selector to use unified urlsByFilename state
noctillion Jan 22, 2026
a530349
remove unused hook
noctillion Jan 22, 2026
0d0f8bb
Merge branch 'master' of github.com:bento-platform/bento_web into ref…
noctillion Jan 22, 2026
01ded59
refactor useffect for early return
noctillion Feb 2, 2026
c71d062
fix defaut array for fuzzySearchResponse
noctillion Feb 2, 2026
1aa33de
refactor dispatch for drs urls search
noctillion Feb 2, 2026
e311854
add null check for allExperimentResults
noctillion Feb 3, 2026
3b737a0
Merge branch 'master' into refact/drs_individual_overview
davidlougheed Feb 5, 2026
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
25 changes: 25 additions & 0 deletions src/components/explorer/ExplorerIndividualContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import IndividualInterpretations from "./IndividualInterpretations";
import IndividualMedicalActions from "./IndividualMedicalActions";
import IndividualMeasurements from "./IndividualMeasurements";

import { useAppDispatch } from "@/store";
import { retrieveDrsUrls } from "@/modules/drs/actions";
import { guessFileType } from "@/utils/files";

const MENU_STYLE = {
marginLeft: "-24px",
marginRight: "-24px",
Expand Down Expand Up @@ -62,6 +66,27 @@ const ExplorerIndividualContent = () => {
const resourcesTuple = useIndividualResources(individual);
const individualContext = useMemo(() => ({ individualID, resourcesTuple }), [individualID, resourcesTuple]);

const dispatch = useAppDispatch();

const biosamplesData = useDeduplicatedIndividualBiosamples(individual);

const allExperimentResults = useMemo(() => {
const rawResults = biosamplesData.flatMap((b) =>
(b?.experiments ?? []).flatMap((e) => e?.experiment_results ?? []),
);
return Object.values(Object.fromEntries(rawResults.map((r) => [r.id, r])));
}, [biosamplesData]);

useEffect(() => {
if (allExperimentResults.length > 0) {
const downloadableFiles = allExperimentResults.map((r) => ({
...r,
file_format: r.file_format ?? guessFileType(r.filename),
}));
dispatch(retrieveDrsUrls(downloadableFiles)).catch(console.error);
}
}, [dispatch, allExperimentResults]);

const individualUrl = explorerIndividualUrl(individualID);

const overviewPath = "overview";
Expand Down
24 changes: 3 additions & 21 deletions src/components/explorer/IndividualExperiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import {
individualPropTypesShape,
ontologyShape,
} from "@/propTypes";
import { getFileDownloadUrlsFromDrs } from "@/modules/drs/actions";
import { useAppDispatch, useAppSelector } from "@/store";
import { guessFileType } from "@/utils/files";
import { useAppSelector } from "@/store";

import { useDeduplicatedIndividualBiosamples } from "./utils";
import { VIEWABLE_FILE_EXTENSIONS } from "@/components/display/FileDisplay";
Expand All @@ -37,8 +35,8 @@ const VIEWABLE_FILE_FORMATS = ["PDF", "CSV", "TSV"];
const ExperimentResultActions = ({ result }) => {
const { filename } = result;

const downloadUrls = useAppSelector((state) => state.drs.downloadUrlsByFilename);
const url = downloadUrls[filename]?.url;
const drsUrls = useAppSelector((state) => state.drs.urlsByFilename);
const url = drsUrls[filename]?.url;

const [viewModalVisible, setViewModalVisible] = useState(false);

Expand Down Expand Up @@ -305,8 +303,6 @@ const EXPERIMENT_COLUMNS = [
];

const Experiments = ({ individual, handleExperimentClick }) => {
const dispatch = useAppDispatch();

const { selectedExperiment } = useParams();

useEffect(() => {
Expand All @@ -325,20 +321,6 @@ const Experiments = ({ individual, handleExperimentClick }) => {
const biosamplesData = useDeduplicatedIndividualBiosamples(individual);
const experimentsData = useMemo(() => biosamplesData.flatMap((b) => b?.experiments ?? []), [biosamplesData]);

useEffect(() => {
// retrieve any download urls if experiments data changes

const downloadableFiles = experimentsData
.flatMap((e) => e?.experiment_results ?? [])
.map((r) => ({
// enforce file_format property
...r,
file_format: r.file_format ?? guessFileType(r.filename),
}));

dispatch(getFileDownloadUrlsFromDrs(downloadableFiles)).catch(console.error);
}, [dispatch, experimentsData]);

return (
<RoutedIndividualContentTable
data={experimentsData}
Expand Down
59 changes: 9 additions & 50 deletions src/components/explorer/IndividualTracks.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ import { SettingOutlined } from "@ant-design/icons";

import { BENTO_PUBLIC_URL, BENTO_URL } from "@/config";
import { individualPropTypesShape } from "@/propTypes";
import { getIgvUrlsFromDrs } from "@/modules/drs/actions";
import { setIgvPosition } from "@/modules/explorer/actions";
import { useIgvGenomes } from "@/modules/explorer/hooks";
import { useReferenceGenomes } from "@/modules/reference/hooks";
import { useService } from "@/modules/services/hooks";
import { useAppDispatch, useAppSelector } from "@/store";
import { guessFileType } from "@/utils/files";
import { simpleDeepCopy } from "@/utils/misc";

import { useDeduplicatedIndividualBiosamples } from "./utils";
import {
useDeduplicatedIndividualBiosamples,
ALIGNMENT_FORMATS_LOWER,
expResFileFormatLower,
isViewableInIgv,
expResFileFormatToIgvTypeAndFormat,
} from "./utils";

const SQUISHED_CALL_HEIGHT = 10;
const EXPANDED_CALL_HEIGHT = 50;
Expand Down Expand Up @@ -51,39 +55,6 @@ const DEBOUNCE_WAIT = 500;
// verify url set is for this individual (may have stale urls from previous request)
const hasFreshUrls = (files, urls) => files.every((f) => urls.hasOwnProperty(f.filename));

const ALIGNMENT_FORMATS_LOWER = ["bam", "cram"];
const ANNOTATION_FORMATS_LOWER = ["bigbed"]; // TODO: experiment result: support more
const MUTATION_FORMATS_LOWER = ["maf"];
const WIG_FORMATS_LOWER = ["bigwig"]; // TODO: experiment result: support wig/bedGraph?
const VARIANT_FORMATS_LOWER = ["vcf", "gvcf"];
const VIEWABLE_FORMATS_LOWER = [
...ALIGNMENT_FORMATS_LOWER,
...ANNOTATION_FORMATS_LOWER,
...MUTATION_FORMATS_LOWER,
...WIG_FORMATS_LOWER,
...VARIANT_FORMATS_LOWER,
];

const expResFileFormatLower = (expRes) => expRes.file_format?.toLowerCase() ?? guessFileType(expRes.filename);

// For an experiment result to be viewable in IGV.js, it must have:
// - an assembly ID, so we can contextualize it correctly
// - a file format in the list of file formats we know how to handle
const isViewableInIgv = (expRes) =>
!!expRes.genome_assembly_id && VIEWABLE_FORMATS_LOWER.includes(expResFileFormatLower(expRes));

const expResFileFormatToIgvTypeAndFormat = (fileFormat) => {
const ff = fileFormat.toLowerCase();

if (ALIGNMENT_FORMATS_LOWER.includes(ff)) return ["alignment", ff];
if (ANNOTATION_FORMATS_LOWER.includes(ff)) return ["annotation", "bigBed"]; // TODO: expand if we support more
if (MUTATION_FORMATS_LOWER.includes(ff)) return ["mut", ff];
if (WIG_FORMATS_LOWER.includes(ff)) return ["wig", "bigWig"]; // TODO: expand if we support wig/bedGraph
if (VARIANT_FORMATS_LOWER.includes(ff)) return ["variant", "vcf"];

return [undefined, undefined];
};

const TrackControlTable = memo(({ toggleView, allFoundFiles }) => {
const trackTableColumns = [
{
Expand Down Expand Up @@ -151,20 +122,19 @@ const IGV_JS_ANNOTATION_ALIASES = {
const IndividualTracks = ({ individual }) => {
const accessToken = useAccessToken();

const dispatch = useAppDispatch();
const igvDivRef = useRef();
const igvBrowserRef = useRef(null);
const [creatingIgvBrowser, setCreatingIgvBrowser] = useState(false);

const { igvUrlsByFilename: igvUrls, isFetchingIgvUrls } = useAppSelector((state) => state.drs);
const { urlsByFilename: igvUrls, isFetchingUrls: isFetchingIgvUrls } = useAppSelector((state) => state.drs);

// read stored position only on first render
const { igvPosition } = useAppSelector(
(state) => state.explorer,
() => true, // We don't want to re-render anything when the position changes
);

const dispatch = useAppDispatch();

const referenceService = useService("reference");
// Built-in igv.js genomes (with annotations):
const { hasAttempted: igvGenomesAttempted, itemsByID: igvGenomesByID } = useIgvGenomes();
Expand Down Expand Up @@ -293,17 +263,6 @@ const IndividualTracks = ({ individual }) => {
[dispatch],
);

// retrieve urls on mount
useEffect(() => {
if (allTracks.length) {
// don't search if all urls already known
if (hasFreshUrls(allTracks, igvUrls)) {
return;
}
dispatch(getIgvUrlsFromDrs(allTracks)).catch(console.error);
}
}, [dispatch, allTracks, igvUrls]);

// update access token whenever necessary
useEffect(() => {
if (BENTO_URL) {
Expand Down
40 changes: 40 additions & 0 deletions src/components/explorer/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
import { useEffect, useMemo } from "react";
import { fetchDatasetResourcesIfNecessary } from "@/modules/datasets/actions";
import { useAppDispatch, useAppSelector } from "@/store";
import { guessFileType } from "@/utils/files";

// --- CONSTANTS ---
export const ALIGNMENT_FORMATS_LOWER = ["bam", "cram"];
export const ANNOTATION_FORMATS_LOWER = ["bigbed"];
export const MUTATION_FORMATS_LOWER = ["maf"];
export const WIG_FORMATS_LOWER = ["bigwig"];
export const VARIANT_FORMATS_LOWER = ["vcf", "gvcf"];

export const IGV_VIEWABLE_FORMATS_LOWER = [
...ALIGNMENT_FORMATS_LOWER,
...ANNOTATION_FORMATS_LOWER,
...MUTATION_FORMATS_LOWER,
...WIG_FORMATS_LOWER,
...VARIANT_FORMATS_LOWER,
];

// --- HELPER FUNCTIONS ---

export const expResFileFormatLower = (expRes) => expRes.file_format?.toLowerCase() ?? guessFileType(expRes.filename);

// For an experiment result to be viewable in IGV.js, it must have:
// - an assembly ID, so we can contextualize it correctly
// - a file format in the list of file formats we know how to handle
export const isViewableInIgv = (expRes) =>
!!expRes.genome_assembly_id && IGV_VIEWABLE_FORMATS_LOWER.includes(expResFileFormatLower(expRes));

export const expResFileFormatToIgvTypeAndFormat = (fileFormat) => {
const ff = fileFormat.toLowerCase();

if (ALIGNMENT_FORMATS_LOWER.includes(ff)) return ["alignment", ff];
if (ANNOTATION_FORMATS_LOWER.includes(ff)) return ["annotation", "bigBed"]; // TODO: experiment result: support more
if (MUTATION_FORMATS_LOWER.includes(ff)) return ["mut", ff];
if (WIG_FORMATS_LOWER.includes(ff)) return ["wig", "bigWig"]; // TODO: expand if we support wig/bedGraph
if (VARIANT_FORMATS_LOWER.includes(ff)) return ["variant", "vcf"];

return [undefined, undefined];
};

// --- HOOKS ---

export const useDeduplicatedIndividualBiosamples = (individual) =>
useMemo(
Expand Down
Loading