diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0dad2c5..d7bd37d 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -25,7 +25,7 @@ jobs: path: dist/ test: - name: Test (${{ matrix.jbrowse-version }}) + name: Test production builds needs: build-and-lint runs-on: ubuntu-latest strategy: diff --git a/config.json b/config.json index 8ccba33..4a3c192 100644 --- a/config.json +++ b/config.json @@ -117,5 +117,6 @@ } ] } - ] + ], + "aggregateTextSearchAdapters": [] } diff --git a/eslint.config.mjs b/eslint.config.mjs index 130ea74..42d1a9b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,6 +15,7 @@ export default defineConfig( 'esbuild-watch.mjs', 'eslint.config.mjs', 'ucsc/*', + '.test*', '.test-jbrowse/*', ], }, diff --git a/package.json b/package.json index ecae472..2dc82ac 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build": "tsc && NODE_ENV=production node esbuild.mjs && cp distconfig.json dist/config.json", "prebuild": "yarn clean", "lint": "eslint --report-unused-disable-directives --max-warnings 0", - "pretest": "test -d .test-jbrowse || npx @jbrowse/cli create .test-jbrowse --nightly", + "pretest": "rm -rf .test-jbrowse && npx @jbrowse/cli create .test-jbrowse --nightly", "test": "vitest run", "test:watch": "vitest", "test:setup": "node scripts/test-versions.mjs setup", @@ -31,8 +31,9 @@ "dependencies": { "@emotion/styled": "^11.14.1", "g2p_mapper": "^2.0.0", - "pako": "^2.1.0", - "react-msaview": "^5.0.3", + "idb": "^8.0.3", + "pako-esm2": "^2.0.0", + "react-msaview": "^5.0.5", "swr": "^2.3.8" }, "devDependencies": { @@ -58,7 +59,6 @@ "eslint-plugin-unicorn": "^62.0.0", "mobx": "^6.15.0", "mobx-react": "^9.2.1", - "msa-parsers": "^5.0.3", "prettier": "^3.7.4", "pretty-bytes": "^7.1.0", "puppeteer": "^24.34.0", diff --git a/src/AddHighlightModel/GenomeMouseoverHighlight.tsx b/src/AddHighlightModel/GenomeMouseoverHighlight.tsx index 5963897..ccac8df 100644 --- a/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +++ b/src/AddHighlightModel/GenomeMouseoverHighlight.tsx @@ -12,35 +12,51 @@ const GenomeMouseoverHighlight = observer(function ({ }: { model: LinearGenomeViewModel }) { - const { hovered } = getSession(model) - return hovered && - typeof hovered === 'object' && - 'hoverPosition' in hovered ? ( - - ) : null + const session = getSession(model) + const { hovered, views } = session + + // Early return if no MSA view exists + const hasMsaView = views.some(s => s.type === 'MsaView') + if (!hasMsaView) { + return null + } + + // Early return if no hover position + if ( + !hovered || + typeof hovered !== 'object' || + !('hoverPosition' in hovered) + ) { + return null + } + + return }) -const GenomeMouseoverHighlightPostNullCheck = observer(function ({ +const GenomeMouseoverHighlightRenderer = observer(function ({ model, + hovered, }: { model: LinearGenomeViewModel + + hovered: any }) { const { classes } = useStyles() - const session = getSession(model) - if (session.views.some(s => s.type === 'MsaView')) { - const { hovered } = session - const { offsetPx } = model - // @ts-expect-error - const { coord, refName } = hovered.hoverPosition - - const s = model.bpToPx({ refName, coord: coord - 1 }) - const e = model.bpToPx({ refName, coord: coord }) - if (s && e) { - const width = Math.max(Math.abs(e.offsetPx - s.offsetPx), 4) - const left = Math.min(s.offsetPx, e.offsetPx) - offsetPx - return
- } + const { offsetPx } = model + const { coord, refName } = hovered.hoverPosition as { + coord: number + refName: string + } + + const s = model.bpToPx({ refName, coord: coord - 1 }) + const e = model.bpToPx({ refName, coord: coord }) + + if (s && e) { + const width = Math.max(Math.abs(e.offsetPx - s.offsetPx), 4) + const left = Math.min(s.offsetPx, e.offsetPx) - offsetPx + return
} + return null }) diff --git a/src/AddHighlightModel/MsaToGenomeHighlight.tsx b/src/AddHighlightModel/MsaToGenomeHighlight.tsx index bd58dda..5766508 100644 --- a/src/AddHighlightModel/MsaToGenomeHighlight.tsx +++ b/src/AddHighlightModel/MsaToGenomeHighlight.tsx @@ -1,53 +1,74 @@ import React from 'react' -import { Assembly } from '@jbrowse/core/assemblyManager/assembly' import { getSession } from '@jbrowse/core/util' import { observer } from 'mobx-react' import { useStyles } from './util' -import { JBrowsePluginMsaViewModel } from '../MsaViewPanel/model' +import type { JBrowsePluginMsaViewModel } from '../MsaViewPanel/model' import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' type LGV = LinearGenomeViewModel -function getCanonicalName(assembly: Assembly, s: string) { - return assembly.getCanonicalRefName(s) ?? s -} - +// Outer component: only re-renders when MSA view or highlights change const MsaToGenomeHighlight = observer(function MsaToGenomeHighlight2({ model, }: { model: LGV }) { - const { classes } = useStyles() - const { assemblyManager, views } = getSession(model) - const p = views.find(f => f.type === 'MsaView') as + const { views } = getSession(model) + const msaView = views.find(f => f.type === 'MsaView') as | JBrowsePluginMsaViewModel | undefined + + const highlights = msaView?.connectedHighlights + + // Early return if no highlights - avoid all other work + if (!highlights || highlights.length === 0) { + return null + } + + return +}) + +// Inner component: handles the scroll-dependent rendering +const MsaToGenomeHighlightRenderer = observer(function ({ + model, + highlights, +}: { + model: LGV + highlights: { refName: string; start: number; end: number }[] +}) { + const { classes } = useStyles() + const { assemblyManager } = getSession(model) const assembly = assemblyManager.get(model.assemblyNames[0]!) - return assembly ? ( + const { offsetPx } = model + + if (!assembly) { + return null + } + + return ( <> - {p?.connectedHighlights.map((r, idx) => { - const refName = getCanonicalName(assembly, r.refName) + {highlights.map((r, idx) => { + const refName = assembly.getCanonicalRefName(r.refName) ?? r.refName const s = model.bpToPx({ refName, coord: r.start }) const e = model.bpToPx({ refName, coord: r.end }) if (s && e) { const width = Math.max(Math.abs(e.offsetPx - s.offsetPx), 4) - const left = Math.min(s.offsetPx, e.offsetPx) - model.offsetPx + const left = Math.min(s.offsetPx, e.offsetPx) - offsetPx return (
) - } else { - return null } + return null })} - ) : null + ) }) export default MsaToGenomeHighlight diff --git a/src/AddHighlightModel/index.tsx b/src/AddHighlightModel/index.tsx index bebdc33..d0fb398 100644 --- a/src/AddHighlightModel/index.tsx +++ b/src/AddHighlightModel/index.tsx @@ -1,6 +1,7 @@ import React from 'react' import PluginManager from '@jbrowse/core/PluginManager' +import { getSession } from '@jbrowse/core/util' import HighlightComponents from './HighlightComponents' @@ -13,6 +14,13 @@ export default function AddHighlightComponentsModelF( 'LinearGenomeView-TracksContainerComponent', // @ts-expect-error (rest: React.ReactNode[], { model }: { model: LinearGenomeViewModel }) => { + // Quick check: don't add any components if no MSA view exists + const { views } = getSession(model) + const hasMsaView = views.some(v => v.type === 'MsaView') + if (!hasMsaView) { + return rest + } + return [ ...rest, 0 + + const [value, setValue] = useState('ncbi_blast') - const handleChange = (_event: React.SyntheticEvent, newValue: number) => { + const handleChange = (_event: React.SyntheticEvent, newValue: string) => { setValue(newValue) } return ( - - - - + + {hasPreloadedDatasets ? ( + + ) : null} + + - + - - - - + {hasPreloadedDatasets ? ( + + + + ) : null} + - + void + feature: Feature +}) { + const [results, setResults] = useState([]) + const [loading, setLoading] = useState(true) + const view = getContainingView(model) as LinearGenomeViewModel + + const geneId = feature.get('id') + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + ;(async () => { + try { + const cached = await getAllCachedResults() + setResults(cached.filter(r => r.geneId === geneId)) + setLoading(false) + } catch (e) { + console.error(e) + } + })() + }, [geneId]) + + const handleDelete = async (id: string) => { + await deleteCachedResult(id) + setResults(r => r.filter(result => result.id !== id)) + } + + const handleClearAll = async () => { + await clearAllCachedResults() + setResults([]) + } + + const handleUseCached = (cached: CachedBlastResult) => { + blastLaunchViewFromCache({ + view, + cached, + newViewTitle: `BLAST - ${cached.geneId ?? cached.transcriptId ?? 'Unknown gene'}`, + }) + handleClose() + } + + if (loading) { + return Loading cached results... + } + + if (results.length === 0) { + return ( + + No cached BLAST results found for this gene. Run a BLAST query to cache + results. + + ) + } + + return ( +
+
+ + Cached BLAST Results ({results.length}) + + +
+ + {results.map(result => ( + { + e.stopPropagation() + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleDelete(result.id) + }} + > + + + } + > + { + handleUseCached(result) + }} + > + + + + ))} + +
+ ) +}) + +export default CachedBlastResults diff --git a/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx b/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx index 378f7d6..91fa9ea 100644 --- a/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx +++ b/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx @@ -6,7 +6,11 @@ import { Feature, getContainingView, } from '@jbrowse/core/util' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { + Accordion, + AccordionDetails, + AccordionSummary, Button, DialogActions, DialogContent, @@ -16,8 +20,10 @@ import { import { observer } from 'mobx-react' import { makeStyles } from 'tss-react/mui' +import CachedBlastResults from './CachedBlastResults' import { blastLaunchView } from './blastLaunchView' import TextField2 from '../../../components/TextField2' +import { getAllCachedResults } from '../../../utils/blastCache' import { getGeneDisplayName, getTranscriptDisplayName } from '../../util' import TranscriptSelector from '../TranscriptSelector' import { useTranscriptSelection } from '../useTranscriptSelection' @@ -63,6 +69,23 @@ const NCBIBlastAutomaticPanel = observer(function ({ useState('clustalo') const [selectedBlastProgram, setSelectedBlastProgram] = useState('quick-blastp') + const [hasCachedResults, setHasCachedResults] = useState(false) + const [error, setError] = useState() + + const geneId = feature.get('id') + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + ;(async () => { + try { + const results = await getAllCachedResults() + setHasCachedResults(results.some(r => r.geneId === geneId)) + } catch (e) { + console.error(e) + setError(e) + } + })() + }, [geneId]) + const { options, setSelectedId, @@ -76,7 +99,7 @@ const NCBIBlastAutomaticPanel = observer(function ({ setSelectedBlastProgram('blastp') } }, [selectedBlastDatabase]) - const e = proteinSequenceError ?? launchViewError + const e = proteinSequenceError ?? launchViewError ?? error const style = { width: 150 } return ( <> @@ -172,6 +195,21 @@ const NCBIBlastAutomaticPanel = observer(function ({ you need a COBALT alignment, please use the manual approach of submitting BLAST yourself and downloading the resulting files + + {hasCachedResults ? ( + + }> + Previous BLAST Results + + + + + + ) : null}