jbrowse-plugin-msaview 2.8.2 → 2.9.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.
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastRIDPanel.d.ts → BlastQuery/BlastAutomaticPanel.d.ts} +2 -3
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.js → BlastQuery/BlastAutomaticPanel.js} +6 -25
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.d.ts → BlastQuery/BlastManualPanel.d.ts} +2 -3
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.js → BlastQuery/BlastManualPanel.js} +7 -4
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.d.ts → BlastQuery/BlastMethodSelector.d.ts} +2 -2
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.js → BlastQuery/BlastMethodSelector.js} +1 -2
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.d.ts → BlastQuery/BlastPanel.d.ts} +3 -12
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.js → BlastQuery/BlastPanel.js} +14 -16
- package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.d.ts +8 -0
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.js → BlastQuery/BlastSettingsDialog.js} +13 -9
- package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +17 -0
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +3 -3
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +1 -1
- package/dist/MsaViewPanel/components/JobLink.d.ts +5 -0
- package/dist/MsaViewPanel/components/JobLink.js +13 -0
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +1 -2
- package/dist/MsaViewPanel/components/LoadingBLAST.js +8 -15
- package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +11 -19
- package/dist/MsaViewPanel/doLaunchOrthologs.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +127 -0
- package/dist/MsaViewPanel/model.d.ts +1 -4
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +30 -30
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +8 -4
- package/dist/utils/blastCache.js +4 -5
- package/dist/utils/ebiBlast.d.ts +52 -0
- package/dist/utils/ebiBlast.js +63 -0
- package/dist/utils/ebiJobDispatcher.d.ts +33 -0
- package/dist/utils/ebiJobDispatcher.js +55 -0
- package/dist/utils/fetch.js +24 -1
- package/dist/utils/msa.d.ts +1 -1
- package/dist/utils/msa.js +25 -32
- package/dist/utils/types.d.ts +9 -14
- package/dist/utils/useLocalStorage.d.ts +1 -0
- package/dist/utils/useLocalStorage.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx → BlastQuery/BlastAutomaticPanel.tsx} +13 -59
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.tsx → BlastQuery/BlastManualPanel.tsx} +7 -6
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.tsx → BlastQuery/BlastMethodSelector.tsx} +2 -7
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.tsx → BlastQuery/BlastPanel.tsx} +21 -25
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.tsx → BlastQuery/BlastSettingsDialog.tsx} +26 -13
- package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +22 -0
- package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +5 -7
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +2 -2
- package/src/MsaViewPanel/components/JobLink.tsx +17 -0
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +10 -41
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
- package/src/MsaViewPanel/doLaunchBlast.ts +12 -28
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +159 -0
- package/src/MsaViewPanel/model.ts +1 -5
- package/src/utils/blastCache.ts +8 -9
- package/src/utils/ebiBlast.ts +114 -0
- package/src/utils/ebiJobDispatcher.ts +91 -0
- package/src/utils/fetch.ts +26 -1
- package/src/utils/msa.ts +26 -47
- package/src/utils/types.ts +8 -13
- package/src/utils/useLocalStorage.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.d.ts +0 -10
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.js +0 -74
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBISettingsDialog.d.ts +0 -5
- package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.d.ts +0 -7
- package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.js +0 -4
- package/dist/MsaViewPanel/components/RIDLink.d.ts +0 -6
- package/dist/MsaViewPanel/components/RIDLink.js +0 -12
- package/dist/utils/ncbiBlast.d.ts +0 -30
- package/dist/utils/ncbiBlast.js +0 -84
- package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.tsx +0 -137
- package/src/LaunchMsaView/components/NCBIBlastQuery/consts.ts +0 -10
- package/src/MsaViewPanel/components/RIDLink.tsx +0 -19
- package/src/utils/ncbiBlast.ts +0 -143
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.js +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.js +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.js +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.tsx +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.ts +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.ts +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
|
|
2
|
+
|
|
3
|
+
import type { BlastHit } from './types'
|
|
4
|
+
import type { BlastDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
|
|
5
|
+
|
|
6
|
+
const TOOL = 'ncbiblast'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The subset of EBI's ncbiblast JSON result this plugin reads. The service
|
|
10
|
+
* returns a great deal more per hit (urls, bit scores, e-values, the match
|
|
11
|
+
* string); only what the MSA rows are built from is typed here.
|
|
12
|
+
*/
|
|
13
|
+
interface EbiBlastJson {
|
|
14
|
+
hits?: {
|
|
15
|
+
hit_acc?: string
|
|
16
|
+
hit_id?: string
|
|
17
|
+
hit_desc?: string
|
|
18
|
+
/** the UniProt fields are absent on hits from non-UniProt databases */
|
|
19
|
+
hit_os?: string
|
|
20
|
+
hit_uni_de?: string
|
|
21
|
+
hit_uni_os?: string
|
|
22
|
+
/** NCBI taxon id, delivered as a string */
|
|
23
|
+
hit_uni_ox?: string
|
|
24
|
+
hit_hsps?: { hsp_hseq?: string }[]
|
|
25
|
+
}[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map EBI's hit shape onto the normalized one. Exported for testing against a
|
|
30
|
+
* captured response — the field names are the whole risk here, and nothing else
|
|
31
|
+
* in CI would notice if EBI renamed one.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeEbiBlastHits(result: EbiBlastJson): BlastHit[] {
|
|
34
|
+
return (result.hits ?? []).map(hit => {
|
|
35
|
+
const taxid = Number.parseInt(hit.hit_uni_ox ?? '', 10)
|
|
36
|
+
return {
|
|
37
|
+
description: [
|
|
38
|
+
{
|
|
39
|
+
accession: hit.hit_acc ?? 'unknown',
|
|
40
|
+
id: hit.hit_id ?? hit.hit_acc ?? 'unknown',
|
|
41
|
+
sciname: hit.hit_uni_os ?? hit.hit_os ?? 'unknown',
|
|
42
|
+
taxid: Number.isNaN(taxid) ? undefined : taxid,
|
|
43
|
+
// hit_uni_de is the bare protein name; hit_desc repeats it with the
|
|
44
|
+
// OS=/OX=/GN= suffix that makeId already covers with real columns
|
|
45
|
+
title: hit.hit_uni_de ?? hit.hit_desc,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
hsps: (hit.hit_hsps ?? []).flatMap(hsp =>
|
|
49
|
+
hsp.hsp_hseq ? [{ hseq: hsp.hsp_hseq }] : [],
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Human-facing link to a job, shown while it runs and on error — so it has to
|
|
57
|
+
* be EBI's own results UI, not the REST result endpoint, which does not exist
|
|
58
|
+
* yet at the moment the link is on screen.
|
|
59
|
+
*/
|
|
60
|
+
export function ebiBlastResultUrl(jobId: string) {
|
|
61
|
+
return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function queryEbiBlastFromJobId({
|
|
65
|
+
jobId,
|
|
66
|
+
onProgress,
|
|
67
|
+
}: {
|
|
68
|
+
jobId: string
|
|
69
|
+
onProgress: (arg: string) => void
|
|
70
|
+
}) {
|
|
71
|
+
onProgress(`Checking BLAST status for job: ${jobId}...`)
|
|
72
|
+
await waitForEbiJob({
|
|
73
|
+
tool: TOOL,
|
|
74
|
+
jobId,
|
|
75
|
+
onCountdown: s => {
|
|
76
|
+
onProgress(`Re-checking BLAST status in... ${s}`)
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const hits = normalizeEbiBlastHits(
|
|
81
|
+
JSON.parse(
|
|
82
|
+
await fetchEbiResult({ tool: TOOL, jobId, type: 'json' }),
|
|
83
|
+
) as EbiBlastJson,
|
|
84
|
+
)
|
|
85
|
+
if (hits.length === 0) {
|
|
86
|
+
throw new Error('No hits found')
|
|
87
|
+
}
|
|
88
|
+
return { rid: jobId, hits }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function queryEbiBlast({
|
|
92
|
+
query,
|
|
93
|
+
blastDatabase,
|
|
94
|
+
onProgress,
|
|
95
|
+
onRid,
|
|
96
|
+
}: {
|
|
97
|
+
query: string
|
|
98
|
+
blastDatabase: BlastDatabase
|
|
99
|
+
onProgress: (arg: string) => void
|
|
100
|
+
onRid: (arg: string) => void
|
|
101
|
+
}) {
|
|
102
|
+
onProgress('Submitting to EBI BLAST...')
|
|
103
|
+
const jobId = await submitEbiJob({
|
|
104
|
+
tool: TOOL,
|
|
105
|
+
params: {
|
|
106
|
+
program: 'blastp',
|
|
107
|
+
stype: 'protein',
|
|
108
|
+
database: blastDatabase,
|
|
109
|
+
sequence: query,
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
onRid(jobId)
|
|
113
|
+
return queryEbiBlastFromJobId({ jobId, onProgress })
|
|
114
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { textfetch } from './fetch'
|
|
2
|
+
import { pollLoop } from './poll'
|
|
3
|
+
import { readLocalStorage } from './useLocalStorage'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EBI's Job Dispatcher REST services (clustalo, muscle, ncbiblast, ...) all
|
|
7
|
+
* speak the same run/status/result protocol, so the transport lives here and
|
|
8
|
+
* each tool only supplies its own parameters and result types.
|
|
9
|
+
*
|
|
10
|
+
* Unlike NCBI's Blast.cgi these endpoints send `Access-Control-Allow-Origin: *`,
|
|
11
|
+
* which is why the BLAST backend moved here — see docs/blast.md.
|
|
12
|
+
*/
|
|
13
|
+
export const EBI_BASE = 'https://www.ebi.ac.uk/Tools/services/rest'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* EBI asks for a contact address on every submission so they can reach whoever
|
|
17
|
+
* is generating the load. A deployment that sends real volume should point this
|
|
18
|
+
* at its own maintainer via the BLAST settings dialog — otherwise every
|
|
19
|
+
* msaview job in the world is attributed to one person.
|
|
20
|
+
*/
|
|
21
|
+
export const EBI_EMAIL_STORAGE_KEY = 'msa-ebiContactEmail'
|
|
22
|
+
export const DEFAULT_EBI_EMAIL = 'colin.diesh@gmail.com'
|
|
23
|
+
|
|
24
|
+
export function getEbiEmail() {
|
|
25
|
+
const configured = readLocalStorage(
|
|
26
|
+
EBI_EMAIL_STORAGE_KEY,
|
|
27
|
+
DEFAULT_EBI_EMAIL,
|
|
28
|
+
).trim()
|
|
29
|
+
return configured || DEFAULT_EBI_EMAIL
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Statuses that mean the job is over and produced no result. */
|
|
33
|
+
const FAILED_STATUSES = new Set(['ERROR', 'FAILURE', 'NOT_FOUND'])
|
|
34
|
+
|
|
35
|
+
export async function submitEbiJob({
|
|
36
|
+
tool,
|
|
37
|
+
params,
|
|
38
|
+
}: {
|
|
39
|
+
tool: string
|
|
40
|
+
params: Record<string, string>
|
|
41
|
+
}) {
|
|
42
|
+
const jobId = await textfetch(`${EBI_BASE}/${tool}/run`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body: new URLSearchParams({ email: getEbiEmail(), ...params }),
|
|
45
|
+
})
|
|
46
|
+
return jobId.trim()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function waitForEbiJob({
|
|
50
|
+
tool,
|
|
51
|
+
jobId,
|
|
52
|
+
intervalSeconds = 10,
|
|
53
|
+
onCountdown,
|
|
54
|
+
}: {
|
|
55
|
+
tool: string
|
|
56
|
+
jobId: string
|
|
57
|
+
intervalSeconds?: number
|
|
58
|
+
onCountdown: (secondsRemaining: number) => void
|
|
59
|
+
}) {
|
|
60
|
+
await pollLoop({
|
|
61
|
+
intervalSeconds,
|
|
62
|
+
onCountdown,
|
|
63
|
+
check: async () => {
|
|
64
|
+
// exact match, not includes(): a job whose status is ERROR must not be
|
|
65
|
+
// able to poll forever waiting for a FINISHED that will never arrive
|
|
66
|
+
const status = (
|
|
67
|
+
await textfetch(`${EBI_BASE}/${tool}/status/${jobId}`)
|
|
68
|
+
).trim()
|
|
69
|
+
if (status === 'FINISHED') {
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
if (FAILED_STATUSES.has(status)) {
|
|
73
|
+
throw new Error(`EBI ${tool} job ${jobId} returned status ${status}`)
|
|
74
|
+
}
|
|
75
|
+
// RUNNING, QUEUED, PENDING and anything else EBI adds later
|
|
76
|
+
return false
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function fetchEbiResult({
|
|
82
|
+
tool,
|
|
83
|
+
jobId,
|
|
84
|
+
type,
|
|
85
|
+
}: {
|
|
86
|
+
tool: string
|
|
87
|
+
jobId: string
|
|
88
|
+
type: string
|
|
89
|
+
}) {
|
|
90
|
+
return textfetch(`${EBI_BASE}/${tool}/result/${jobId}/${type}`)
|
|
91
|
+
}
|
package/src/utils/fetch.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
function hostOf(url: string) {
|
|
2
|
+
try {
|
|
3
|
+
return new URL(url).host
|
|
4
|
+
} catch {
|
|
5
|
+
// a relative url, e.g. a same-origin proxy path
|
|
6
|
+
return url
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
export async function handleFetch(url: string, args?: RequestInit) {
|
|
2
|
-
|
|
11
|
+
let response: Response
|
|
12
|
+
try {
|
|
13
|
+
response = await fetch(url, args)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// fetch rejects with a bare "TypeError: Failed to fetch" for every network
|
|
16
|
+
// level failure, including a cross-origin response the browser refused to
|
|
17
|
+
// hand over. That is not a hypothetical: NCBI's Blast.cgi stopped sending
|
|
18
|
+
// Access-Control-Allow-Origin to third-party origins, and the raw TypeError
|
|
19
|
+
// told users nothing at all about why. See docs/blast.md.
|
|
20
|
+
if (e instanceof TypeError) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Could not reach ${hostOf(url)} — the request failed at the network level. This is usually a CORS restriction (the server refused to let this site read the response), an offline connection, or a blocked request.`,
|
|
23
|
+
{ cause: e },
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
throw e
|
|
27
|
+
}
|
|
3
28
|
|
|
4
29
|
if (!response.ok) {
|
|
5
30
|
throw new Error(
|
package/src/utils/msa.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { pollLoop } from './poll'
|
|
1
|
+
import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
|
|
3
2
|
|
|
4
|
-
import type { MsaAlgorithm } from '../LaunchMsaView/components/
|
|
5
|
-
|
|
6
|
-
const base = `https://www.ebi.ac.uk/Tools/services/rest`
|
|
7
|
-
const email = 'colin.diesh@gmail.com'
|
|
3
|
+
import type { MsaAlgorithm } from '../LaunchMsaView/components/BlastQuery/consts'
|
|
8
4
|
|
|
9
5
|
const algorithms: Record<
|
|
10
6
|
MsaAlgorithm,
|
|
@@ -15,54 +11,27 @@ const algorithms: Record<
|
|
|
15
11
|
}
|
|
16
12
|
> = {
|
|
17
13
|
clustalo: {
|
|
18
|
-
params: {
|
|
14
|
+
params: {},
|
|
19
15
|
msaResult: 'aln-clustal_num',
|
|
20
16
|
treeResult: 'phylotree',
|
|
21
17
|
},
|
|
22
18
|
muscle: {
|
|
23
|
-
params: {
|
|
19
|
+
params: { format: 'clw', tree: 'tree1' },
|
|
24
20
|
msaResult: 'fa',
|
|
25
21
|
treeResult: 'phylotree',
|
|
26
22
|
},
|
|
27
23
|
kalign: {
|
|
28
|
-
params: {
|
|
24
|
+
params: { stype: 'protein' },
|
|
29
25
|
msaResult: 'fa',
|
|
30
26
|
treeResult: 'phylotree',
|
|
31
27
|
},
|
|
32
28
|
mafft: {
|
|
33
|
-
params: {
|
|
29
|
+
params: { stype: 'protein' },
|
|
34
30
|
msaResult: 'fa',
|
|
35
31
|
treeResult: 'phylotree',
|
|
36
32
|
},
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
async function wait({
|
|
40
|
-
onProgress,
|
|
41
|
-
jobId,
|
|
42
|
-
algorithm,
|
|
43
|
-
}: {
|
|
44
|
-
jobId: string
|
|
45
|
-
algorithm: MsaAlgorithm
|
|
46
|
-
onProgress: (arg: string) => void
|
|
47
|
-
}) {
|
|
48
|
-
await pollLoop({
|
|
49
|
-
intervalSeconds: 10,
|
|
50
|
-
onCountdown: s => {
|
|
51
|
-
onProgress(`Re-checking MSA status in... ${s}`)
|
|
52
|
-
},
|
|
53
|
-
check: async () => {
|
|
54
|
-
const result = await textfetch(`${base}/${algorithm}/status/${jobId}`)
|
|
55
|
-
if (result.includes('FINISHED')) {
|
|
56
|
-
return true
|
|
57
|
-
}
|
|
58
|
-
if (result.includes('FAILURE')) {
|
|
59
|
-
throw new Error(`Failed to run: jobId ${jobId}`)
|
|
60
|
-
}
|
|
61
|
-
return false
|
|
62
|
-
},
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
35
|
export async function launchMSA({
|
|
67
36
|
algorithm,
|
|
68
37
|
sequence,
|
|
@@ -76,17 +45,27 @@ export async function launchMSA({
|
|
|
76
45
|
|
|
77
46
|
onProgress(`Launching ${algorithm} MSA...`)
|
|
78
47
|
|
|
79
|
-
const jobId = await
|
|
80
|
-
|
|
81
|
-
|
|
48
|
+
const jobId = await submitEbiJob({
|
|
49
|
+
tool: algorithm,
|
|
50
|
+
params: { ...config.params, sequence },
|
|
51
|
+
})
|
|
52
|
+
await waitForEbiJob({
|
|
53
|
+
tool: algorithm,
|
|
54
|
+
jobId,
|
|
55
|
+
onCountdown: s => {
|
|
56
|
+
onProgress(`Re-checking MSA status in... ${s}`)
|
|
57
|
+
},
|
|
82
58
|
})
|
|
83
|
-
await wait({ jobId, algorithm, onProgress })
|
|
84
59
|
return {
|
|
85
|
-
msa: await
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
60
|
+
msa: await fetchEbiResult({
|
|
61
|
+
tool: algorithm,
|
|
62
|
+
jobId,
|
|
63
|
+
type: config.msaResult,
|
|
64
|
+
}),
|
|
65
|
+
tree: await fetchEbiResult({
|
|
66
|
+
tool: algorithm,
|
|
67
|
+
jobId,
|
|
68
|
+
type: config.treeResult,
|
|
69
|
+
}),
|
|
91
70
|
}
|
|
92
71
|
}
|
package/src/utils/types.ts
CHANGED
|
@@ -6,17 +6,12 @@ export interface BlastHitDescription {
|
|
|
6
6
|
title?: string
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}[]
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}[]
|
|
9
|
+
/**
|
|
10
|
+
* The shape utils/ebiBlast.ts normalizes EBI's hits into. It keeps the field
|
|
11
|
+
* names NCBI's JSON2 used, so everything downstream of the search — row naming,
|
|
12
|
+
* taxonomy lookup, the MSA — was left untouched when the backend moved.
|
|
13
|
+
*/
|
|
14
|
+
export interface BlastHit {
|
|
15
|
+
description: BlastHitDescription[]
|
|
16
|
+
hsps: { hseq: string }[]
|
|
22
17
|
}
|
|
@@ -5,7 +5,7 @@ import { useState } from 'react'
|
|
|
5
5
|
// throw "(0, PR.useLocalStorage) is not a function" on hosts built during that
|
|
6
6
|
// window. Same failure mode as `defaultCodonTable`; keeping our own copy takes
|
|
7
7
|
// this plugin out of the whack-a-mole.
|
|
8
|
-
function readLocalStorage<T>(key: string, initialValue: T): T {
|
|
8
|
+
export function readLocalStorage<T>(key: string, initialValue: T): T {
|
|
9
9
|
try {
|
|
10
10
|
const item = globalThis.localStorage.getItem(key)
|
|
11
11
|
return item === null ? initialValue : (JSON.parse(item) as T)
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.9.0'
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util';
|
|
3
|
-
declare const NCBIBlastAutomaticPanel: ({ handleClose, feature, model, children, baseUrl, }: {
|
|
4
|
-
model: AbstractTrackModel;
|
|
5
|
-
feature: Feature;
|
|
6
|
-
baseUrl: string;
|
|
7
|
-
handleClose: () => void;
|
|
8
|
-
children: React.ReactNode;
|
|
9
|
-
}) => React.JSX.Element;
|
|
10
|
-
export default NCBIBlastAutomaticPanel;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Typography } from '@mui/material';
|
|
3
|
-
import { observer } from 'mobx-react';
|
|
4
|
-
import { makeStyles } from 'tss-react/mui';
|
|
5
|
-
import MsaAlgorithmSelect from './MsaAlgorithmSelect';
|
|
6
|
-
import { blastLaunchView } from './blastLaunchView';
|
|
7
|
-
import ExternalLink from '../../../components/ExternalLink';
|
|
8
|
-
import TextField2 from '../../../components/TextField2';
|
|
9
|
-
import { getBlastViewTitle, getLinearGenomeView } from '../../util';
|
|
10
|
-
import LaunchPanelContent from '../LaunchPanelContent';
|
|
11
|
-
import SubmitCancelActions from '../SubmitCancelActions';
|
|
12
|
-
import TranscriptSelector from '../TranscriptSelector';
|
|
13
|
-
import { useTranscriptSelection } from '../useTranscriptSelection';
|
|
14
|
-
const useStyles = makeStyles()({
|
|
15
|
-
marginBottom: {
|
|
16
|
-
marginBottom: 16,
|
|
17
|
-
},
|
|
18
|
-
ridField: {
|
|
19
|
-
width: 150,
|
|
20
|
-
},
|
|
21
|
-
infoText: {
|
|
22
|
-
marginTop: 20,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
const NCBIBlastRIDPanel = observer(function ({ handleClose, feature, model, children, baseUrl, }) {
|
|
26
|
-
const { classes } = useStyles();
|
|
27
|
-
const view = getLinearGenomeView(model);
|
|
28
|
-
const [launchViewError, setLaunchViewError] = useState();
|
|
29
|
-
const [rid, setRid] = useState('');
|
|
30
|
-
const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] = useState('clustalo');
|
|
31
|
-
const transcriptSelection = useTranscriptSelection({ feature, view });
|
|
32
|
-
const { selectedTranscript, proteinSequence } = transcriptSelection;
|
|
33
|
-
const e = transcriptSelection.error ?? launchViewError;
|
|
34
|
-
const trimmedRid = rid.trim();
|
|
35
|
-
return (React.createElement(React.Fragment, null,
|
|
36
|
-
React.createElement(LaunchPanelContent, { error: e },
|
|
37
|
-
children,
|
|
38
|
-
React.createElement(Typography, { variant: "body2", className: classes.marginBottom }, "Enter the RID (Request ID) from a previously submitted NCBI BLAST query. You can find the RID in the BLAST results page URL or at the top of the results page. RIDs are typically valid for 24-36 hours after submission."),
|
|
39
|
-
React.createElement(TextField2, { variant: "outlined", label: "BLAST RID", placeholder: "e.g., ABC12345", fullWidth: true, className: classes.marginBottom, value: rid, onChange: event => {
|
|
40
|
-
setRid(event.target.value);
|
|
41
|
-
} }),
|
|
42
|
-
trimmedRid ? (React.createElement(Typography, { variant: "body2", className: classes.marginBottom },
|
|
43
|
-
React.createElement(ExternalLink, { href: `${baseUrl}?CMD=Get&RID=${trimmedRid}` }, "View BLAST results on NCBI"))) : null,
|
|
44
|
-
React.createElement(MsaAlgorithmSelect, { className: classes.ridField, value: selectedMsaAlgorithm, onChange: setSelectedMsaAlgorithm }),
|
|
45
|
-
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
|
|
46
|
-
React.createElement(Typography, { className: classes.infoText }, "This will fetch the BLAST results for the provided RID and run them through a multiple sequence alignment. The protein sequence from the selected transcript will be added as the query sequence in the MSA.")),
|
|
47
|
-
React.createElement(SubmitCancelActions, { submitDisabled: !proteinSequence || !trimmedRid, onSubmit: () => {
|
|
48
|
-
try {
|
|
49
|
-
if (selectedTranscript && trimmedRid) {
|
|
50
|
-
setLaunchViewError(undefined);
|
|
51
|
-
blastLaunchView({
|
|
52
|
-
feature: selectedTranscript,
|
|
53
|
-
view,
|
|
54
|
-
newViewTitle: getBlastViewTitle(feature, selectedTranscript),
|
|
55
|
-
blastParams: {
|
|
56
|
-
baseUrl,
|
|
57
|
-
blastProgram: 'blastp',
|
|
58
|
-
blastDatabase: 'nr',
|
|
59
|
-
msaAlgorithm: selectedMsaAlgorithm,
|
|
60
|
-
selectedTranscript,
|
|
61
|
-
proteinSequence,
|
|
62
|
-
rid: trimmedRid,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
handleClose();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
catch (e) {
|
|
69
|
-
console.error(e);
|
|
70
|
-
setLaunchViewError(e);
|
|
71
|
-
}
|
|
72
|
-
}, onCancel: handleClose })));
|
|
73
|
-
});
|
|
74
|
-
export default NCBIBlastRIDPanel;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare const BASE_BLAST_URL = "https://blast.ncbi.nlm.nih.gov/Blast.cgi";
|
|
2
|
-
export declare const msaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft"];
|
|
3
|
-
export type MsaAlgorithm = (typeof msaAlgorithms)[number];
|
|
4
|
-
export declare const blastDatabaseOptions: readonly ["nr", "nr_cluster_seq"];
|
|
5
|
-
export type BlastDatabase = (typeof blastDatabaseOptions)[number];
|
|
6
|
-
export declare const blastPrograms: readonly ["blastp", "quick-blastp"];
|
|
7
|
-
export type BlastProgram = (typeof blastPrograms)[number];
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Typography } from '@mui/material';
|
|
3
|
-
import ExternalLink from '../../components/ExternalLink';
|
|
4
|
-
function RIDLink({ baseUrl, rid }) {
|
|
5
|
-
return (React.createElement(Typography, null,
|
|
6
|
-
"RID ",
|
|
7
|
-
rid,
|
|
8
|
-
" (",
|
|
9
|
-
React.createElement(ExternalLink, { href: `${baseUrl}?CMD=Get&RID=${rid}` }, "see status"),
|
|
10
|
-
")"));
|
|
11
|
-
}
|
|
12
|
-
export default RIDLink;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { BlastDatabase, BlastProgram } from '../LaunchMsaView/components/NCBIBlastQuery/consts';
|
|
2
|
-
export declare function queryBlastFromRid({ rid, baseUrl, onProgress, }: {
|
|
3
|
-
rid: string;
|
|
4
|
-
baseUrl: string;
|
|
5
|
-
onProgress: (arg: string) => void;
|
|
6
|
-
}): Promise<{
|
|
7
|
-
rid: string;
|
|
8
|
-
hits: {
|
|
9
|
-
description: import("./types").BlastHitDescription[];
|
|
10
|
-
hsps: {
|
|
11
|
-
hseq: string;
|
|
12
|
-
}[];
|
|
13
|
-
}[];
|
|
14
|
-
}>;
|
|
15
|
-
export declare function queryBlast({ query, blastDatabase, blastProgram, baseUrl, onProgress, onRid, }: {
|
|
16
|
-
query: string;
|
|
17
|
-
blastDatabase: BlastDatabase;
|
|
18
|
-
blastProgram: BlastProgram;
|
|
19
|
-
baseUrl: string;
|
|
20
|
-
onProgress: (arg: string) => void;
|
|
21
|
-
onRid: (arg: string) => void;
|
|
22
|
-
}): Promise<{
|
|
23
|
-
rid: string;
|
|
24
|
-
hits: {
|
|
25
|
-
description: import("./types").BlastHitDescription[];
|
|
26
|
-
hsps: {
|
|
27
|
-
hseq: string;
|
|
28
|
-
}[];
|
|
29
|
-
}[];
|
|
30
|
-
}>;
|
package/dist/utils/ncbiBlast.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { jsonfetch, textfetch } from './fetch';
|
|
2
|
-
import { pollLoop } from './poll';
|
|
3
|
-
export async function queryBlastFromRid({ rid, baseUrl, onProgress, }) {
|
|
4
|
-
onProgress(`Checking BLAST status for RID: ${rid}...`);
|
|
5
|
-
await waitForRid({
|
|
6
|
-
rid,
|
|
7
|
-
onProgress,
|
|
8
|
-
baseUrl,
|
|
9
|
-
});
|
|
10
|
-
const ret = await jsonfetch(`${baseUrl}?CMD=Get&RID=${rid}&FORMAT_TYPE=JSON2_S&FORMAT_OBJECT=Alignment`);
|
|
11
|
-
return {
|
|
12
|
-
rid,
|
|
13
|
-
hits: ret.BlastOutput2[0]?.report.results.search.hits ?? [],
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
export async function queryBlast({ query, blastDatabase, blastProgram, baseUrl, onProgress, onRid, }) {
|
|
17
|
-
onProgress('Submitting to NCBI BLAST...');
|
|
18
|
-
const { rid } = await initialQuery({
|
|
19
|
-
query,
|
|
20
|
-
blastDatabase,
|
|
21
|
-
blastProgram,
|
|
22
|
-
baseUrl,
|
|
23
|
-
});
|
|
24
|
-
onRid(rid);
|
|
25
|
-
return queryBlastFromRid({ rid, baseUrl, onProgress });
|
|
26
|
-
}
|
|
27
|
-
async function initialQuery({ query, blastProgram, blastDatabase, baseUrl, }) {
|
|
28
|
-
const res = await textfetch(baseUrl, {
|
|
29
|
-
method: 'POST',
|
|
30
|
-
body: new URLSearchParams({
|
|
31
|
-
CMD: 'Put',
|
|
32
|
-
PROGRAM: blastProgram === 'quick-blastp' ? 'blastp' : blastProgram,
|
|
33
|
-
DATABASE: blastDatabase,
|
|
34
|
-
QUERY: query,
|
|
35
|
-
...(blastDatabase === 'nr_cluster_seq'
|
|
36
|
-
? {
|
|
37
|
-
CLUSTERED_DB: 'on',
|
|
38
|
-
DB_TYPE: 'Experimental Databases',
|
|
39
|
-
}
|
|
40
|
-
: {}),
|
|
41
|
-
...(blastProgram === 'quick-blastp'
|
|
42
|
-
? { BLAST_PROGRAMS: 'kmerBlastp' }
|
|
43
|
-
: {}),
|
|
44
|
-
}),
|
|
45
|
-
});
|
|
46
|
-
// the initial submission/query to the BLAST "REST API" does not return JSON
|
|
47
|
-
// as a response (e.g. FORMAT_TYPE=JSON does not work), so the RID is
|
|
48
|
-
// literally parsed from the text of the HTML that is returned
|
|
49
|
-
const rid = /^ {4}RID = (.*$)/m.exec(res)?.[1];
|
|
50
|
-
const rtoe = /^ {4}RTOE = (.*$)/m.exec(res)?.[1];
|
|
51
|
-
if (!rid) {
|
|
52
|
-
throw new Error('Failed to get RID from BLAST request');
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
rid,
|
|
56
|
-
rtoe,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
async function waitForRid({ rid, onProgress, baseUrl, }) {
|
|
60
|
-
await pollLoop({
|
|
61
|
-
intervalSeconds: 20,
|
|
62
|
-
onCountdown: s => {
|
|
63
|
-
onProgress(`Re-checking BLAST status in... ${s}`);
|
|
64
|
-
},
|
|
65
|
-
check: async () => {
|
|
66
|
-
const res = await textfetch(`${baseUrl}?CMD=Get&FORMAT_OBJECT=SearchInfo&RID=${rid}`);
|
|
67
|
-
const status = /\s+Status=(\S+)/m.exec(res)?.[1];
|
|
68
|
-
const hasHits = /\s+ThereAreHits=yes/m.test(res);
|
|
69
|
-
if (status === 'WAITING') {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
if (status === 'FAILED') {
|
|
73
|
-
throw new Error(`BLAST ${rid} failed`);
|
|
74
|
-
}
|
|
75
|
-
if (status === 'READY') {
|
|
76
|
-
if (hasHits) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
throw new Error('No hits found');
|
|
80
|
-
}
|
|
81
|
-
throw new Error(`BLAST ${rid} returned unexpected status: ${status ?? 'unknown'}`);
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
}
|