jbrowse-plugin-msaview 3.2.0 → 3.4.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 (106) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +61 -17
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +9 -4
  16. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
  17. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
  18. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  19. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  20. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  21. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  22. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  23. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  24. package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
  25. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  26. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  27. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  28. package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
  29. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  30. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  31. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
  32. package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
  33. package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
  34. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
  35. package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
  36. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +106 -1
  37. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  38. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  39. package/dist/MsaViewPanel/model.d.ts +41 -11
  40. package/dist/MsaViewPanel/model.js +6 -0
  41. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  42. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  43. package/dist/MsaViewPanel/util.d.ts +18 -0
  44. package/dist/MsaViewPanel/util.js +17 -0
  45. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  46. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  47. package/dist/utils/blastCache.d.ts +10 -6
  48. package/dist/utils/blastCache.js +15 -3
  49. package/dist/utils/ebiBlast.d.ts +1 -1
  50. package/dist/utils/msa.d.ts +12 -0
  51. package/dist/utils/msa.js +35 -12
  52. package/dist/utils/msaRows.d.ts +31 -0
  53. package/dist/utils/msaRows.js +67 -0
  54. package/dist/utils/pantherOrthologs.d.ts +79 -0
  55. package/dist/utils/pantherOrthologs.js +262 -0
  56. package/dist/utils/phmmer.d.ts +53 -0
  57. package/dist/utils/phmmer.js +118 -0
  58. package/dist/utils/taxonomyNames.d.ts +1 -1
  59. package/dist/utils/taxonomyNames.js +6 -1
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +27 -21
  63. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  64. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  65. package/src/AddHighlightModel/index.tsx +1 -1
  66. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
  67. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  68. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  69. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  70. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  71. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  72. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  73. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  74. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +21 -5
  75. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
  76. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  77. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  78. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  79. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  80. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  81. package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
  82. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  83. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  84. package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
  85. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
  86. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  87. package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
  88. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +119 -2
  89. package/src/MsaViewPanel/doLaunchOrthologs.ts +100 -38
  90. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  91. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  92. package/src/MsaViewPanel/model.ts +38 -5
  93. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  94. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  95. package/src/MsaViewPanel/util.ts +18 -0
  96. package/src/utils/blastCache.ts +33 -12
  97. package/src/utils/ebiBlast.ts +1 -1
  98. package/src/utils/msa.ts +43 -12
  99. package/src/utils/msaRows.ts +95 -0
  100. package/src/utils/pantherOrthologs.ts +399 -0
  101. package/src/utils/phmmer.ts +174 -0
  102. package/src/utils/taxonomyNames.ts +6 -1
  103. package/src/version.ts +1 -1
  104. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  105. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  106. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -5,7 +5,6 @@ import { FormControl, FormControlLabel, Radio, RadioGroup } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
6
  import { makeStyles } from 'tss-react/mui'
7
7
 
8
- import { launchView } from './launchView'
9
8
  import TextField2 from '../../../components/TextField2'
10
9
  import { useQueryRowName } from '../../useQueryRowName'
11
10
  import { getGeneDisplayName, getLinearGenomeView } from '../../util'
@@ -14,6 +13,7 @@ import QueryRowSelector from '../QueryRowSelector'
14
13
  import SubmitCancelActions from '../SubmitCancelActions'
15
14
  import TranscriptSelector from '../TranscriptSelector'
16
15
  import { useTranscriptSelection } from '../useTranscriptSelection'
16
+ import { launchView } from './launchView'
17
17
 
18
18
  import type {
19
19
  AbstractTrackModel,
@@ -4,10 +4,9 @@ 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'
8
- import { orthologLaunchView } from './orthologLaunchView'
9
7
  import TextField2 from '../../../components/TextField2'
10
8
  import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs'
9
+ import { useLocalStorage } from '../../../utils/useLocalStorage'
11
10
  import {
12
11
  getGeneDisplayName,
13
12
  getGeneIdentifiers,
@@ -19,7 +18,13 @@ import LaunchPanelContent from '../LaunchPanelContent'
19
18
  import SubmitCancelActions from '../SubmitCancelActions'
20
19
  import TranscriptSelector from '../TranscriptSelector'
21
20
  import { useTranscriptSelection } from '../useTranscriptSelection'
21
+ import OrthologSourceSelect, {
22
+ ORTHOLOG_SOURCE_STORAGE_KEY,
23
+ } from './OrthologSourceSelect'
24
+ import QuerySpeciesSelect from './QuerySpeciesSelect'
25
+ import { orthologLaunchView } from './orthologLaunchView'
22
26
 
27
+ import type { OrthologSource } from '../../../MsaViewPanel/model'
23
28
  import type { MsaAlgorithm } from '../BlastQuery/consts'
24
29
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
25
30
 
@@ -42,6 +47,10 @@ const OrthologPanel = observer(function ({
42
47
  const view = getLinearGenomeView(model)
43
48
  const [launchViewError, setLaunchViewError] = useState<unknown>()
44
49
  const [taxId, setTaxId] = useState(9606)
50
+ const [source, setSource] = useLocalStorage<OrthologSource>(
51
+ ORTHOLOG_SOURCE_STORAGE_KEY,
52
+ 'ncbi',
53
+ )
45
54
  const [msaAlgorithm, setMsaAlgorithm] = useState<MsaAlgorithm>('clustalo')
46
55
  const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies))
47
56
 
@@ -65,11 +74,17 @@ const OrthologPanel = observer(function ({
65
74
  instant, and it is now the aligner that costs the wait, about half a
66
75
  second per row. */}
67
76
  <Typography variant="body2">
68
- NCBI&apos;s precomputed orthologs, one gene per species, looked up
69
- rather than searched for. No BLAST job to queue.
77
+ Precomputed orthologs, one gene per species, looked up rather than
78
+ searched for. No BLAST job to queue.
70
79
  </Typography>
71
80
 
72
81
  <div>
82
+ <OrthologSourceSelect
83
+ className={classes.selectField}
84
+ value={source}
85
+ onChange={setSource}
86
+ />
87
+
73
88
  <QuerySpeciesSelect
74
89
  className={classes.selectField}
75
90
  value={taxId}
@@ -93,7 +108,7 @@ const OrthologPanel = observer(function ({
93
108
  setMaxSpecies(event.target.value)
94
109
  }}
95
110
  error={!rowCountValid}
96
- helperText="the closest N species NCBI has"
111
+ helperText={`the closest N species ${source === 'panther' ? 'PANTHER' : 'NCBI'} has`}
97
112
  />
98
113
  </div>
99
114
 
@@ -112,6 +127,7 @@ const OrthologPanel = observer(function ({
112
127
  newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
113
128
  orthologParams: {
114
129
  taxId,
130
+ source,
115
131
  maxSpecies: rowCount,
116
132
  geneCandidates,
117
133
  msaAlgorithm,
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+
3
+ import { MenuItem } from '@mui/material'
4
+
5
+ import TextField2 from '../../../components/TextField2'
6
+
7
+ import type { OrthologSource } from '../../../MsaViewPanel/model'
8
+
9
+ export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source'
10
+
11
+ export const orthologSourceLabels: Record<OrthologSource, string> = {
12
+ ncbi: 'NCBI orthologs',
13
+ panther: 'PANTHER',
14
+ }
15
+
16
+ // Which species a source can answer for, in the words a reader picking one
17
+ // needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
18
+ // from human to yeast and Arabidopsis.
19
+ const hints: Record<OrthologSource, string> = {
20
+ ncbi: 'vertebrates and insects',
21
+ panther: 'also yeast, worm, fly and plants',
22
+ }
23
+
24
+ export default function OrthologSourceSelect({
25
+ value,
26
+ onChange,
27
+ className,
28
+ }: {
29
+ value: OrthologSource
30
+ onChange: (val: OrthologSource) => void
31
+ className?: string
32
+ }) {
33
+ return (
34
+ <TextField2
35
+ variant="outlined"
36
+ label="Source"
37
+ className={className}
38
+ select
39
+ value={value}
40
+ helperText={hints[value]}
41
+ onChange={event => {
42
+ onChange(event.target.value as OrthologSource)
43
+ }}
44
+ >
45
+ {(Object.keys(orthologSourceLabels) as OrthologSource[]).map(val => (
46
+ <MenuItem value={val} key={val}>
47
+ {orthologSourceLabels[val]}
48
+ </MenuItem>
49
+ ))}
50
+ </TextField2>
51
+ )
52
+ }
@@ -4,8 +4,8 @@ import React from 'react'
4
4
  import { cleanup, render, screen } from '@testing-library/react'
5
5
  import { afterEach, beforeEach, expect, test, vi } from 'vitest'
6
6
 
7
- import SubmitCancelActions from './SubmitCancelActions'
8
7
  import { LAUNCH_PLACEMENT_KEY } from '../../utils/workspaces'
8
+ import SubmitCancelActions from './SubmitCancelActions'
9
9
 
10
10
  import type { AbstractTrackModel } from '@jbrowse/core/util'
11
11
 
@@ -1,8 +1,8 @@
1
1
  import { getSession } from '@jbrowse/core/util'
2
2
 
3
+ import { useFetch } from '../../utils/useFetch'
3
4
  import { getProteinSequenceFromFeature } from './calculateProteinSequence'
4
5
  import { fetchSeq } from './fetchSeq'
5
- import { useFetch } from '../../utils/useFetch'
6
6
 
7
7
  import type { Feature } from '@jbrowse/core/util'
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'vitest'
2
2
 
3
- import { detectQueryRow, getMsaRowNames } from './detectQueryRow'
3
+ import { findQueryRow } from './detectQueryRow'
4
4
 
5
5
  const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD'
6
6
 
@@ -18,25 +18,25 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
18
18
  MKWVTFISLLLLFSSAYSRG--RRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPYD
19
19
  `
20
20
 
21
- describe('detectQueryRow', () => {
21
+ describe('findQueryRow', () => {
22
22
  test('finds the query by sequence when the aligner renamed it', () => {
23
- expect(detectQueryRow(clustal, protein)).toMatchObject({
23
+ expect(findQueryRow(clustal, protein).match).toMatchObject({
24
24
  name: 'Query_1',
25
25
  quality: 'exact',
26
26
  })
27
27
  })
28
28
 
29
29
  test('ignores gaps in the aligned row', () => {
30
- expect(detectQueryRow(fasta, protein)?.name).toBe('Query_1')
30
+ expect(findQueryRow(fasta, protein).match?.name).toBe('Query_1')
31
31
  })
32
32
 
33
33
  test('tolerates the trailing stop codon the translation carries', () => {
34
- expect(detectQueryRow(clustal, `${protein}*`)?.name).toBe('Query_1')
34
+ expect(findQueryRow(clustal, `${protein}*`).match?.name).toBe('Query_1')
35
35
  })
36
36
 
37
37
  test('matches a row that is the query trimmed to the aligned region', () => {
38
38
  const trimmed = `>hit_one\nWRONGWRONGWRONGWRONG\n>aligned_query\n${protein.slice(5, 40)}\n`
39
- expect(detectQueryRow(trimmed, protein)).toMatchObject({
39
+ expect(findQueryRow(trimmed, protein).match).toMatchObject({
40
40
  name: 'aligned_query',
41
41
  quality: 'partial',
42
42
  })
@@ -51,21 +51,23 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
51
51
  MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
52
52
  `
53
53
  expect(
54
- detectQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'),
54
+ findQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW').match,
55
55
  ).toBeUndefined()
56
56
  })
57
57
 
58
58
  test('returns nothing rather than throwing on a half-pasted alignment', () => {
59
- expect(detectQueryRow('>partial\nMKWV', protein)).toBeUndefined()
60
- expect(detectQueryRow('not an alignment at all', protein)).toBeUndefined()
61
- expect(detectQueryRow('', protein)).toBeUndefined()
62
- expect(detectQueryRow(clustal, '')).toBeUndefined()
59
+ expect(findQueryRow('>partial\nMKWV', protein).match).toBeUndefined()
60
+ expect(
61
+ findQueryRow('not an alignment at all', protein).match,
62
+ ).toBeUndefined()
63
+ expect(findQueryRow('', protein).match).toBeUndefined()
64
+ expect(findQueryRow(clustal, '').match).toBeUndefined()
63
65
  })
64
66
  })
65
67
 
66
- describe('getMsaRowNames', () => {
68
+ describe('findQueryRow row names', () => {
67
69
  test('lists the rows for the override dropdown', () => {
68
- expect(getMsaRowNames(clustal)).toEqual([
70
+ expect(findQueryRow(clustal, protein).names).toEqual([
69
71
  'Query_1',
70
72
  'sp|P02769|ALBU',
71
73
  'sp|Q5XLE4|OTHE',
@@ -73,7 +75,7 @@ describe('getMsaRowNames', () => {
73
75
  })
74
76
 
75
77
  test('is empty rather than throwing while the user is still pasting', () => {
76
- expect(getMsaRowNames('CLUSTAL W')).toEqual([])
77
- expect(getMsaRowNames('')).toEqual([])
78
+ expect(findQueryRow('CLUSTAL W', protein).names).toEqual([])
79
+ expect(findQueryRow('', protein).names).toEqual([])
78
80
  })
79
81
  })
@@ -65,27 +65,48 @@ const SIMILARITY_FLOOR = 0.9
65
65
  */
66
66
  const PARTIAL_COVERAGE_FLOOR = 0.5
67
67
 
68
- export function detectQueryRow(
68
+ export interface MsaQueryRow {
69
+ /** every row name, in file order, for the picker to offer */
70
+ names: string[]
71
+ /** the row whose residues are the query's, if one of them is */
72
+ match?: QueryRowMatch
73
+ }
74
+
75
+ /**
76
+ * The picker's whole answer for a pasted alignment: its row names, and which of
77
+ * them is the query.
78
+ *
79
+ * One function rather than two because there is one parse. Both answers were
80
+ * wanted on every keystroke in the paste box, and asking separately parsed a
81
+ * few-hundred-row alignment twice per character.
82
+ */
83
+ export function findQueryRow(
69
84
  msaText: string,
70
85
  proteinSequence: string,
71
- ): QueryRowMatch | undefined {
72
- const query = normalize(proteinSequence)
73
- if (!query || !msaText.trim()) {
74
- return undefined
86
+ ): MsaQueryRow {
87
+ if (!msaText.trim()) {
88
+ return { names: [] }
75
89
  }
76
90
 
77
- let names: string[]
78
- let parsed: { getRow: (name: string) => string }
91
+ let parsed
79
92
  try {
80
- const msa = parseMSA(msaText)
81
- names = msa.getNames()
82
- parsed = msa
93
+ parsed = parseMSA(msaText)
83
94
  } catch {
84
95
  // a half-pasted alignment throws here on every keystroke; the caller shows
85
96
  // the field rather than an error
86
- return undefined
97
+ return { names: [] }
87
98
  }
88
99
 
100
+ const names = parsed.getNames()
101
+ const query = normalize(proteinSequence)
102
+ return { names, match: query ? bestMatch(parsed, names, query) : undefined }
103
+ }
104
+
105
+ function bestMatch(
106
+ parsed: { getRow: (name: string) => string },
107
+ names: string[],
108
+ query: string,
109
+ ): QueryRowMatch | undefined {
89
110
  const candidates: QueryRowMatch[] = []
90
111
  for (const name of names) {
91
112
  const row = normalize(getUngappedSequence(parsed.getRow(name)))
@@ -112,21 +133,11 @@ export function detectQueryRow(
112
133
  }
113
134
  }
114
135
 
115
- const order: MatchQuality[] = ['exact', 'partial', 'similar']
136
+ // an exact match returns above, so only these two can be here
137
+ const order: MatchQuality[] = ['partial', 'similar']
116
138
  return candidates.sort(
117
139
  (a, b) =>
118
140
  order.indexOf(a.quality) - order.indexOf(b.quality) ||
119
141
  b.identity - a.identity,
120
142
  )[0]
121
143
  }
122
-
123
- export function getMsaRowNames(msaText: string): string[] {
124
- if (!msaText.trim()) {
125
- return []
126
- }
127
- try {
128
- return parseMSA(msaText).getNames()
129
- } catch {
130
- return []
131
- }
132
- }
@@ -1,6 +1,6 @@
1
1
  import { useMemo, useState } from 'react'
2
2
 
3
- import { detectQueryRow, getMsaRowNames } from './detectQueryRow'
3
+ import { findQueryRow } from './detectQueryRow'
4
4
 
5
5
  /**
6
6
  * The MSA row name to launch with, found by sequence rather than typed.
@@ -15,19 +15,16 @@ export function useQueryRowName(msaText: string, proteinSequence: string) {
15
15
 
16
16
  // parsing runs on every keystroke in the paste box otherwise, and an
17
17
  // alignment of a few hundred rows is not free
18
- const { detected, names } = useMemo(
19
- () => ({
20
- detected: detectQueryRow(msaText, proteinSequence),
21
- names: getMsaRowNames(msaText),
22
- }),
18
+ const { names, match } = useMemo(
19
+ () => findQueryRow(msaText, proteinSequence),
23
20
  [msaText, proteinSequence],
24
21
  )
25
22
 
26
23
  return {
27
- detected,
24
+ detected: match,
28
25
  names,
29
- querySeqName: override ?? detected?.name ?? '',
26
+ querySeqName: override ?? match?.name ?? '',
30
27
  setQuerySeqName: setOverride,
31
- isAutoDetected: override === undefined && !!detected,
28
+ isAutoDetected: override === undefined && !!match,
32
29
  }
33
30
  }
@@ -12,7 +12,7 @@ import {
12
12
  storeMsaData,
13
13
  } from './msaDataStore'
14
14
  import { getProteinViews } from './structureConnection'
15
- import { getUniprotIdFromAlphaFoldUrl } from './util'
15
+ import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util'
16
16
 
17
17
  import type { JBrowsePluginMsaViewModel } from './model'
18
18
 
@@ -233,7 +233,7 @@ function genomeHighlightsToVisibleColumns(
233
233
  field: 'hoverGenomeHighlights' | 'clickGenomeHighlights',
234
234
  ) {
235
235
  const { connectedViewId, transcriptToMsaMap, querySeqName } = self
236
- if (!transcriptToMsaMap) {
236
+ if (!transcriptToMsaMap || !hasQueryRow(self)) {
237
237
  return []
238
238
  }
239
239
  const { g2p } = transcriptToMsaMap
@@ -1,8 +1,9 @@
1
- import type { ReactNode } from 'react'
2
1
  import React, { Component } from 'react'
3
2
 
4
3
  import { ErrorMessage } from '@jbrowse/core/ui'
5
4
 
5
+ import type { ReactNode } from 'react'
6
+
6
7
  interface Props {
7
8
  children: ReactNode
8
9
  }
@@ -4,12 +4,17 @@ import { Typography } from '@mui/material'
4
4
 
5
5
  import ExternalLink from '../../components/ExternalLink'
6
6
  import { ebiBlastResultUrl } from '../../utils/ebiBlast'
7
+ import { isPhmmerJobId, phmmerResultUrl } from '../../utils/phmmer'
7
8
 
8
9
  function JobLink({ jobId }: { jobId: string }) {
10
+ // read off the job id rather than the launch params, so a link rebuilt for an
11
+ // old cached job still points at the tool that actually ran it
12
+ const url = isPhmmerJobId(jobId)
13
+ ? phmmerResultUrl(jobId)
14
+ : ebiBlastResultUrl(jobId)
9
15
  return (
10
16
  <Typography>
11
- Job {jobId} (
12
- <ExternalLink href={ebiBlastResultUrl(jobId)}>see status</ExternalLink>)
17
+ Job {jobId} (<ExternalLink href={url}>see status</ExternalLink>)
13
18
  </Typography>
14
19
  )
15
20
  }
@@ -0,0 +1,62 @@
1
+ import React from 'react'
2
+
3
+ import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui'
4
+ import { Typography } from '@mui/material'
5
+ import { observer } from 'mobx-react'
6
+ import { makeStyles } from 'tss-react/mui'
7
+
8
+ import JobLink from './JobLink'
9
+
10
+ import type { JBrowsePluginMsaViewModel } from '../model'
11
+
12
+ const useStyles = makeStyles()({
13
+ margin: {
14
+ padding: 20,
15
+ },
16
+ })
17
+
18
+ /**
19
+ * What a view shows while it is still building its alignment, and what it shows
20
+ * when that fails.
21
+ *
22
+ * Every launch that resolves something leaves its request on the model until it
23
+ * succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
24
+ * there IS "no alignment yet", and the error a failed launch records is only
25
+ * readable here. This used to key on `blastParams` alone, which left an ortholog
26
+ * launch rendering an empty MSAView for the minutes its alignment takes and, on
27
+ * failure, forever: the error was set and nothing drew it.
28
+ */
29
+ const LaunchProgress = observer(function LaunchProgress2({
30
+ model,
31
+ }: {
32
+ model: JBrowsePluginMsaViewModel
33
+ }) {
34
+ const { blastParams, orthologParams, progress, rid, error } = model
35
+ const { classes } = useStyles()
36
+ const message = blastParams
37
+ ? 'Running EBI BLAST'
38
+ : orthologParams
39
+ ? 'Building ortholog alignment'
40
+ : 'Loading alignment'
41
+ return (
42
+ <div className={classes.margin}>
43
+ {error ? (
44
+ <>
45
+ <Typography variant="h5">{message} failed</Typography>
46
+ {/* the job outlives the browser, so its link is worth keeping next to
47
+ the failure -- EBI's own page says more about a job than we can */}
48
+ {rid ? <JobLink jobId={rid} /> : null}
49
+ <ErrorMessage error={error} />
50
+ </>
51
+ ) : (
52
+ <>
53
+ <LoadingEllipses message={message} variant="h5" />
54
+ {rid ? <JobLink jobId={rid} /> : null}
55
+ <Typography>{progress || 'Initializing'}</Typography>
56
+ </>
57
+ )}
58
+ </div>
59
+ )
60
+ })
61
+
62
+ export default LaunchProgress
@@ -0,0 +1,83 @@
1
+ // @vitest-environment jsdom
2
+ import React from 'react'
3
+
4
+ import { cleanup, render, screen } from '@testing-library/react'
5
+ import { afterEach, expect, test, vi } from 'vitest'
6
+
7
+ import MsaViewPanel from './MsaViewPanel'
8
+
9
+ import type { JBrowsePluginMsaViewModel } from '../model'
10
+
11
+ // react-msaview's MSAView is the "nothing is launching" branch and wants a real
12
+ // MST model; a marker is enough to say the panel reached it
13
+ vi.mock('react-msaview', () => ({
14
+ MSAView: () => <div>the alignment</div>,
15
+ }))
16
+
17
+ function panel(model: Partial<JBrowsePluginMsaViewModel>) {
18
+ return render(<MsaViewPanel model={model as JBrowsePluginMsaViewModel} />)
19
+ }
20
+
21
+ afterEach(() => {
22
+ cleanup()
23
+ })
24
+
25
+ test('an alignment with no pending launch draws itself', () => {
26
+ panel({ progress: '' })
27
+ expect(screen.getByText('the alignment')).toBeTruthy()
28
+ })
29
+
30
+ test('a running BLAST shows its progress, not an empty alignment', () => {
31
+ panel({
32
+ blastParams: { proteinSequence: 'MKV' } as never,
33
+ progress: 'Submitting query',
34
+ })
35
+ expect(screen.queryByText('the alignment')).toBeNull()
36
+ expect(screen.getByText(/Running EBI BLAST/)).toBeTruthy()
37
+ expect(screen.getByText('Submitting query')).toBeTruthy()
38
+ })
39
+
40
+ // the bug this file was written for: an ortholog launch sets orthologParams
41
+ // rather than blastParams, and the panel keyed on blastParams alone -- so it
42
+ // rendered an empty MSAView for the minutes the alignment takes, and drew
43
+ // nothing at all when the launch failed
44
+ test('a running ortholog launch shows its progress', () => {
45
+ panel({
46
+ orthologParams: { taxId: 9606 } as never,
47
+ progress: 'Resolving orthologs',
48
+ })
49
+ expect(screen.queryByText('the alignment')).toBeNull()
50
+ expect(screen.getByText(/Building ortholog alignment/)).toBeTruthy()
51
+ expect(screen.getByText('Resolving orthologs')).toBeTruthy()
52
+ })
53
+
54
+ test('a failed ortholog launch shows why', () => {
55
+ panel({
56
+ orthologParams: { taxId: 9606 } as never,
57
+ progress: '',
58
+ error: new Error('Only 1 ortholog(s) found for this gene'),
59
+ })
60
+ expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy()
61
+ })
62
+
63
+ test('a failed init shows why', () => {
64
+ panel({
65
+ init: { msaName: 'ENST00000288602' },
66
+ progress: '',
67
+ error: new Error('No alignment named ENST00000288602 in msa.fa.gz'),
68
+ })
69
+ expect(screen.queryByText('the alignment')).toBeNull()
70
+ expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy()
71
+ })
72
+
73
+ test('a running job links out to it', () => {
74
+ panel({
75
+ blastParams: { proteinSequence: 'MKV' } as never,
76
+ progress: 'Re-checking BLAST status in... 7',
77
+ rid: 'ncbiblast-R20260826-123456-0001-abc',
78
+ })
79
+ const link = screen.getByRole('link')
80
+ expect(link.getAttribute('href')).toContain(
81
+ 'jobId=ncbiblast-R20260826-123456-0001-abc',
82
+ )
83
+ })
@@ -6,7 +6,7 @@ import { MSAView } from 'react-msaview'
6
6
  import { makeStyles } from 'tss-react/mui'
7
7
 
8
8
  import { ErrorBoundary } from './ErrorBoundary'
9
- import LoadingBLAST from './LoadingBLAST'
9
+ import LaunchProgress from './LaunchProgress'
10
10
 
11
11
  import type { JBrowsePluginMsaViewModel } from '../model'
12
12
 
@@ -22,12 +22,15 @@ const MsaViewPanel = observer(function MsaViewPanel2({
22
22
  model: JBrowsePluginMsaViewModel
23
23
  }) {
24
24
  const { classes } = useStyles()
25
- const { blastParams, loadingStoredData } = model
25
+ const { blastParams, orthologParams, init, loadingStoredData } = model
26
+ // an unresolved launch request means there is no alignment to draw yet, so all
27
+ // three gate the same panel -- see LaunchProgress
28
+ const launching = !!(blastParams ?? orthologParams ?? init)
26
29
  return (
27
30
  <ErrorBoundary>
28
31
  <div>
29
- {blastParams ? (
30
- <LoadingBLAST model={model} />
32
+ {launching ? (
33
+ <LaunchProgress model={model} />
31
34
  ) : loadingStoredData ? (
32
35
  <div className={classes.loadingContainer}>
33
36
  <LoadingEllipses message="Loading MSA data" variant="h6" />