jbrowse-plugin-msaview 3.4.0 → 3.4.1
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.
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +4 -6
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +2 -1
- package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +93 -70
- package/dist/MsaViewPanel/components/LaunchProgress.js +11 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +19 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +10 -12
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +38 -24
- package/dist/MsaViewPanel/model.d.ts +160 -10
- package/dist/MsaViewPanel/model.js +51 -1
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +16 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
- package/dist/MsaViewPanel/runLaunch.js +65 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +129 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +35 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.js +32 -0
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/ebiBlast.d.ts +4 -2
- package/dist/utils/ebiBlast.js +6 -4
- package/dist/utils/ebiJobDispatcher.d.ts +6 -3
- package/dist/utils/ebiJobDispatcher.js +13 -6
- package/dist/utils/fetch.d.ts +8 -1
- package/dist/utils/fetch.js +28 -2
- package/dist/utils/msa.d.ts +4 -2
- package/dist/utils/msa.js +9 -5
- package/dist/utils/ncbiDomains.d.ts +2 -2
- package/dist/utils/ncbiOrthologs.d.ts +11 -1
- package/dist/utils/ncbiOrthologs.js +26 -7
- package/dist/utils/ncbiOrthologs.test.js +23 -1
- package/dist/utils/pantherOrthologs.js +2 -10
- package/dist/utils/phmmer.d.ts +2 -1
- package/dist/utils/phmmer.js +4 -2
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -14
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +7 -11
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +2 -2
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +100 -66
- package/src/MsaViewPanel/components/LaunchProgress.tsx +20 -2
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +21 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +14 -10
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +41 -26
- package/src/MsaViewPanel/doLaunchOrthologs.ts +8 -4
- package/src/MsaViewPanel/model.ts +54 -1
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +22 -13
- package/src/MsaViewPanel/runLaunch.test.ts +154 -0
- package/src/MsaViewPanel/runLaunch.ts +102 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +34 -1
- package/src/utils/ebiBlast.ts +8 -2
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/msa.ts +11 -3
- package/src/utils/ncbiDomains.ts +4 -2
- package/src/utils/ncbiOrthologs.test.ts +25 -0
- package/src/utils/ncbiOrthologs.ts +27 -7
- package/src/utils/pantherOrthologs.ts +6 -11
- package/src/utils/phmmer.ts +5 -1
- package/src/utils/poll.ts +8 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.4.
|
|
2
|
+
"version": "3.4.1",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -28,41 +28,41 @@
|
|
|
28
28
|
"@jbrowse/core": "^4.3.0",
|
|
29
29
|
"@jbrowse/mobx-state-tree": "^5.13.0",
|
|
30
30
|
"@jbrowse/plugin-linear-genome-view": "^4.3.0",
|
|
31
|
-
"@mui/icons-material": "^9.
|
|
32
|
-
"@mui/material": "^9.
|
|
33
|
-
"@mui/system": "^9.
|
|
31
|
+
"@mui/icons-material": "^9.4.0",
|
|
32
|
+
"@mui/material": "^9.4.0",
|
|
33
|
+
"@mui/system": "^9.4.0",
|
|
34
34
|
"@mui/x-data-grid": "^9.12.0",
|
|
35
35
|
"@testing-library/dom": "^10.4.1",
|
|
36
|
-
"@testing-library/react": "^16.3.
|
|
37
|
-
"@types/node": "^26.
|
|
36
|
+
"@testing-library/react": "^16.3.3",
|
|
37
|
+
"@types/node": "^26.4.1",
|
|
38
38
|
"@types/react": "^19.2.18",
|
|
39
39
|
"esbuild": "^0.28.2",
|
|
40
40
|
"eslint": "^10.9.1",
|
|
41
41
|
"eslint-plugin-import-x": "^4.17.1",
|
|
42
42
|
"eslint-plugin-react": "^7.37.5",
|
|
43
43
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
44
|
-
"eslint-plugin-unicorn": "^
|
|
44
|
+
"eslint-plugin-unicorn": "^74.0.0",
|
|
45
45
|
"git-cliff": "^2.13.1",
|
|
46
46
|
"jsdom": "^30.0.1",
|
|
47
47
|
"mobx": "^6.16.1",
|
|
48
48
|
"mobx-react": "^9.2.2",
|
|
49
|
-
"msa-parsers": "^6.
|
|
49
|
+
"msa-parsers": "^6.4.2",
|
|
50
50
|
"oxfmt": "^0.65.0",
|
|
51
|
-
"oxlint": "^1.
|
|
51
|
+
"oxlint": "^1.81.0",
|
|
52
52
|
"oxlint-tsgolint": "^7.0.2001",
|
|
53
53
|
"pixelmatch": "^7.2.0",
|
|
54
54
|
"pngjs": "^7.0.0",
|
|
55
|
-
"pretty-bytes": "^7.1.
|
|
56
|
-
"puppeteer": "^25.
|
|
55
|
+
"pretty-bytes": "^7.1.2",
|
|
56
|
+
"puppeteer": "^25.9.0",
|
|
57
57
|
"react": "^19.2.8",
|
|
58
58
|
"react-dom": "^19.2.8",
|
|
59
|
-
"react-msaview": "^6.
|
|
59
|
+
"react-msaview": "^6.4.2",
|
|
60
60
|
"rimraf": "^6.1.3",
|
|
61
61
|
"rxjs": "^7.8.2",
|
|
62
62
|
"serve": "^14.2.6",
|
|
63
63
|
"tss-react": "^4.9.21",
|
|
64
|
-
"typescript": "^6.0.
|
|
65
|
-
"typescript-eslint": "^8.
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"typescript-eslint": "^8.69.0",
|
|
66
66
|
"vitest": "^4.1.11"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
@@ -24,15 +24,14 @@ import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
|
24
24
|
import CachedBlastResults from './CachedBlastResults'
|
|
25
25
|
import MsaAlgorithmSelect from './MsaAlgorithmSelect'
|
|
26
26
|
import { blastLaunchView } from './blastLaunchView'
|
|
27
|
+
import { databaseOptionsFor, defaultSearchFor, searchPrograms } from './consts'
|
|
27
28
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
searchPrograms,
|
|
32
|
-
} from './consts'
|
|
29
|
+
useStoredMsaAlgorithm,
|
|
30
|
+
useStoredSearchChoice,
|
|
31
|
+
} from './searchChoiceStorage'
|
|
33
32
|
import { useCachedBlastResults } from './useCachedBlastResults'
|
|
34
33
|
|
|
35
|
-
import type {
|
|
34
|
+
import type { SearchChoice, SearchProgram } from './consts'
|
|
36
35
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
37
36
|
|
|
38
37
|
const useStyles = makeStyles()({
|
|
@@ -70,12 +69,9 @@ const BlastAutomaticPanel = observer(function ({
|
|
|
70
69
|
// one piece of state, not two: a program and a database that program does not
|
|
71
70
|
// have is a 400 from EBI minutes after Submit, and holding them apart is what
|
|
72
71
|
// would let them drift into that
|
|
73
|
-
const [search, setSearch] =
|
|
74
|
-
program: 'blastp',
|
|
75
|
-
database: defaultBlastDatabase,
|
|
76
|
-
})
|
|
72
|
+
const [search, setSearch] = useStoredSearchChoice()
|
|
77
73
|
const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] =
|
|
78
|
-
|
|
74
|
+
useStoredMsaAlgorithm()
|
|
79
75
|
const isPhmmer = search.program === 'phmmer'
|
|
80
76
|
|
|
81
77
|
const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature])
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { validMsaAlgorithm, validSearchChoice } from './searchChoiceStorage'
|
|
4
|
+
|
|
5
|
+
test('a stored pair both services recognise comes back as it went in', () => {
|
|
6
|
+
expect(
|
|
7
|
+
validSearchChoice({ program: 'phmmer', database: 'swissprot' }),
|
|
8
|
+
).toEqual({
|
|
9
|
+
program: 'phmmer',
|
|
10
|
+
database: 'swissprot',
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('a database the stored program does not have falls back to the defaults', () => {
|
|
15
|
+
// uniprotkb_swissprot is blastp's name for it; sending it to phmmer is a 400
|
|
16
|
+
// from EBI minutes after Submit
|
|
17
|
+
expect(
|
|
18
|
+
validSearchChoice({ program: 'phmmer', database: 'uniprotkb_swissprot' }),
|
|
19
|
+
).toEqual({ program: 'blastp', database: 'uniprotkb_swissprot' })
|
|
20
|
+
// uniprotkb_reference_proteomes shipped as a menu entry EBI rejects, and 3.0.0
|
|
21
|
+
// is old enough that someone's storage still holds it
|
|
22
|
+
expect(
|
|
23
|
+
validSearchChoice({
|
|
24
|
+
program: 'blastp',
|
|
25
|
+
database: 'uniprotkb_reference_proteomes',
|
|
26
|
+
}),
|
|
27
|
+
).toEqual({ program: 'blastp', database: 'uniprotkb_swissprot' })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('storage holding something else entirely falls back rather than throwing', () => {
|
|
31
|
+
for (const stored of [null, undefined, 'blastp', 42, {}, []]) {
|
|
32
|
+
expect(validSearchChoice(stored)).toEqual({
|
|
33
|
+
program: 'blastp',
|
|
34
|
+
database: 'uniprotkb_swissprot',
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('an unknown algorithm falls back to clustalo', () => {
|
|
40
|
+
expect(validMsaAlgorithm('mafft')).toBe('mafft')
|
|
41
|
+
expect(validMsaAlgorithm('t_coffee')).toBe('clustalo')
|
|
42
|
+
expect(validMsaAlgorithm(null)).toBe('clustalo')
|
|
43
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage'
|
|
2
|
+
import {
|
|
3
|
+
databaseOptionsFor,
|
|
4
|
+
defaultSearchFor,
|
|
5
|
+
msaAlgorithms,
|
|
6
|
+
searchPrograms,
|
|
7
|
+
} from './consts'
|
|
8
|
+
|
|
9
|
+
import type { MsaAlgorithm, SearchChoice } from './consts'
|
|
10
|
+
|
|
11
|
+
// The panel reopens on the last search run rather than on the defaults, the way
|
|
12
|
+
// the Orthologs tab remembers its source.
|
|
13
|
+
//
|
|
14
|
+
// The program and its database stay ONE stored value for the reason consts.ts
|
|
15
|
+
// keeps them one state: neither service knows the other's database names. What
|
|
16
|
+
// comes back is checked against the current options before it is used — an
|
|
17
|
+
// option this plugin has since dropped would otherwise reach EBI and come back
|
|
18
|
+
// a 400, minutes after the user pressed Submit.
|
|
19
|
+
export const SEARCH_CHOICE_STORAGE_KEY = 'msaView-blastSearch'
|
|
20
|
+
export const MSA_ALGORITHM_STORAGE_KEY = 'msaView-msaAlgorithm'
|
|
21
|
+
|
|
22
|
+
const defaultSearch = defaultSearchFor('blastp')
|
|
23
|
+
const defaultMsaAlgorithm: MsaAlgorithm = 'clustalo'
|
|
24
|
+
|
|
25
|
+
export function validSearchChoice(stored: unknown): SearchChoice {
|
|
26
|
+
const { program, database } = (stored ?? {}) as {
|
|
27
|
+
program?: string
|
|
28
|
+
database?: string
|
|
29
|
+
}
|
|
30
|
+
const known = searchPrograms.find(p => p === program)
|
|
31
|
+
return known && databaseOptionsFor(known).some(d => d === database)
|
|
32
|
+
? ({ program: known, database } as SearchChoice)
|
|
33
|
+
: defaultSearch
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validMsaAlgorithm(stored: unknown): MsaAlgorithm {
|
|
37
|
+
return msaAlgorithms.find(a => a === stored) ?? defaultMsaAlgorithm
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useStoredSearchChoice() {
|
|
41
|
+
const [stored, setStored] = useLocalStorage<unknown>(
|
|
42
|
+
SEARCH_CHOICE_STORAGE_KEY,
|
|
43
|
+
defaultSearch,
|
|
44
|
+
)
|
|
45
|
+
return [
|
|
46
|
+
validSearchChoice(stored),
|
|
47
|
+
(choice: SearchChoice) => {
|
|
48
|
+
setStored(choice)
|
|
49
|
+
},
|
|
50
|
+
] as const
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useStoredMsaAlgorithm() {
|
|
54
|
+
const [stored, setStored] = useLocalStorage<unknown>(
|
|
55
|
+
MSA_ALGORITHM_STORAGE_KEY,
|
|
56
|
+
defaultMsaAlgorithm,
|
|
57
|
+
)
|
|
58
|
+
return [
|
|
59
|
+
validMsaAlgorithm(stored),
|
|
60
|
+
(algorithm: MsaAlgorithm) => {
|
|
61
|
+
setStored(algorithm)
|
|
62
|
+
},
|
|
63
|
+
] as const
|
|
64
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getTranscriptDisplayName,
|
|
15
15
|
} from '../../util'
|
|
16
16
|
import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect'
|
|
17
|
+
import { useStoredMsaAlgorithm } from '../BlastQuery/searchChoiceStorage'
|
|
17
18
|
import LaunchPanelContent from '../LaunchPanelContent'
|
|
18
19
|
import SubmitCancelActions from '../SubmitCancelActions'
|
|
19
20
|
import TranscriptSelector from '../TranscriptSelector'
|
|
@@ -25,7 +26,6 @@ import QuerySpeciesSelect from './QuerySpeciesSelect'
|
|
|
25
26
|
import { orthologLaunchView } from './orthologLaunchView'
|
|
26
27
|
|
|
27
28
|
import type { OrthologSource } from '../../../MsaViewPanel/model'
|
|
28
|
-
import type { MsaAlgorithm } from '../BlastQuery/consts'
|
|
29
29
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
30
30
|
|
|
31
31
|
const useStyles = makeStyles()({
|
|
@@ -51,7 +51,7 @@ const OrthologPanel = observer(function ({
|
|
|
51
51
|
ORTHOLOG_SOURCE_STORAGE_KEY,
|
|
52
52
|
'ncbi',
|
|
53
53
|
)
|
|
54
|
-
const [msaAlgorithm, setMsaAlgorithm] =
|
|
54
|
+
const [msaAlgorithm, setMsaAlgorithm] = useStoredMsaAlgorithm()
|
|
55
55
|
const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies))
|
|
56
56
|
|
|
57
57
|
const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature])
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import LaunchMsaViewExtensionPointF from './index'
|
|
4
|
+
|
|
5
|
+
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
6
|
+
import type { AbstractSessionModel } from '@jbrowse/core/util'
|
|
7
|
+
|
|
8
|
+
function launch(args: Record<string, unknown>) {
|
|
9
|
+
let run: ((args: unknown) => unknown) | undefined
|
|
10
|
+
LaunchMsaViewExtensionPointF({
|
|
11
|
+
addToExtensionPoint(_name: string, cb: (args: unknown) => unknown) {
|
|
12
|
+
run = cb
|
|
13
|
+
},
|
|
14
|
+
} as unknown as PluginManager)
|
|
15
|
+
const added: Record<string, unknown>[] = []
|
|
16
|
+
const session = {
|
|
17
|
+
addView(_type: string, snapshot: Record<string, unknown>) {
|
|
18
|
+
added.push(snapshot)
|
|
19
|
+
return { id: 'view-1' }
|
|
20
|
+
},
|
|
21
|
+
} as unknown as AbstractSessionModel
|
|
22
|
+
run!({ session, ...args })
|
|
23
|
+
return added[0]!
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('inline data carries no init, which the panel would read as a launch in flight', () => {
|
|
27
|
+
const snapshot = launch({ data: { msa: '>a\nMEEP' } })
|
|
28
|
+
expect('init' in snapshot).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('a file location travels through init', () => {
|
|
32
|
+
const snapshot = launch({
|
|
33
|
+
msaFileLocation: { uri: 'http://example.com/a.fa' },
|
|
34
|
+
})
|
|
35
|
+
expect(snapshot.init).toEqual({
|
|
36
|
+
msaUrl: 'http://example.com/a.fa',
|
|
37
|
+
msaIndexedLocation: undefined,
|
|
38
|
+
msaName: undefined,
|
|
39
|
+
querySeqName: undefined,
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('one field set is enough to need init', () => {
|
|
44
|
+
const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' })
|
|
45
|
+
expect(snapshot.init).toEqual({
|
|
46
|
+
msaUrl: undefined,
|
|
47
|
+
msaIndexedLocation: undefined,
|
|
48
|
+
msaName: undefined,
|
|
49
|
+
querySeqName: 'QUERY',
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -4,6 +4,7 @@ import type { OrthologParams } from '../MsaViewPanel/model'
|
|
|
4
4
|
import type { MsaViewPlacement } from '../utils/workspaces'
|
|
5
5
|
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
6
6
|
import type { AbstractSessionModel } from '@jbrowse/core/util'
|
|
7
|
+
import type { ColumnTrackSpec, Highlight } from 'react-msaview'
|
|
7
8
|
|
|
8
9
|
interface LaunchMsaViewArgs {
|
|
9
10
|
session: AbstractSessionModel
|
|
@@ -25,6 +26,15 @@ interface LaunchMsaViewArgs {
|
|
|
25
26
|
showBranchLen?: boolean
|
|
26
27
|
querySeqName?: string
|
|
27
28
|
highlightColumns?: number[]
|
|
29
|
+
/**
|
|
30
|
+
* Labeled highlights in 1-based inclusive coordinates: `{row, start, end}`
|
|
31
|
+
* for residues of that row, `{start, end}` for alignment columns, `{rows}`
|
|
32
|
+
* for whole rows. Prefer this over `highlightColumns`: a residue range stays
|
|
33
|
+
* put when `allowedGappyness` changes the visible columns.
|
|
34
|
+
*/
|
|
35
|
+
highlights?: Highlight[]
|
|
36
|
+
/** per-column tracks supplied as data, see react-msaview's docs/layers.md */
|
|
37
|
+
columnTracks?: ColumnTrackSpec[]
|
|
28
38
|
/**
|
|
29
39
|
* Build the alignment from NCBI orthologs at launch time instead of naming a
|
|
30
40
|
* file: `{ taxId, geneCandidates }` is enough, and `taxa` and
|
|
@@ -96,6 +106,16 @@ export default function LaunchMsaViewExtensionPointF(
|
|
|
96
106
|
// directly, and so is orthologParams (the model's own autorun picks it up).
|
|
97
107
|
// Only sources needing launch-time resolution go through `init`: msaUrl
|
|
98
108
|
// (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
|
|
109
|
+
//
|
|
110
|
+
// An init whose every field is undefined is still a truthy object, and
|
|
111
|
+
// MsaViewPanel reads any init as a launch in flight, so an inline-data
|
|
112
|
+
// launch flashed "Loading alignment" until processInit cleared it.
|
|
113
|
+
const init = {
|
|
114
|
+
msaUrl: msaFileLocation?.uri,
|
|
115
|
+
msaIndexedLocation,
|
|
116
|
+
msaName,
|
|
117
|
+
querySeqName,
|
|
118
|
+
}
|
|
99
119
|
launchMsaView(session, {
|
|
100
120
|
...rest,
|
|
101
121
|
data,
|
|
@@ -107,12 +127,7 @@ export default function LaunchMsaViewExtensionPointF(
|
|
|
107
127
|
},
|
|
108
128
|
}
|
|
109
129
|
: {}),
|
|
110
|
-
init: {
|
|
111
|
-
msaUrl: msaFileLocation?.uri,
|
|
112
|
-
msaIndexedLocation,
|
|
113
|
-
msaName,
|
|
114
|
-
querySeqName,
|
|
115
|
-
},
|
|
130
|
+
...(Object.values(init).some(v => v !== undefined) ? { init } : {}),
|
|
116
131
|
})
|
|
117
132
|
|
|
118
133
|
return args
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
|
+
import { transaction } from 'mobx'
|
|
2
3
|
|
|
3
4
|
import { doLaunchBlast } from './doLaunchBlast'
|
|
4
5
|
import { doLaunchOrthologs } from './doLaunchOrthologs'
|
|
@@ -11,10 +12,15 @@ import {
|
|
|
11
12
|
retrieveMsaData,
|
|
12
13
|
storeMsaData,
|
|
13
14
|
} from './msaDataStore'
|
|
15
|
+
import { runLaunch } from './runLaunch'
|
|
14
16
|
import { getProteinViews } from './structureConnection'
|
|
15
17
|
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util'
|
|
16
18
|
|
|
17
19
|
import type { JBrowsePluginMsaViewModel } from './model'
|
|
20
|
+
import type { MsaDataPayload } from './msaDataStore'
|
|
21
|
+
|
|
22
|
+
const EXPIRED_MESSAGE =
|
|
23
|
+
"This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared. Relaunch the alignment to rebuild it."
|
|
18
24
|
|
|
19
25
|
export function loadStoredData(self: JBrowsePluginMsaViewModel) {
|
|
20
26
|
const { dataStoreId, rows } = self
|
|
@@ -24,15 +30,27 @@ export function loadStoredData(self: JBrowsePluginMsaViewModel) {
|
|
|
24
30
|
self.setLoadingStoredData(true)
|
|
25
31
|
const storedData = await retrieveMsaData(dataStoreId)
|
|
26
32
|
if (storedData) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// one transaction, so storeDataToIndexedDB sees the restored data and
|
|
34
|
+
// the record of it together and does not write it straight back
|
|
35
|
+
transaction(() => {
|
|
36
|
+
if (storedData.msa) {
|
|
37
|
+
self.setMSA(storedData.msa)
|
|
38
|
+
}
|
|
39
|
+
if (storedData.tree) {
|
|
40
|
+
self.setTree(storedData.tree)
|
|
41
|
+
}
|
|
42
|
+
if (storedData.treeMetadata) {
|
|
43
|
+
self.setTreeMetadata(storedData.treeMetadata)
|
|
44
|
+
}
|
|
45
|
+
self.setLastStoredData(currentData(self))
|
|
46
|
+
})
|
|
47
|
+
} else {
|
|
48
|
+
// the id names nothing, so clearing it is what lets react-msaview's
|
|
49
|
+
// "Return to import form" actually return instead of landing back here
|
|
50
|
+
transaction(() => {
|
|
51
|
+
self.setDataStoreId(undefined)
|
|
52
|
+
self.setError(new Error(EXPIRED_MESSAGE))
|
|
53
|
+
})
|
|
36
54
|
}
|
|
37
55
|
} catch (e) {
|
|
38
56
|
console.error('Failed to load MSA data from IndexedDB:', e)
|
|
@@ -43,40 +61,68 @@ export function loadStoredData(self: JBrowsePluginMsaViewModel) {
|
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
const {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
-
}
|
|
64
|
+
function currentData(self: JBrowsePluginMsaViewModel): MsaDataPayload {
|
|
65
|
+
const { msa, tree, treeMetadata } = self.data
|
|
66
|
+
return { msa, tree, treeMetadata }
|
|
67
|
+
}
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
function sameData(a: MsaDataPayload | undefined, b: MsaDataPayload) {
|
|
70
|
+
return (
|
|
71
|
+
!!a &&
|
|
72
|
+
a.msa === b.msa &&
|
|
73
|
+
a.tree === b.tree &&
|
|
74
|
+
a.treeMetadata === b.treeMetadata
|
|
75
|
+
)
|
|
76
|
+
}
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Keep IndexedDB holding what the view holds. The first run writes a new row and
|
|
80
|
+
* records its id in the session snapshot; later runs update that row, because
|
|
81
|
+
* the alignment keeps changing after it arrives — react-msaview's
|
|
82
|
+
* "calculate neighbor-joining tree" replaces `data.tree` — and a session
|
|
83
|
+
* reopened against a stale row restores the wrong picture.
|
|
84
|
+
*
|
|
85
|
+
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
86
|
+
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
87
|
+
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
88
|
+
* IndexedDB (private mode) fails once rather than in a loop.
|
|
89
|
+
*
|
|
90
|
+
* A view whose data comes from a filehandle stores nothing at all: the file is
|
|
91
|
+
* the source of truth and react-msaview refetches it at startup.
|
|
92
|
+
*/
|
|
93
|
+
export function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel) {
|
|
94
|
+
const { rows, dataStoreId, isStoringData, lastStoredData } = self
|
|
95
|
+
const data = currentData(self)
|
|
96
|
+
if (
|
|
97
|
+
rows.length === 0 ||
|
|
98
|
+
isStoringData ||
|
|
99
|
+
self.msaFilehandle ||
|
|
100
|
+
self.treeFilehandle ||
|
|
101
|
+
!(data.msa || data.tree) ||
|
|
102
|
+
sameData(lastStoredData, data)
|
|
103
|
+
) {
|
|
104
|
+
return
|
|
79
105
|
}
|
|
106
|
+
|
|
107
|
+
// mark as storing synchronously so re-runs of this autorun (e.g. when data
|
|
108
|
+
// observables change while the write is pending) don't kick off a duplicate
|
|
109
|
+
// write and leave an orphan IndexedDB entry
|
|
110
|
+
self.setIsStoringData(true)
|
|
111
|
+
void (async () => {
|
|
112
|
+
try {
|
|
113
|
+
const id = dataStoreId ?? generateDataStoreId()
|
|
114
|
+
if (await storeMsaData(id, data)) {
|
|
115
|
+
self.setDataStoreId(id)
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('Failed to store MSA data to IndexedDB:', e)
|
|
119
|
+
} finally {
|
|
120
|
+
transaction(() => {
|
|
121
|
+
self.setLastStoredData(data)
|
|
122
|
+
self.setIsStoringData(false)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
})()
|
|
80
126
|
}
|
|
81
127
|
|
|
82
128
|
/**
|
|
@@ -88,39 +134,27 @@ export function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel) {
|
|
|
88
134
|
*/
|
|
89
135
|
export function launchOrthologsIfNeeded(self: JBrowsePluginMsaViewModel) {
|
|
90
136
|
if (self.orthologParams) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.setData(data)
|
|
137
|
+
runLaunch({
|
|
138
|
+
self,
|
|
139
|
+
message: 'Resolving orthologs',
|
|
140
|
+
launch: scope => doLaunchOrthologs({ self, scope }),
|
|
141
|
+
onLaunched: () => {
|
|
97
142
|
self.setOrthologParams(undefined)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
console.error(e)
|
|
101
|
-
} finally {
|
|
102
|
-
self.setProgress('')
|
|
103
|
-
}
|
|
104
|
-
})()
|
|
143
|
+
},
|
|
144
|
+
})
|
|
105
145
|
}
|
|
106
146
|
}
|
|
107
147
|
|
|
108
148
|
export function launchBlastIfNeeded(self: JBrowsePluginMsaViewModel) {
|
|
109
149
|
if (self.blastParams) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
self.setData(data)
|
|
150
|
+
runLaunch({
|
|
151
|
+
self,
|
|
152
|
+
message: 'Submitting query',
|
|
153
|
+
launch: scope => doLaunchBlast({ self, scope }),
|
|
154
|
+
onLaunched: () => {
|
|
116
155
|
self.setBlastParams(undefined)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
console.error(e)
|
|
120
|
-
} finally {
|
|
121
|
-
self.setProgress('')
|
|
122
|
-
}
|
|
123
|
-
})()
|
|
156
|
+
},
|
|
157
|
+
})
|
|
124
158
|
}
|
|
125
159
|
}
|
|
126
160
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
3
|
import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui'
|
|
4
|
-
import { Typography } from '@mui/material'
|
|
4
|
+
import { Button, Typography } from '@mui/material'
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
6
|
import { makeStyles } from 'tss-react/mui'
|
|
7
7
|
|
|
@@ -13,6 +13,11 @@ const useStyles = makeStyles()({
|
|
|
13
13
|
margin: {
|
|
14
14
|
padding: 20,
|
|
15
15
|
},
|
|
16
|
+
progressRow: {
|
|
17
|
+
display: 'flex',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
gap: 12,
|
|
20
|
+
},
|
|
16
21
|
})
|
|
17
22
|
|
|
18
23
|
/**
|
|
@@ -52,7 +57,20 @@ const LaunchProgress = observer(function LaunchProgress2({
|
|
|
52
57
|
<>
|
|
53
58
|
<LoadingEllipses message={message} variant="h5" />
|
|
54
59
|
{rid ? <JobLink jobId={rid} /> : null}
|
|
55
|
-
<
|
|
60
|
+
<div className={classes.progressRow}>
|
|
61
|
+
<Typography>{progress || 'Initializing'}</Typography>
|
|
62
|
+
{/* stops the polling and returns to the import form; the EBI job
|
|
63
|
+
itself runs on regardless, which is what its JobLink is for */}
|
|
64
|
+
<Button
|
|
65
|
+
variant="outlined"
|
|
66
|
+
size="small"
|
|
67
|
+
onClick={() => {
|
|
68
|
+
model.cancelLaunch()
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Cancel
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
56
74
|
</>
|
|
57
75
|
)}
|
|
58
76
|
</div>
|
|
@@ -70,6 +70,27 @@ test('a failed init shows why', () => {
|
|
|
70
70
|
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy()
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
// a launch runs for 10+ minutes, so leaving with no way out means watching it
|
|
74
|
+
test('a running launch offers a way out, and a failed one does not', () => {
|
|
75
|
+
const cancelLaunch = vi.fn()
|
|
76
|
+
panel({
|
|
77
|
+
blastParams: { proteinSequence: 'MKV' } as never,
|
|
78
|
+
progress: 'Re-checking BLAST status in... 7',
|
|
79
|
+
cancelLaunch,
|
|
80
|
+
})
|
|
81
|
+
screen.getByRole('button', { name: 'Cancel' }).click()
|
|
82
|
+
expect(cancelLaunch).toHaveBeenCalled()
|
|
83
|
+
|
|
84
|
+
cleanup()
|
|
85
|
+
panel({
|
|
86
|
+
blastParams: { proteinSequence: 'MKV' } as never,
|
|
87
|
+
progress: '',
|
|
88
|
+
error: new Error('No hits found'),
|
|
89
|
+
cancelLaunch,
|
|
90
|
+
})
|
|
91
|
+
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull()
|
|
92
|
+
})
|
|
93
|
+
|
|
73
94
|
test('a running job links out to it', () => {
|
|
74
95
|
panel({
|
|
75
96
|
blastParams: { proteinSequence: 'MKV' } as never,
|