jbrowse-plugin-msaview 2.8.2 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastRIDPanel.d.ts → BlastQuery/BlastAutomaticPanel.d.ts} +2 -3
  2. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.js → BlastQuery/BlastAutomaticPanel.js} +6 -25
  3. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.d.ts → BlastQuery/BlastManualPanel.d.ts} +2 -3
  4. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.js → BlastQuery/BlastManualPanel.js} +7 -4
  5. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.d.ts → BlastQuery/BlastMethodSelector.d.ts} +2 -2
  6. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.js → BlastQuery/BlastMethodSelector.js} +1 -2
  7. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.d.ts → BlastQuery/BlastPanel.d.ts} +3 -12
  8. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.js → BlastQuery/BlastPanel.js} +14 -16
  9. package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.d.ts +8 -0
  10. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.js → BlastQuery/BlastSettingsDialog.js} +13 -9
  11. package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +12 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +17 -0
  14. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +3 -3
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +15 -35
  16. package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.d.ts +15 -0
  17. package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +67 -0
  18. package/dist/LaunchMsaView/index.js +19 -1
  19. package/dist/MsaViewPanel/components/JobLink.d.ts +5 -0
  20. package/dist/MsaViewPanel/components/JobLink.js +13 -0
  21. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +1 -2
  22. package/dist/MsaViewPanel/components/LoadingBLAST.js +8 -15
  23. package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
  24. package/dist/MsaViewPanel/doLaunchBlast.js +11 -19
  25. package/dist/MsaViewPanel/doLaunchOrthologs.js +43 -12
  26. package/dist/MsaViewPanel/doLaunchOrthologs.test.d.ts +1 -0
  27. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +201 -0
  28. package/dist/MsaViewPanel/model.d.ts +20 -12
  29. package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -33
  30. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  31. package/dist/utils/blastCache.d.ts +8 -4
  32. package/dist/utils/blastCache.js +4 -5
  33. package/dist/utils/ebiBlast.d.ts +52 -0
  34. package/dist/utils/ebiBlast.js +63 -0
  35. package/dist/utils/ebiJobDispatcher.d.ts +33 -0
  36. package/dist/utils/ebiJobDispatcher.js +80 -0
  37. package/dist/utils/ebiJobDispatcher.test.d.ts +1 -0
  38. package/dist/utils/ebiJobDispatcher.test.js +46 -0
  39. package/dist/utils/eutils.d.ts +9 -0
  40. package/dist/utils/eutils.js +18 -0
  41. package/dist/utils/fetch.js +24 -1
  42. package/dist/utils/msa.d.ts +1 -1
  43. package/dist/utils/msa.js +25 -32
  44. package/dist/utils/ncbiOrthologs.d.ts +17 -75
  45. package/dist/utils/ncbiOrthologs.js +67 -63
  46. package/dist/utils/ncbiOrthologs.test.js +82 -2
  47. package/dist/utils/ncbiTaxonomy.d.ts +11 -0
  48. package/dist/utils/ncbiTaxonomy.js +33 -0
  49. package/dist/utils/types.d.ts +9 -14
  50. package/dist/utils/useLocalStorage.d.ts +1 -0
  51. package/dist/utils/useLocalStorage.js +1 -1
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/package.json +3 -3
  55. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx → BlastQuery/BlastAutomaticPanel.tsx} +13 -59
  56. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.tsx → BlastQuery/BlastManualPanel.tsx} +7 -6
  57. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.tsx → BlastQuery/BlastMethodSelector.tsx} +2 -7
  58. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.tsx → BlastQuery/BlastPanel.tsx} +21 -25
  59. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.tsx → BlastQuery/BlastSettingsDialog.tsx} +26 -13
  60. package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.tsx +1 -1
  61. package/src/LaunchMsaView/components/BlastQuery/consts.ts +22 -0
  62. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +5 -7
  63. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +35 -73
  64. package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +92 -0
  65. package/src/LaunchMsaView/index.ts +20 -2
  66. package/src/MsaViewPanel/components/JobLink.tsx +17 -0
  67. package/src/MsaViewPanel/components/LoadingBLAST.tsx +10 -41
  68. package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
  69. package/src/MsaViewPanel/doLaunchBlast.ts +12 -28
  70. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +247 -0
  71. package/src/MsaViewPanel/doLaunchOrthologs.ts +56 -13
  72. package/src/MsaViewPanel/model.ts +10 -7
  73. package/src/utils/blastCache.ts +8 -9
  74. package/src/utils/ebiBlast.ts +114 -0
  75. package/src/utils/ebiJobDispatcher.test.ts +54 -0
  76. package/src/utils/ebiJobDispatcher.ts +120 -0
  77. package/src/utils/eutils.ts +19 -0
  78. package/src/utils/fetch.ts +26 -1
  79. package/src/utils/msa.ts +26 -47
  80. package/src/utils/ncbiOrthologs.test.ts +96 -2
  81. package/src/utils/ncbiOrthologs.ts +83 -71
  82. package/src/utils/ncbiTaxonomy.ts +37 -0
  83. package/src/utils/types.ts +8 -13
  84. package/src/utils/useLocalStorage.ts +1 -1
  85. package/src/version.ts +1 -1
  86. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.d.ts +0 -10
  87. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.js +0 -74
  88. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBISettingsDialog.d.ts +0 -5
  89. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.d.ts +0 -7
  90. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.js +0 -4
  91. package/dist/MsaViewPanel/components/RIDLink.d.ts +0 -6
  92. package/dist/MsaViewPanel/components/RIDLink.js +0 -12
  93. package/dist/utils/ncbiBlast.d.ts +0 -30
  94. package/dist/utils/ncbiBlast.js +0 -84
  95. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.tsx +0 -137
  96. package/src/LaunchMsaView/components/NCBIBlastQuery/consts.ts +0 -10
  97. package/src/MsaViewPanel/components/RIDLink.tsx +0 -19
  98. package/src/utils/ncbiBlast.ts +0 -143
  99. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.d.ts +0 -0
  100. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.d.ts +0 -0
  101. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.js +0 -0
  102. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.d.ts +0 -0
  103. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.js +0 -0
  104. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.d.ts +0 -0
  105. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.js +0 -0
  106. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.tsx +0 -0
  107. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.ts +0 -0
  108. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.ts +0 -0
@@ -4,12 +4,14 @@ import SettingsIcon from '@mui/icons-material/Settings'
4
4
  import { IconButton } from '@mui/material'
5
5
  import { makeStyles } from 'tss-react/mui'
6
6
 
7
- import NCBIBlastAutomaticPanel from './NCBIBlastAutomaticPanel'
8
- import NCBIBlastManualPanel from './NCBIBlastManualPanel'
9
- import NCBIBlastMethodSelector from './NCBIBlastMethodSelector'
10
- import NCBIBlastRIDPanel from './NCBIBlastRIDPanel'
11
- import NCBISettingsDialog from './NCBISettingsDialog'
12
- import { BASE_BLAST_URL } from './consts'
7
+ import BlastAutomaticPanel from './BlastAutomaticPanel'
8
+ import BlastManualPanel from './BlastManualPanel'
9
+ import BlastMethodSelector from './BlastMethodSelector'
10
+ import BlastSettingsDialog from './BlastSettingsDialog'
11
+ import {
12
+ DEFAULT_EBI_EMAIL,
13
+ EBI_EMAIL_STORAGE_KEY,
14
+ } from '../../../utils/ebiJobDispatcher'
13
15
  import { useLocalStorage } from '../../../utils/useLocalStorage'
14
16
 
15
17
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
@@ -21,14 +23,13 @@ const useStyles = makeStyles()({
21
23
  })
22
24
 
23
25
  const panelMap = {
24
- automatic: NCBIBlastAutomaticPanel,
25
- rid: NCBIBlastRIDPanel,
26
- manual: NCBIBlastManualPanel,
26
+ automatic: BlastAutomaticPanel,
27
+ manual: BlastManualPanel,
27
28
  } as const
28
29
 
29
30
  export type BlastLookupMethod = keyof typeof panelMap
30
31
 
31
- export default function NCBIBlastPanel({
32
+ export default function BlastPanel({
32
33
  handleClose,
33
34
  model,
34
35
  feature,
@@ -39,9 +40,9 @@ export default function NCBIBlastPanel({
39
40
  }) {
40
41
  const [lookupMethod, setLookupMethod] =
41
42
  useState<BlastLookupMethod>('automatic')
42
- const [baseUrl, setBaseUrl] = useLocalStorage(
43
- 'msa-blastRootUrl',
44
- BASE_BLAST_URL,
43
+ const [ebiEmail, setEbiEmail] = useLocalStorage(
44
+ EBI_EMAIL_STORAGE_KEY,
45
+ DEFAULT_EBI_EMAIL,
45
46
  )
46
47
  const [settingsOpen, setSettingsOpen] = useState(false)
47
48
  const { classes } = useStyles()
@@ -60,24 +61,19 @@ export default function NCBIBlastPanel({
60
61
  <SettingsIcon />
61
62
  </IconButton>
62
63
 
63
- <Panel
64
- model={model}
65
- feature={feature}
66
- handleClose={handleClose}
67
- baseUrl={baseUrl}
68
- >
69
- <NCBIBlastMethodSelector
64
+ <Panel model={model} feature={feature} handleClose={handleClose}>
65
+ <BlastMethodSelector
70
66
  lookupMethod={lookupMethod}
71
67
  setLookupMethod={setLookupMethod}
72
68
  />
73
69
  </Panel>
74
70
 
75
71
  {settingsOpen ? (
76
- <NCBISettingsDialog
77
- baseUrl={baseUrl}
78
- handleClose={newUrl => {
79
- if (newUrl) {
80
- setBaseUrl(newUrl)
72
+ <BlastSettingsDialog
73
+ ebiEmail={ebiEmail}
74
+ handleClose={settings => {
75
+ if (settings) {
76
+ setEbiEmail(settings.ebiEmail)
81
77
  }
82
78
  setSettingsOpen(false)
83
79
  }}
@@ -6,27 +6,35 @@ import {
6
6
  DialogActions,
7
7
  DialogContent,
8
8
  DialogTitle,
9
+ Typography,
9
10
  } from '@mui/material'
10
11
  import { makeStyles } from 'tss-react/mui'
11
12
 
12
- import { BASE_BLAST_URL } from './consts'
13
13
  import TextField2 from '../../../components/TextField2'
14
+ import { DEFAULT_EBI_EMAIL } from '../../../utils/ebiJobDispatcher'
14
15
 
15
16
  const useStyles = makeStyles()({
16
- urlField: {
17
+ field: {
17
18
  minWidth: 300,
18
19
  },
20
+ help: {
21
+ marginBottom: 8,
22
+ },
19
23
  })
20
24
 
21
- export default function NCBISettingsDialog({
25
+ export interface BlastSettings {
26
+ ebiEmail: string
27
+ }
28
+
29
+ export default function BlastSettingsDialog({
22
30
  handleClose,
23
- baseUrl,
31
+ ebiEmail,
24
32
  }: {
25
- handleClose: (arg?: string) => void
26
- baseUrl: string
33
+ handleClose: (arg?: BlastSettings) => void
34
+ ebiEmail: string
27
35
  }) {
28
36
  const { classes } = useStyles()
29
- const [tempBaseUrl, setTempBaseUrl] = useState(baseUrl)
37
+ const [tempEbiEmail, setTempEbiEmail] = useState(ebiEmail)
30
38
  return (
31
39
  <Dialog
32
40
  open
@@ -37,22 +45,27 @@ export default function NCBISettingsDialog({
37
45
  >
38
46
  <DialogTitle>BLAST Settings</DialogTitle>
39
47
  <DialogContent>
48
+ <Typography variant="subtitle2" className={classes.help}>
49
+ Searches run at EBI, which asks for a contact address on every job so
50
+ they can reach whoever is generating the load. If your site sends real
51
+ volume, use your own.
52
+ </Typography>
40
53
  <TextField2
41
54
  autoFocus
42
55
  margin="dense"
43
- label="BLAST Base URL"
56
+ label="EBI contact email"
44
57
  fullWidth
45
58
  variant="outlined"
46
- value={tempBaseUrl}
47
- className={classes.urlField}
59
+ value={tempEbiEmail}
60
+ className={classes.field}
48
61
  onChange={e => {
49
- setTempBaseUrl(e.target.value)
62
+ setTempEbiEmail(e.target.value)
50
63
  }}
51
64
  />
52
65
  <Button
53
66
  variant="contained"
54
67
  onClick={() => {
55
- setTempBaseUrl(BASE_BLAST_URL)
68
+ setTempEbiEmail(DEFAULT_EBI_EMAIL)
56
69
  }}
57
70
  >
58
71
  Reset
@@ -72,7 +85,7 @@ export default function NCBISettingsDialog({
72
85
  color="primary"
73
86
  variant="contained"
74
87
  onClick={() => {
75
- handleClose(tempBaseUrl)
88
+ handleClose({ ebiEmail: tempEbiEmail })
76
89
  }}
77
90
  >
78
91
  Save
@@ -149,7 +149,7 @@ const CachedBlastResults = observer(function ({
149
149
  }}
150
150
  >
151
151
  <ListItemText
152
- primary={`${getResultDisplayName(result)} - ${result.blastDatabase}/${result.blastProgram} (${result.msaAlgorithm})`}
152
+ primary={`${getResultDisplayName(result)} - ${result.blastDatabase}${result.blastProgram ? `/${result.blastProgram}` : ''} (${result.msaAlgorithm})`}
153
153
  secondary={`${new Date(result.timestamp).toLocaleString()} - Seq: ${result.proteinSequence.slice(0, 30)}...`}
154
154
  />
155
155
  </ListItemButton>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Only used to build the link-out on the manual panel, which sends the user to
3
+ * NCBI's own site to run BLAST there. Nothing fetches this url: NCBI stopped
4
+ * sending Access-Control-Allow-Origin to third-party origins, so a browser
5
+ * cannot read a response from it at all. See docs/blast.md.
6
+ */
7
+ export const BASE_BLAST_URL = 'https://blast.ncbi.nlm.nih.gov/Blast.cgi'
8
+
9
+ export const msaAlgorithms = ['clustalo', 'muscle', 'kalign', 'mafft'] as const
10
+ export type MsaAlgorithm = (typeof msaAlgorithms)[number]
11
+
12
+ export const blastDatabaseOptions = [
13
+ 'uniprotkb_swissprot',
14
+ 'uniprotkb',
15
+ 'uniprotkb_reference_proteomes',
16
+ 'uniprotkb_trembl',
17
+ ] as const
18
+ export type BlastDatabase = (typeof blastDatabaseOptions)[number]
19
+
20
+ // curated, so it returns roughly one good sequence per species rather than the
21
+ // many near-identical TrEMBL entries an alignment reads poorly
22
+ export const defaultBlastDatabase: BlastDatabase = 'uniprotkb_swissprot'
@@ -4,8 +4,8 @@ import { Dialog } from '@jbrowse/core/ui'
4
4
  import { getSession } from '@jbrowse/core/util'
5
5
  import { Tab, Tabs } from '@mui/material'
6
6
 
7
+ import BlastPanel from './BlastQuery/BlastPanel'
7
8
  import ManualMSALoader from './ManualMSALoader/ManualMSALoader'
8
- import NCBIBlastPanel from './NCBIBlastQuery/NCBIBlastPanel'
9
9
  import OrthologPanel from './OrthologQuery/OrthologPanel'
10
10
  import PreLoadedMSA from './PreLoadedMSA/PreLoadedMSADataPanel'
11
11
  import { readMsaDatasets } from './PreLoadedMSA/types'
@@ -41,7 +41,9 @@ export default function LaunchMsaViewDialog({
41
41
  }}
42
42
  >
43
43
  <Tab label="Orthologs (fast)" value="orthologs" />
44
- <Tab label="NCBI BLAST query" value="ncbi_blast" />
44
+ {/* the tab value stays 'ncbi_blast' — it is only local state, and
45
+ renaming it buys nothing */}
46
+ <Tab label="BLAST query" value="ncbi_blast" />
45
47
  {hasPreloadedDatasets ? (
46
48
  <Tab label="Pre-loaded MSA datasets" value="preloaded_msa" />
47
49
  ) : null}
@@ -55,11 +57,7 @@ export default function LaunchMsaViewDialog({
55
57
  />
56
58
  </TabPanel>
57
59
  <TabPanel value={value} index="ncbi_blast">
58
- <NCBIBlastPanel
59
- handleClose={handleClose}
60
- feature={feature}
61
- model={model}
62
- />
60
+ <BlastPanel handleClose={handleClose} feature={feature} model={model} />
63
61
  </TabPanel>
64
62
  {hasPreloadedDatasets ? (
65
63
  <TabPanel value={value} index="preloaded_msa">
@@ -1,48 +1,32 @@
1
1
  import React, { useMemo, useState } from 'react'
2
2
 
3
- import { Checkbox, FormControlLabel, MenuItem, Typography } from '@mui/material'
3
+ import { Typography } from '@mui/material'
4
4
  import { observer } from 'mobx-react'
5
5
  import { makeStyles } from 'tss-react/mui'
6
6
 
7
+ import QuerySpeciesSelect from './QuerySpeciesSelect'
7
8
  import { orthologLaunchView } from './orthologLaunchView'
8
9
  import TextField2 from '../../../components/TextField2'
9
- import { COMMON_SPECIES } from '../../../utils/ncbiOrthologs'
10
+ import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs'
10
11
  import {
11
12
  getGeneDisplayName,
12
13
  getGeneIdentifiers,
13
14
  getLinearGenomeView,
14
15
  getTranscriptDisplayName,
15
16
  } from '../../util'
17
+ import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect'
16
18
  import LaunchPanelContent from '../LaunchPanelContent'
17
- import MsaAlgorithmSelect from '../NCBIBlastQuery/MsaAlgorithmSelect'
18
19
  import SubmitCancelActions from '../SubmitCancelActions'
19
20
  import TranscriptSelector from '../TranscriptSelector'
20
21
  import { useTranscriptSelection } from '../useTranscriptSelection'
21
22
 
22
- import type { MsaAlgorithm } from '../NCBIBlastQuery/consts'
23
+ import type { MsaAlgorithm } from '../BlastQuery/consts'
23
24
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
24
25
 
25
26
  const useStyles = makeStyles()({
26
27
  selectField: {
27
28
  width: 180,
28
29
  },
29
- // A GRID, not a wrapping flex row of fixed-width items. The old form was three
30
- // 160px columns inside a 560px box, which is five rows for thirteen species and
31
- // eight for twenty-three -- and the checkbox list is the tallest thing in the
32
- // dialog, so those rows are the dialog's height. Five auto-fitted columns is
33
- // five rows for twenty-three, i.e. more species in less space, and it reflows
34
- // rather than being pinned to a width the dialog may not have.
35
- speciesBox: {
36
- display: 'grid',
37
- gridTemplateColumns: 'repeat(auto-fit, minmax(130px, 1fr))',
38
- maxWidth: 700,
39
- marginTop: 4,
40
- },
41
- // The label carries the row height; the default control padding is what makes
42
- // 23 rows of it tall.
43
- species: {
44
- marginRight: 0,
45
- },
46
30
  })
47
31
 
48
32
  const OrthologPanel = observer(function ({
@@ -59,86 +43,64 @@ const OrthologPanel = observer(function ({
59
43
  const [launchViewError, setLaunchViewError] = useState<unknown>()
60
44
  const [taxId, setTaxId] = useState(9606)
61
45
  const [msaAlgorithm, setMsaAlgorithm] = useState<MsaAlgorithm>('clustalo')
62
- const [excluded, setExcluded] = useState<number[]>([])
46
+ const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies))
63
47
 
64
48
  const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature])
65
49
  const transcriptSelection = useTranscriptSelection({ feature, view })
66
50
  const { selectedTranscript, proteinSequence } = transcriptSelection
67
51
  const e = transcriptSelection.error ?? launchViewError
68
52
 
69
- const taxa = COMMON_SPECIES.map(s => s.taxId).filter(
70
- t => !excluded.includes(t),
71
- )
53
+ const rowCount = Number(maxSpecies)
54
+ const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2
72
55
 
73
56
  return (
74
57
  <>
75
58
  <LaunchPanelContent error={e}>
76
- {/* One line rather than seven. What a reader needs here is which tab
77
- to pick, and that is the seconds-against-minutes comparison; the
78
- rest (species labels, CDD overlay, the query row being the selected
79
- transcript) is visible in the result or documented, and as prose it
80
- was most of the dialog's height. */}
59
+ {/* One line rather than seven. What a reader needs here is which tab to
60
+ pick; the rest (species labels, CDD overlay, the query row being the
61
+ selected transcript) is visible in the result or documented, and as
62
+ prose it was most of the dialog's height.
63
+
64
+ The comparison stays but the number moved: the lookup is still
65
+ instant, and it is now the aligner that costs the wait, about half a
66
+ second per row. */}
81
67
  <Typography variant="body2">
82
- NCBI&apos;s precomputed orthologs, one gene per species, aligned at EBI
83
- in seconds rather than the 10+ minutes BLAST takes.
68
+ NCBI&apos;s precomputed orthologs, one gene per species, looked up
69
+ rather than searched for. No BLAST job to queue.
84
70
  </Typography>
85
71
 
86
72
  <div>
87
- <TextField2
88
- variant="outlined"
89
- label="Query species"
73
+ <QuerySpeciesSelect
90
74
  className={classes.selectField}
91
- select
92
75
  value={taxId}
93
- onChange={event => {
94
- setTaxId(Number(event.target.value))
95
- }}
96
- helperText="the species this gene is from"
97
- >
98
- {COMMON_SPECIES.map(s => (
99
- <MenuItem value={s.taxId} key={s.taxId}>
100
- {s.label}
101
- </MenuItem>
102
- ))}
103
- </TextField2>
76
+ onChange={setTaxId}
77
+ />
104
78
 
105
79
  <MsaAlgorithmSelect
106
80
  className={classes.selectField}
107
81
  value={msaAlgorithm}
108
82
  onChange={setMsaAlgorithm}
109
83
  />
110
- </div>
111
84
 
112
- <Typography variant="subtitle2" style={{ marginTop: 8 }}>
113
- Species to include (those without an ortholog are skipped)
114
- </Typography>
115
- <div className={classes.speciesBox}>
116
- {COMMON_SPECIES.map(s => (
117
- <FormControlLabel
118
- className={classes.species}
119
- key={s.taxId}
120
- control={
121
- <Checkbox
122
- checked={!excluded.includes(s.taxId)}
123
- onChange={event => {
124
- setExcluded(
125
- event.target.checked
126
- ? excluded.filter(t => t !== s.taxId)
127
- : [...excluded, s.taxId],
128
- )
129
- }}
130
- />
131
- }
132
- label={s.label}
133
- />
134
- ))}
85
+ <TextField2
86
+ variant="outlined"
87
+ label="Rows to align"
88
+ className={classes.selectField}
89
+ type="number"
90
+ value={maxSpecies}
91
+ onChange={event => {
92
+ setMaxSpecies(event.target.value)
93
+ }}
94
+ error={!rowCountValid}
95
+ helperText="the closest N species NCBI has"
96
+ />
135
97
  </div>
136
98
 
137
99
  <TranscriptSelector feature={feature} {...transcriptSelection} />
138
100
 
139
101
  </LaunchPanelContent>
140
102
  <SubmitCancelActions
141
- submitDisabled={!proteinSequence || taxa.length < 2}
103
+ submitDisabled={!proteinSequence || !rowCountValid}
142
104
  onSubmit={() => {
143
105
  try {
144
106
  if (selectedTranscript) {
@@ -149,7 +111,7 @@ const OrthologPanel = observer(function ({
149
111
  newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
150
112
  orthologParams: {
151
113
  taxId,
152
- taxa,
114
+ maxSpecies: rowCount,
153
115
  geneCandidates,
154
116
  msaAlgorithm,
155
117
  selectedTranscript,
@@ -0,0 +1,92 @@
1
+ import React, { useEffect, useState } from 'react'
2
+
3
+ import TextField2 from '../../../components/TextField2'
4
+ import { resolveTaxId } from '../../../utils/ncbiTaxonomy'
5
+ import { fetchTaxonomyInfo } from '../../../utils/taxonomyNames'
6
+
7
+ /**
8
+ * The species the query gene came from, as free text resolved against NCBI's
9
+ * taxonomy rather than picked from a fixed list.
10
+ *
11
+ * The resolved taxon is shown back as helper text, which is the whole point of
12
+ * resolving on a debounce instead of on submit: a typo resolves to some other
13
+ * organism rather than to nothing, and the only place that surfaces is the gene
14
+ * lookup, as "could not resolve NLRP1 in taxon 9986".
15
+ */
16
+ export default function QuerySpeciesSelect({
17
+ value,
18
+ onChange,
19
+ className,
20
+ }: {
21
+ value: number
22
+ onChange: (taxId: number) => void
23
+ className?: string
24
+ }) {
25
+ const [text, setText] = useState('human')
26
+ const [resolved, setResolved] = useState<string>()
27
+ const [error, setError] = useState<unknown>()
28
+
29
+ useEffect(() => {
30
+ // read through a call rather than as a property: the cleanup writes it from
31
+ // another turn of the loop, and a bare `run.live` narrows to true after the
32
+ // first check, which reads to the compiler as a redundant second one
33
+ const run = { live: true }
34
+ const cancelled = () => !run.live
35
+ async function lookup() {
36
+ try {
37
+ setError(undefined)
38
+ const taxId = await resolveTaxId(text)
39
+ if (cancelled()) {
40
+ return
41
+ }
42
+ if (!taxId) {
43
+ setResolved(undefined)
44
+ setError(new Error(`No NCBI taxon matches "${text}"`))
45
+ return
46
+ }
47
+ const info = (await fetchTaxonomyInfo([taxId])).get(taxId)
48
+ if (cancelled()) {
49
+ return
50
+ }
51
+ setResolved(
52
+ [info?.sciname, info?.commonName && `(${info.commonName})`]
53
+ .filter(Boolean)
54
+ .join(' ') || `taxon ${taxId}`,
55
+ )
56
+ onChange(taxId)
57
+ } catch (e) {
58
+ if (!cancelled()) {
59
+ setError(e)
60
+ }
61
+ }
62
+ }
63
+ const timer = setTimeout(() => {
64
+ void lookup()
65
+ }, 400)
66
+ return () => {
67
+ run.live = false
68
+ clearTimeout(timer)
69
+ }
70
+ // onChange is a setState updater from the parent and stable in practice;
71
+ // including it would re-run the lookup on every parent render
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [text])
74
+
75
+ return (
76
+ <TextField2
77
+ variant="outlined"
78
+ label="Query species"
79
+ className={className}
80
+ value={text}
81
+ onChange={event => {
82
+ setText(event.target.value)
83
+ }}
84
+ error={!!error}
85
+ helperText={
86
+ error
87
+ ? `${error}`
88
+ : (resolved ?? `the species this gene is from (taxon ${value})`)
89
+ }
90
+ />
91
+ )
92
+ }
@@ -26,7 +26,6 @@ interface ContextMenuInfo {
26
26
  interface DisplayModel {
27
27
  contextMenuItems: () => MenuItem[]
28
28
  contextMenuInfo?: ContextMenuInfo
29
- isGeneLike?: boolean
30
29
  fetchFullFeature?: (
31
30
  featureId: string,
32
31
  displayedRegionIndex: number,
@@ -34,6 +33,25 @@ interface DisplayModel {
34
33
  contextMenuFeature?: Feature
35
34
  }
36
35
 
36
+ // Read off the clicked item rather than off the display.
37
+ //
38
+ // LinearBasicDisplay used to publish an `isGeneLike` getter and this gated on
39
+ // it. jbrowse-components 684142b3 (2026-08-16) inlined that getter into its own
40
+ // `contextMenuItems`, and every host built after it returns `undefined` here --
41
+ // so the gate was never satisfied, `onClick` stayed undefined, and the item
42
+ // silently left the right-click menu on every gene track. Nothing failed loudly:
43
+ // the display still had contextMenuInfo and fetchFullFeature, and the menu still
44
+ // opened with its own items in it.
45
+ //
46
+ // A predicate over the type we were already given cannot go the same way, and it
47
+ // costs one comparison. Deliberately the same loose case-insensitive test the
48
+ // host applies (`isGeneLikeType` in collapseIntronsMenu.ts): real GFFs carry
49
+ // 'mRNA', 'lnc_RNA', 'protein_coding_gene', 'transcript'.
50
+ function isGeneLikeType(type: string | undefined) {
51
+ const t = (type ?? '').toLowerCase()
52
+ return t.includes('gene') || t.includes('rna') || t.includes('transcript')
53
+ }
54
+
37
55
  const GENE_LIKE_TYPES = new Set(['gene', 'mRNA', 'transcript'])
38
56
 
39
57
  function extendStateModel(stateModel: IAnyModelType) {
@@ -54,7 +72,7 @@ function extendStateModel(stateModel: IAnyModelType) {
54
72
  const fetchFullFeature = self.fetchFullFeature
55
73
  const legacyFeature = self.contextMenuFeature
56
74
  const onClick =
57
- info && fetchFullFeature && self.isGeneLike
75
+ info && fetchFullFeature && isGeneLikeType(info.item.type)
58
76
  ? () => {
59
77
  fetchFullFeature(info.item.featureId, info.displayedRegionIndex)
60
78
  .then(feature => {
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+
3
+ import { Typography } from '@mui/material'
4
+
5
+ import ExternalLink from '../../components/ExternalLink'
6
+ import { ebiBlastResultUrl } from '../../utils/ebiBlast'
7
+
8
+ function JobLink({ jobId }: { jobId: string }) {
9
+ return (
10
+ <Typography>
11
+ Job {jobId} (
12
+ <ExternalLink href={ebiBlastResultUrl(jobId)}>see status</ExternalLink>)
13
+ </Typography>
14
+ )
15
+ }
16
+
17
+ export default JobLink
@@ -5,7 +5,7 @@ import { Typography } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
6
  import { makeStyles } from 'tss-react/mui'
7
7
 
8
- import RIDLink from './RIDLink'
8
+ import JobLink from './JobLink'
9
9
 
10
10
  import type { JBrowsePluginMsaViewModel } from '../model'
11
11
 
@@ -18,57 +18,26 @@ const useStyles = makeStyles()(theme => ({
18
18
  },
19
19
  }))
20
20
 
21
- function RIDError({
22
- baseUrl,
23
- rid,
24
- error,
25
- }: {
26
- baseUrl: string
27
- rid?: string
28
- error: unknown
29
- }) {
30
- return (
31
- <div>
32
- {rid ? <RIDLink rid={rid} baseUrl={baseUrl} /> : null}
33
- <ErrorMessage error={error} />
34
- </div>
35
- )
36
- }
37
-
38
- function RIDProgress({
39
- baseUrl,
40
- rid,
41
- progress,
42
- }: {
43
- baseUrl: string
44
- rid: string
45
- progress: string
46
- }) {
47
- const { classes } = useStyles()
48
- return (
49
- <div className={classes.loading}>
50
- {rid ? <RIDLink baseUrl={baseUrl} rid={rid} /> : null}
51
- <Typography>{progress}</Typography>
52
- </div>
53
- )
54
- }
55
-
56
21
  const LoadingBLAST = observer(function LoadingBLAST2({
57
22
  model,
58
- baseUrl,
59
23
  }: {
60
24
  model: JBrowsePluginMsaViewModel
61
- baseUrl: string
62
25
  }) {
63
26
  const { progress, rid, error } = model
64
27
  const { classes } = useStyles()
65
28
  return (
66
29
  <div className={classes.margin}>
67
- <LoadingEllipses message="Running NCBI BLAST" variant="h5" />
30
+ <LoadingEllipses message="Running EBI BLAST" variant="h5" />
68
31
  {error ? (
69
- <RIDError baseUrl={baseUrl} rid={rid} error={error} />
32
+ <div>
33
+ {rid ? <JobLink jobId={rid} /> : null}
34
+ <ErrorMessage error={error} />
35
+ </div>
70
36
  ) : rid ? (
71
- <RIDProgress baseUrl={baseUrl} rid={rid} progress={progress} />
37
+ <div className={classes.loading}>
38
+ <JobLink jobId={rid} />
39
+ <Typography>{progress}</Typography>
40
+ </div>
72
41
  ) : (
73
42
  <Typography>{progress || 'Initializing BLAST query'}</Typography>
74
43
  )}
@@ -27,7 +27,7 @@ const MsaViewPanel = observer(function MsaViewPanel2({
27
27
  <ErrorBoundary>
28
28
  <div>
29
29
  {blastParams ? (
30
- <LoadingBLAST model={model} baseUrl={blastParams.baseUrl} />
30
+ <LoadingBLAST model={model} />
31
31
  ) : loadingStoredData ? (
32
32
  <div className={classes.loadingContainer}>
33
33
  <LoadingEllipses message="Loading MSA data" variant="h6" />