jbrowse-plugin-msaview 3.3.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/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
- package/dist/AddHighlightModel/index.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +60 -18
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
- 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/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +5 -4
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
- package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
- package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
- package/dist/LaunchMsaView/detectQueryRow.js +20 -21
- package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
- package/dist/LaunchMsaView/useQueryRowName.js +5 -8
- 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 +95 -72
- package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
- package/dist/MsaViewPanel/components/JobLink.js +7 -1
- package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
- package/dist/MsaViewPanel/components/LaunchProgress.js +50 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
- package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +87 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +4 -2
- package/dist/MsaViewPanel/doLaunchBlast.js +86 -56
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +39 -25
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +190 -19
- 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/observeProteinHighlights.test.js +11 -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/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
- package/dist/MsaViewPanel/util.d.ts +18 -0
- package/dist/MsaViewPanel/util.js +17 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +10 -6
- package/dist/utils/blastCache.js +47 -3
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/ebiBlast.d.ts +5 -3
- 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 +15 -1
- package/dist/utils/msa.js +40 -13
- package/dist/utils/msaRows.d.ts +31 -0
- package/dist/utils/msaRows.js +67 -0
- 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 +54 -0
- package/dist/utils/phmmer.js +120 -0
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/utils/taxonomyNames.d.ts +1 -1
- package/dist/utils/taxonomyNames.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +30 -24
- package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
- package/src/AddHighlightModel/index.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +85 -31
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +7 -7
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
- package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
- package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
- package/src/LaunchMsaView/detectQueryRow.ts +34 -23
- package/src/LaunchMsaView/useQueryRowName.ts +6 -9
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +102 -68
- package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
- package/src/MsaViewPanel/components/JobLink.tsx +7 -2
- package/src/MsaViewPanel/components/LaunchProgress.tsx +80 -0
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +104 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
- package/src/MsaViewPanel/doLaunchBlast.ts +134 -72
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +43 -28
- package/src/MsaViewPanel/doLaunchOrthologs.ts +9 -5
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +82 -6
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +22 -13
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
- 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/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
- package/src/MsaViewPanel/util.ts +18 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +67 -13
- package/src/utils/ebiBlast.ts +9 -3
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/msa.ts +51 -12
- package/src/utils/msaRows.ts +95 -0
- 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 +178 -0
- package/src/utils/poll.ts +8 -1
- package/src/utils/taxonomyNames.ts +6 -1
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
- package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { findQueryRow } from './detectQueryRow';
|
|
3
3
|
const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD';
|
|
4
4
|
// COBALT renames the query `Query_1`, so only the residues identify it
|
|
5
5
|
const clustal = `CLUSTAL W (1.81) multiple sequence alignment
|
|
@@ -13,22 +13,22 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
|
13
13
|
>sp|P02769|ALBU_BOVIN
|
|
14
14
|
MKWVTFISLLLLFSSAYSRG--RRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPYD
|
|
15
15
|
`;
|
|
16
|
-
describe('
|
|
16
|
+
describe('findQueryRow', () => {
|
|
17
17
|
test('finds the query by sequence when the aligner renamed it', () => {
|
|
18
|
-
expect(
|
|
18
|
+
expect(findQueryRow(clustal, protein).match).toMatchObject({
|
|
19
19
|
name: 'Query_1',
|
|
20
20
|
quality: 'exact',
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
test('ignores gaps in the aligned row', () => {
|
|
24
|
-
expect(
|
|
24
|
+
expect(findQueryRow(fasta, protein).match?.name).toBe('Query_1');
|
|
25
25
|
});
|
|
26
26
|
test('tolerates the trailing stop codon the translation carries', () => {
|
|
27
|
-
expect(
|
|
27
|
+
expect(findQueryRow(clustal, `${protein}*`).match?.name).toBe('Query_1');
|
|
28
28
|
});
|
|
29
29
|
test('matches a row that is the query trimmed to the aligned region', () => {
|
|
30
30
|
const trimmed = `>hit_one\nWRONGWRONGWRONGWRONG\n>aligned_query\n${protein.slice(5, 40)}\n`;
|
|
31
|
-
expect(
|
|
31
|
+
expect(findQueryRow(trimmed, protein).match).toMatchObject({
|
|
32
32
|
name: 'aligned_query',
|
|
33
33
|
quality: 'partial',
|
|
34
34
|
});
|
|
@@ -41,25 +41,25 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
|
41
41
|
>hit_two
|
|
42
42
|
MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
43
43
|
`;
|
|
44
|
-
expect(
|
|
44
|
+
expect(findQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW').match).toBeUndefined();
|
|
45
45
|
});
|
|
46
46
|
test('returns nothing rather than throwing on a half-pasted alignment', () => {
|
|
47
|
-
expect(
|
|
48
|
-
expect(
|
|
49
|
-
expect(
|
|
50
|
-
expect(
|
|
47
|
+
expect(findQueryRow('>partial\nMKWV', protein).match).toBeUndefined();
|
|
48
|
+
expect(findQueryRow('not an alignment at all', protein).match).toBeUndefined();
|
|
49
|
+
expect(findQueryRow('', protein).match).toBeUndefined();
|
|
50
|
+
expect(findQueryRow(clustal, '').match).toBeUndefined();
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
-
describe('
|
|
53
|
+
describe('findQueryRow row names', () => {
|
|
54
54
|
test('lists the rows for the override dropdown', () => {
|
|
55
|
-
expect(
|
|
55
|
+
expect(findQueryRow(clustal, protein).names).toEqual([
|
|
56
56
|
'Query_1',
|
|
57
57
|
'sp|P02769|ALBU',
|
|
58
58
|
'sp|Q5XLE4|OTHE',
|
|
59
59
|
]);
|
|
60
60
|
});
|
|
61
61
|
test('is empty rather than throwing while the user is still pasting', () => {
|
|
62
|
-
expect(
|
|
63
|
-
expect(
|
|
62
|
+
expect(findQueryRow('CLUSTAL W', protein).names).toEqual([]);
|
|
63
|
+
expect(findQueryRow('', protein).names).toEqual([]);
|
|
64
64
|
});
|
|
65
65
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { findQueryRow } from './detectQueryRow';
|
|
3
3
|
/**
|
|
4
4
|
* The MSA row name to launch with, found by sequence rather than typed.
|
|
5
5
|
*
|
|
@@ -12,15 +12,12 @@ export function useQueryRowName(msaText, proteinSequence) {
|
|
|
12
12
|
const [override, setOverride] = useState();
|
|
13
13
|
// parsing runs on every keystroke in the paste box otherwise, and an
|
|
14
14
|
// alignment of a few hundred rows is not free
|
|
15
|
-
const {
|
|
16
|
-
detected: detectQueryRow(msaText, proteinSequence),
|
|
17
|
-
names: getMsaRowNames(msaText),
|
|
18
|
-
}), [msaText, proteinSequence]);
|
|
15
|
+
const { names, match } = useMemo(() => findQueryRow(msaText, proteinSequence), [msaText, proteinSequence]);
|
|
19
16
|
return {
|
|
20
|
-
detected,
|
|
17
|
+
detected: match,
|
|
21
18
|
names,
|
|
22
|
-
querySeqName: override ??
|
|
19
|
+
querySeqName: override ?? match?.name ?? '',
|
|
23
20
|
setQuerySeqName: setOverride,
|
|
24
|
-
isAutoDetected: override === undefined && !!
|
|
21
|
+
isAutoDetected: override === undefined && !!match,
|
|
25
22
|
};
|
|
26
23
|
}
|
|
@@ -15,6 +15,16 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
|
15
15
|
// directly, and so is orthologParams (the model's own autorun picks it up).
|
|
16
16
|
// Only sources needing launch-time resolution go through `init`: msaUrl
|
|
17
17
|
// (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
|
|
18
|
+
//
|
|
19
|
+
// An init whose every field is undefined is still a truthy object, and
|
|
20
|
+
// MsaViewPanel reads any init as a launch in flight, so an inline-data
|
|
21
|
+
// launch flashed "Loading alignment" until processInit cleared it.
|
|
22
|
+
const init = {
|
|
23
|
+
msaUrl: msaFileLocation?.uri,
|
|
24
|
+
msaIndexedLocation,
|
|
25
|
+
msaName,
|
|
26
|
+
querySeqName,
|
|
27
|
+
};
|
|
18
28
|
launchMsaView(session, {
|
|
19
29
|
...rest,
|
|
20
30
|
data,
|
|
@@ -26,12 +36,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
|
26
36
|
},
|
|
27
37
|
}
|
|
28
38
|
: {}),
|
|
29
|
-
init: {
|
|
30
|
-
msaUrl: msaFileLocation?.uri,
|
|
31
|
-
msaIndexedLocation,
|
|
32
|
-
msaName,
|
|
33
|
-
querySeqName,
|
|
34
|
-
},
|
|
39
|
+
...(Object.values(init).some(v => v !== undefined) ? { init } : {}),
|
|
35
40
|
});
|
|
36
41
|
return args;
|
|
37
42
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import LaunchMsaViewExtensionPointF from './index';
|
|
3
|
+
function launch(args) {
|
|
4
|
+
let run;
|
|
5
|
+
LaunchMsaViewExtensionPointF({
|
|
6
|
+
addToExtensionPoint(_name, cb) {
|
|
7
|
+
run = cb;
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
const added = [];
|
|
11
|
+
const session = {
|
|
12
|
+
addView(_type, snapshot) {
|
|
13
|
+
added.push(snapshot);
|
|
14
|
+
return { id: 'view-1' };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
run({ session, ...args });
|
|
18
|
+
return added[0];
|
|
19
|
+
}
|
|
20
|
+
test('inline data carries no init, which the panel would read as a launch in flight', () => {
|
|
21
|
+
const snapshot = launch({ data: { msa: '>a\nMEEP' } });
|
|
22
|
+
expect('init' in snapshot).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
test('a file location travels through init', () => {
|
|
25
|
+
const snapshot = launch({
|
|
26
|
+
msaFileLocation: { uri: 'http://example.com/a.fa' },
|
|
27
|
+
});
|
|
28
|
+
expect(snapshot.init).toEqual({
|
|
29
|
+
msaUrl: 'http://example.com/a.fa',
|
|
30
|
+
msaIndexedLocation: undefined,
|
|
31
|
+
msaName: undefined,
|
|
32
|
+
querySeqName: undefined,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
test('one field set is enough to need init', () => {
|
|
36
|
+
const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
|
|
37
|
+
expect(snapshot.init).toEqual({
|
|
38
|
+
msaUrl: undefined,
|
|
39
|
+
msaIndexedLocation: undefined,
|
|
40
|
+
msaName: undefined,
|
|
41
|
+
querySeqName: 'QUERY',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
2
|
export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
|
|
3
|
+
/**
|
|
4
|
+
* Keep IndexedDB holding what the view holds. The first run writes a new row and
|
|
5
|
+
* records its id in the session snapshot; later runs update that row, because
|
|
6
|
+
* the alignment keeps changing after it arrives — react-msaview's
|
|
7
|
+
* "calculate neighbor-joining tree" replaces `data.tree` — and a session
|
|
8
|
+
* reopened against a stale row restores the wrong picture.
|
|
9
|
+
*
|
|
10
|
+
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
11
|
+
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
12
|
+
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
13
|
+
* IndexedDB (private mode) fails once rather than in a loop.
|
|
14
|
+
*
|
|
15
|
+
* A view whose data comes from a filehandle stores nothing at all: the file is
|
|
16
|
+
* the source of truth and react-msaview refetches it at startup.
|
|
17
|
+
*/
|
|
3
18
|
export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
|
|
4
19
|
/**
|
|
5
20
|
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { transaction } from 'mobx';
|
|
2
3
|
import { doLaunchBlast } from './doLaunchBlast';
|
|
3
4
|
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
4
5
|
import { fetchIndexedMsa } from './fetchIndexedMsa';
|
|
5
6
|
import { genomeToMSA } from './genomeToMSA';
|
|
6
7
|
import { loadProteinDomains } from './loadProteinDomains';
|
|
7
8
|
import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
|
|
9
|
+
import { runLaunch } from './runLaunch';
|
|
8
10
|
import { getProteinViews } from './structureConnection';
|
|
9
|
-
import { getUniprotIdFromAlphaFoldUrl } from './util';
|
|
11
|
+
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
|
|
12
|
+
const EXPIRED_MESSAGE = "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.";
|
|
10
13
|
export function loadStoredData(self) {
|
|
11
14
|
const { dataStoreId, rows } = self;
|
|
12
15
|
if (dataStoreId && rows.length === 0) {
|
|
@@ -15,15 +18,28 @@ export function loadStoredData(self) {
|
|
|
15
18
|
self.setLoadingStoredData(true);
|
|
16
19
|
const storedData = await retrieveMsaData(dataStoreId);
|
|
17
20
|
if (storedData) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
// one transaction, so storeDataToIndexedDB sees the restored data and
|
|
22
|
+
// the record of it together and does not write it straight back
|
|
23
|
+
transaction(() => {
|
|
24
|
+
if (storedData.msa) {
|
|
25
|
+
self.setMSA(storedData.msa);
|
|
26
|
+
}
|
|
27
|
+
if (storedData.tree) {
|
|
28
|
+
self.setTree(storedData.tree);
|
|
29
|
+
}
|
|
30
|
+
if (storedData.treeMetadata) {
|
|
31
|
+
self.setTreeMetadata(storedData.treeMetadata);
|
|
32
|
+
}
|
|
33
|
+
self.setLastStoredData(currentData(self));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// the id names nothing, so clearing it is what lets react-msaview's
|
|
38
|
+
// "Return to import form" actually return instead of landing back here
|
|
39
|
+
transaction(() => {
|
|
40
|
+
self.setDataStoreId(undefined);
|
|
41
|
+
self.setError(new Error(EXPIRED_MESSAGE));
|
|
42
|
+
});
|
|
27
43
|
}
|
|
28
44
|
}
|
|
29
45
|
catch (e) {
|
|
@@ -35,40 +51,63 @@ export function loadStoredData(self) {
|
|
|
35
51
|
})();
|
|
36
52
|
}
|
|
37
53
|
}
|
|
54
|
+
function currentData(self) {
|
|
55
|
+
const { msa, tree, treeMetadata } = self.data;
|
|
56
|
+
return { msa, tree, treeMetadata };
|
|
57
|
+
}
|
|
58
|
+
function sameData(a, b) {
|
|
59
|
+
return (!!a &&
|
|
60
|
+
a.msa === b.msa &&
|
|
61
|
+
a.tree === b.tree &&
|
|
62
|
+
a.treeMetadata === b.treeMetadata);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Keep IndexedDB holding what the view holds. The first run writes a new row and
|
|
66
|
+
* records its id in the session snapshot; later runs update that row, because
|
|
67
|
+
* the alignment keeps changing after it arrives — react-msaview's
|
|
68
|
+
* "calculate neighbor-joining tree" replaces `data.tree` — and a session
|
|
69
|
+
* reopened against a stale row restores the wrong picture.
|
|
70
|
+
*
|
|
71
|
+
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
72
|
+
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
73
|
+
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
74
|
+
* IndexedDB (private mode) fails once rather than in a loop.
|
|
75
|
+
*
|
|
76
|
+
* A view whose data comes from a filehandle stores nothing at all: the file is
|
|
77
|
+
* the source of truth and react-msaview refetches it at startup.
|
|
78
|
+
*/
|
|
38
79
|
export function storeDataToIndexedDB(self) {
|
|
39
|
-
const { rows, dataStoreId, isStoringData } = self;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
const { rows, dataStoreId, isStoringData, lastStoredData } = self;
|
|
81
|
+
const data = currentData(self);
|
|
82
|
+
if (rows.length === 0 ||
|
|
83
|
+
isStoringData ||
|
|
84
|
+
self.msaFilehandle ||
|
|
85
|
+
self.treeFilehandle ||
|
|
86
|
+
!(data.msa || data.tree) ||
|
|
87
|
+
sameData(lastStoredData, data)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// mark as storing synchronously so re-runs of this autorun (e.g. when data
|
|
91
|
+
// observables change while the write is pending) don't kick off a duplicate
|
|
92
|
+
// write and leave an orphan IndexedDB entry
|
|
93
|
+
self.setIsStoringData(true);
|
|
94
|
+
void (async () => {
|
|
95
|
+
try {
|
|
96
|
+
const id = dataStoreId ?? generateDataStoreId();
|
|
97
|
+
if (await storeMsaData(id, data)) {
|
|
98
|
+
self.setDataStoreId(id);
|
|
99
|
+
}
|
|
43
100
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (msaData || treeData) {
|
|
47
|
-
// mark as storing synchronously so re-runs of this autorun (e.g. when
|
|
48
|
-
// data observables change while the write is pending) don't kick off a
|
|
49
|
-
// duplicate write and leave an orphan IndexedDB entry
|
|
50
|
-
self.setIsStoringData(true);
|
|
51
|
-
void (async () => {
|
|
52
|
-
try {
|
|
53
|
-
const newId = generateDataStoreId();
|
|
54
|
-
const success = await storeMsaData(newId, {
|
|
55
|
-
msa: msaData,
|
|
56
|
-
tree: treeData,
|
|
57
|
-
treeMetadata: self.data.treeMetadata,
|
|
58
|
-
});
|
|
59
|
-
if (success) {
|
|
60
|
-
self.setDataStoreId(newId);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
console.error('Failed to store MSA data to IndexedDB:', e);
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
self.setIsStoringData(false);
|
|
68
|
-
}
|
|
69
|
-
})();
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.error('Failed to store MSA data to IndexedDB:', e);
|
|
70
103
|
}
|
|
71
|
-
|
|
104
|
+
finally {
|
|
105
|
+
transaction(() => {
|
|
106
|
+
self.setLastStoredData(data);
|
|
107
|
+
self.setIsStoringData(false);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
72
111
|
}
|
|
73
112
|
/**
|
|
74
113
|
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
@@ -79,42 +118,26 @@ export function storeDataToIndexedDB(self) {
|
|
|
79
118
|
*/
|
|
80
119
|
export function launchOrthologsIfNeeded(self) {
|
|
81
120
|
if (self.orthologParams) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.setData(data);
|
|
121
|
+
runLaunch({
|
|
122
|
+
self,
|
|
123
|
+
message: 'Resolving orthologs',
|
|
124
|
+
launch: scope => doLaunchOrthologs({ self, scope }),
|
|
125
|
+
onLaunched: () => {
|
|
88
126
|
self.setOrthologParams(undefined);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
self.setError(e);
|
|
92
|
-
console.error(e);
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
self.setProgress('');
|
|
96
|
-
}
|
|
97
|
-
})();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
98
129
|
}
|
|
99
130
|
}
|
|
100
131
|
export function launchBlastIfNeeded(self) {
|
|
101
132
|
if (self.blastParams) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
self.setData(data);
|
|
133
|
+
runLaunch({
|
|
134
|
+
self,
|
|
135
|
+
message: 'Submitting query',
|
|
136
|
+
launch: scope => doLaunchBlast({ self, scope }),
|
|
137
|
+
onLaunched: () => {
|
|
108
138
|
self.setBlastParams(undefined);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
self.setError(e);
|
|
112
|
-
console.error(e);
|
|
113
|
-
}
|
|
114
|
-
finally {
|
|
115
|
-
self.setProgress('');
|
|
116
|
-
}
|
|
117
|
-
})();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
118
141
|
}
|
|
119
142
|
}
|
|
120
143
|
/**
|
|
@@ -219,7 +242,7 @@ export function syncGenomeHoverToMsaColumn(self) {
|
|
|
219
242
|
*/
|
|
220
243
|
function genomeHighlightsToVisibleColumns(self, field) {
|
|
221
244
|
const { connectedViewId, transcriptToMsaMap, querySeqName } = self;
|
|
222
|
-
if (!transcriptToMsaMap) {
|
|
245
|
+
if (!transcriptToMsaMap || !hasQueryRow(self)) {
|
|
223
246
|
return [];
|
|
224
247
|
}
|
|
225
248
|
const { g2p } = transcriptToMsaMap;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
1
|
import React, { Component } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
3
|
interface Props {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
}
|
|
@@ -14,6 +14,6 @@ export declare class ErrorBoundary extends Component<Props, State> {
|
|
|
14
14
|
error: unknown;
|
|
15
15
|
};
|
|
16
16
|
componentDidCatch(error: unknown, info: React.ErrorInfo): void;
|
|
17
|
-
render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
|
|
17
|
+
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
|
|
18
18
|
}
|
|
19
19
|
export {};
|
|
@@ -2,12 +2,18 @@ import React from 'react';
|
|
|
2
2
|
import { Typography } from '@mui/material';
|
|
3
3
|
import ExternalLink from '../../components/ExternalLink';
|
|
4
4
|
import { ebiBlastResultUrl } from '../../utils/ebiBlast';
|
|
5
|
+
import { isPhmmerJobId, phmmerResultUrl } from '../../utils/phmmer';
|
|
5
6
|
function JobLink({ jobId }) {
|
|
7
|
+
// read off the job id rather than the launch params, so a link rebuilt for an
|
|
8
|
+
// old cached job still points at the tool that actually ran it
|
|
9
|
+
const url = isPhmmerJobId(jobId)
|
|
10
|
+
? phmmerResultUrl(jobId)
|
|
11
|
+
: ebiBlastResultUrl(jobId);
|
|
6
12
|
return (React.createElement(Typography, null,
|
|
7
13
|
"Job ",
|
|
8
14
|
jobId,
|
|
9
15
|
" (",
|
|
10
|
-
React.createElement(ExternalLink, { href:
|
|
16
|
+
React.createElement(ExternalLink, { href: url }, "see status"),
|
|
11
17
|
")"));
|
|
12
18
|
}
|
|
13
19
|
export default JobLink;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JBrowsePluginMsaViewModel } from '../model';
|
|
3
|
+
/**
|
|
4
|
+
* What a view shows while it is still building its alignment, and what it shows
|
|
5
|
+
* when that fails.
|
|
6
|
+
*
|
|
7
|
+
* Every launch that resolves something leaves its request on the model until it
|
|
8
|
+
* succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
|
|
9
|
+
* there IS "no alignment yet", and the error a failed launch records is only
|
|
10
|
+
* readable here. This used to key on `blastParams` alone, which left an ortholog
|
|
11
|
+
* launch rendering an empty MSAView for the minutes its alignment takes and, on
|
|
12
|
+
* failure, forever: the error was set and nothing drew it.
|
|
13
|
+
*/
|
|
14
|
+
declare const LaunchProgress: ({ model, }: {
|
|
15
|
+
model: JBrowsePluginMsaViewModel;
|
|
16
|
+
}) => React.JSX.Element;
|
|
17
|
+
export default LaunchProgress;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
|
|
3
|
+
import { Button, Typography } from '@mui/material';
|
|
4
|
+
import { observer } from 'mobx-react';
|
|
5
|
+
import { makeStyles } from 'tss-react/mui';
|
|
6
|
+
import JobLink from './JobLink';
|
|
7
|
+
const useStyles = makeStyles()({
|
|
8
|
+
margin: {
|
|
9
|
+
padding: 20,
|
|
10
|
+
},
|
|
11
|
+
progressRow: {
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
gap: 12,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* What a view shows while it is still building its alignment, and what it shows
|
|
19
|
+
* when that fails.
|
|
20
|
+
*
|
|
21
|
+
* Every launch that resolves something leaves its request on the model until it
|
|
22
|
+
* succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
|
|
23
|
+
* there IS "no alignment yet", and the error a failed launch records is only
|
|
24
|
+
* readable here. This used to key on `blastParams` alone, which left an ortholog
|
|
25
|
+
* launch rendering an empty MSAView for the minutes its alignment takes and, on
|
|
26
|
+
* failure, forever: the error was set and nothing drew it.
|
|
27
|
+
*/
|
|
28
|
+
const LaunchProgress = observer(function LaunchProgress2({ model, }) {
|
|
29
|
+
const { blastParams, orthologParams, progress, rid, error } = model;
|
|
30
|
+
const { classes } = useStyles();
|
|
31
|
+
const message = blastParams
|
|
32
|
+
? 'Running EBI BLAST'
|
|
33
|
+
: orthologParams
|
|
34
|
+
? 'Building ortholog alignment'
|
|
35
|
+
: 'Loading alignment';
|
|
36
|
+
return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
|
|
37
|
+
React.createElement(Typography, { variant: "h5" },
|
|
38
|
+
message,
|
|
39
|
+
" failed"),
|
|
40
|
+
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
41
|
+
React.createElement(ErrorMessage, { error: error }))) : (React.createElement(React.Fragment, null,
|
|
42
|
+
React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
|
|
43
|
+
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
44
|
+
React.createElement("div", { className: classes.progressRow },
|
|
45
|
+
React.createElement(Typography, null, progress || 'Initializing'),
|
|
46
|
+
React.createElement(Button, { variant: "outlined", size: "small", onClick: () => {
|
|
47
|
+
model.cancelLaunch();
|
|
48
|
+
} }, "Cancel"))))));
|
|
49
|
+
});
|
|
50
|
+
export default LaunchProgress;
|
|
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
|
|
|
4
4
|
import { MSAView } from 'react-msaview';
|
|
5
5
|
import { makeStyles } from 'tss-react/mui';
|
|
6
6
|
import { ErrorBoundary } from './ErrorBoundary';
|
|
7
|
-
import
|
|
7
|
+
import LaunchProgress from './LaunchProgress';
|
|
8
8
|
const useStyles = makeStyles()({
|
|
9
9
|
loadingContainer: {
|
|
10
10
|
padding: 20,
|
|
@@ -12,9 +12,12 @@ const useStyles = makeStyles()({
|
|
|
12
12
|
});
|
|
13
13
|
const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
|
|
14
14
|
const { classes } = useStyles();
|
|
15
|
-
const { blastParams, loadingStoredData } = model;
|
|
15
|
+
const { blastParams, orthologParams, init, loadingStoredData } = model;
|
|
16
|
+
// an unresolved launch request means there is no alignment to draw yet, so all
|
|
17
|
+
// three gate the same panel -- see LaunchProgress
|
|
18
|
+
const launching = !!(blastParams ?? orthologParams ?? init);
|
|
16
19
|
return (React.createElement(ErrorBoundary, null,
|
|
17
|
-
React.createElement("div", null,
|
|
20
|
+
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
|
|
18
21
|
React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
|
|
19
22
|
});
|
|
20
23
|
export default MsaViewPanel;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { afterEach, expect, test, vi } from 'vitest';
|
|
5
|
+
import MsaViewPanel from './MsaViewPanel';
|
|
6
|
+
// react-msaview's MSAView is the "nothing is launching" branch and wants a real
|
|
7
|
+
// MST model; a marker is enough to say the panel reached it
|
|
8
|
+
vi.mock('react-msaview', () => ({
|
|
9
|
+
MSAView: () => React.createElement("div", null, "the alignment"),
|
|
10
|
+
}));
|
|
11
|
+
function panel(model) {
|
|
12
|
+
return render(React.createElement(MsaViewPanel, { model: model }));
|
|
13
|
+
}
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
cleanup();
|
|
16
|
+
});
|
|
17
|
+
test('an alignment with no pending launch draws itself', () => {
|
|
18
|
+
panel({ progress: '' });
|
|
19
|
+
expect(screen.getByText('the alignment')).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
test('a running BLAST shows its progress, not an empty alignment', () => {
|
|
22
|
+
panel({
|
|
23
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
24
|
+
progress: 'Submitting query',
|
|
25
|
+
});
|
|
26
|
+
expect(screen.queryByText('the alignment')).toBeNull();
|
|
27
|
+
expect(screen.getByText(/Running EBI BLAST/)).toBeTruthy();
|
|
28
|
+
expect(screen.getByText('Submitting query')).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
// the bug this file was written for: an ortholog launch sets orthologParams
|
|
31
|
+
// rather than blastParams, and the panel keyed on blastParams alone -- so it
|
|
32
|
+
// rendered an empty MSAView for the minutes the alignment takes, and drew
|
|
33
|
+
// nothing at all when the launch failed
|
|
34
|
+
test('a running ortholog launch shows its progress', () => {
|
|
35
|
+
panel({
|
|
36
|
+
orthologParams: { taxId: 9606 },
|
|
37
|
+
progress: 'Resolving orthologs',
|
|
38
|
+
});
|
|
39
|
+
expect(screen.queryByText('the alignment')).toBeNull();
|
|
40
|
+
expect(screen.getByText(/Building ortholog alignment/)).toBeTruthy();
|
|
41
|
+
expect(screen.getByText('Resolving orthologs')).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
test('a failed ortholog launch shows why', () => {
|
|
44
|
+
panel({
|
|
45
|
+
orthologParams: { taxId: 9606 },
|
|
46
|
+
progress: '',
|
|
47
|
+
error: new Error('Only 1 ortholog(s) found for this gene'),
|
|
48
|
+
});
|
|
49
|
+
expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
test('a failed init shows why', () => {
|
|
52
|
+
panel({
|
|
53
|
+
init: { msaName: 'ENST00000288602' },
|
|
54
|
+
progress: '',
|
|
55
|
+
error: new Error('No alignment named ENST00000288602 in msa.fa.gz'),
|
|
56
|
+
});
|
|
57
|
+
expect(screen.queryByText('the alignment')).toBeNull();
|
|
58
|
+
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
// a launch runs for 10+ minutes, so leaving with no way out means watching it
|
|
61
|
+
test('a running launch offers a way out, and a failed one does not', () => {
|
|
62
|
+
const cancelLaunch = vi.fn();
|
|
63
|
+
panel({
|
|
64
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
65
|
+
progress: 'Re-checking BLAST status in... 7',
|
|
66
|
+
cancelLaunch,
|
|
67
|
+
});
|
|
68
|
+
screen.getByRole('button', { name: 'Cancel' }).click();
|
|
69
|
+
expect(cancelLaunch).toHaveBeenCalled();
|
|
70
|
+
cleanup();
|
|
71
|
+
panel({
|
|
72
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
73
|
+
progress: '',
|
|
74
|
+
error: new Error('No hits found'),
|
|
75
|
+
cancelLaunch,
|
|
76
|
+
});
|
|
77
|
+
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
test('a running job links out to it', () => {
|
|
80
|
+
panel({
|
|
81
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
82
|
+
progress: 'Re-checking BLAST status in... 7',
|
|
83
|
+
rid: 'ncbiblast-R20260826-123456-0001-abc',
|
|
84
|
+
});
|
|
85
|
+
const link = screen.getByRole('link');
|
|
86
|
+
expect(link.getAttribute('href')).toContain('jobId=ncbiblast-R20260826-123456-0001-abc');
|
|
87
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
|
-
|
|
2
|
+
import type { LaunchScope } from './runLaunch';
|
|
3
|
+
export declare function doLaunchBlast({ self, scope, }: {
|
|
3
4
|
self: JBrowsePluginMsaViewModel;
|
|
5
|
+
scope: LaunchScope;
|
|
4
6
|
}): Promise<{
|
|
5
|
-
treeMetadata: string;
|
|
6
7
|
msa: string;
|
|
7
8
|
tree: string;
|
|
9
|
+
treeMetadata: string;
|
|
8
10
|
}>;
|