jbrowse-plugin-protein3d 0.4.11 → 0.4.13
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/LaunchProteinView/components/LaunchSettingsDialog.d.ts +5 -0
- package/dist/LaunchProteinView/components/LaunchSettingsDialog.js +23 -0
- package/dist/LaunchProteinView/components/ProteinViewActions.js +13 -2
- package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.js +12 -3
- package/dist/LaunchProteinView/hooks/useAlphaFoldData.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +2 -1
- package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.js +2 -1
- package/dist/LaunchProteinView/hooks/useFoldseekSearch.js +47 -12
- package/dist/LaunchProteinView/hooks/useStructureFileSequence.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useStructureFileSequence.js +5 -2
- package/dist/LaunchProteinView/services/foldseekApi.d.ts +23 -5
- package/dist/LaunchProteinView/services/foldseekApi.js +21 -13
- package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +2 -1
- package/dist/LaunchProteinView/utils/launchViewUtils.js +7 -2
- package/dist/LaunchProteinView/utils/sideBySide.d.ts +11 -0
- package/dist/LaunchProteinView/utils/sideBySide.js +33 -0
- package/dist/LaunchProteinViewExtensionPoint/index.js +9 -2
- package/dist/ProteinView/applyColorTheme.d.ts +1 -1
- package/dist/ProteinView/loadStructureData.d.ts +18 -0
- package/dist/ProteinView/loadStructureData.js +22 -0
- package/dist/ProteinView/model.d.ts +2 -2
- package/dist/ProteinView/model.js +6 -36
- package/dist/ProteinView/structureLoader.d.ts +30 -0
- package/dist/ProteinView/structureLoader.js +58 -0
- package/dist/config.json +1 -1
- package/dist/fetchUtils.d.ts +1 -1
- package/dist/fetchUtils.js +18 -2
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/molstar-chunk.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -2
- package/src/LaunchProteinView/components/LaunchSettingsDialog.tsx +63 -0
- package/src/LaunchProteinView/components/ProteinViewActions.tsx +21 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldDBSearch.ts +13 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +3 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.ts +12 -10
- package/src/LaunchProteinView/hooks/useFoldseekSearch.ts +49 -12
- package/src/LaunchProteinView/hooks/useStructureFileSequence.ts +5 -2
- package/src/LaunchProteinView/services/foldseekApi.ts +57 -23
- package/src/LaunchProteinView/utils/launchViewUtils.ts +10 -1
- package/src/LaunchProteinView/utils/sideBySide.ts +55 -0
- package/src/LaunchProteinViewExtensionPoint/index.ts +17 -1
- package/src/ProteinView/loadStructureData.ts +36 -0
- package/src/ProteinView/model.ts +6 -47
- package/src/ProteinView/structureLoader.test.ts +102 -0
- package/src/ProteinView/structureLoader.ts +74 -0
- package/src/fetchUtils.test.ts +27 -0
- package/src/fetchUtils.ts +22 -2
- package/src/version.ts +1 -1
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.4.
|
|
1
|
+
export declare const version = "0.4.13";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.4.
|
|
1
|
+
export const version = '0.4.13';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.4.
|
|
2
|
+
"version": "0.4.13",
|
|
3
3
|
"name": "jbrowse-plugin-protein3d",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
"jsdom": "^29.1.1",
|
|
52
52
|
"mobx": "^6.15.4",
|
|
53
53
|
"mobx-react": "^9.2.2",
|
|
54
|
+
"pixelmatch": "^7.2.0",
|
|
55
|
+
"pngjs": "^7.0.0",
|
|
54
56
|
"prettier": "^3.8.3",
|
|
55
57
|
"pretty-bytes": "^7.1.0",
|
|
56
58
|
"puppeteer": "^25.1.0",
|
|
@@ -86,7 +88,7 @@
|
|
|
86
88
|
"lint": "eslint src --report-unused-disable-directives --max-warnings 0",
|
|
87
89
|
"pretest": "test -d .test-jbrowse-nightly || jbrowse create .test-jbrowse-nightly --nightly",
|
|
88
90
|
"preversion": "pnpm lint",
|
|
89
|
-
"version": "node -
|
|
91
|
+
"version": "node scripts/sync-version.mjs && git add src/version.ts",
|
|
90
92
|
"postversion": "git push --follow-tags"
|
|
91
93
|
}
|
|
92
94
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Checkbox,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogActions,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
FormControlLabel,
|
|
11
|
+
FormGroup,
|
|
12
|
+
Typography,
|
|
13
|
+
} from '@mui/material'
|
|
14
|
+
|
|
15
|
+
import { getLaunchSideBySide, setLaunchSideBySide } from '../utils/sideBySide'
|
|
16
|
+
|
|
17
|
+
// Small, self-contained launch settings (NOT the global preferences dialog):
|
|
18
|
+
// just the options that affect how this protein view opens.
|
|
19
|
+
export default function LaunchSettingsDialog({
|
|
20
|
+
open,
|
|
21
|
+
onClose,
|
|
22
|
+
}: {
|
|
23
|
+
open: boolean
|
|
24
|
+
onClose: () => void
|
|
25
|
+
}) {
|
|
26
|
+
const [sideBySide, setSideBySide] = useState(() => getLaunchSideBySide())
|
|
27
|
+
return (
|
|
28
|
+
<Dialog
|
|
29
|
+
open={open}
|
|
30
|
+
onClose={() => {
|
|
31
|
+
onClose()
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<DialogTitle>Launch settings</DialogTitle>
|
|
35
|
+
<DialogContent>
|
|
36
|
+
<FormGroup>
|
|
37
|
+
<FormControlLabel
|
|
38
|
+
control={<Checkbox checked={sideBySide} />}
|
|
39
|
+
label="Open protein view side-by-side with the genome view"
|
|
40
|
+
onChange={(_, checked) => {
|
|
41
|
+
setSideBySide(checked)
|
|
42
|
+
setLaunchSideBySide(checked)
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
</FormGroup>
|
|
46
|
+
<Typography variant="body2" color="text.secondary">
|
|
47
|
+
When enabled, launching a protein view places it to the right of the
|
|
48
|
+
connected genome view in a split layout instead of stacking it below.
|
|
49
|
+
</Typography>
|
|
50
|
+
</DialogContent>
|
|
51
|
+
<DialogActions>
|
|
52
|
+
<Button
|
|
53
|
+
variant="contained"
|
|
54
|
+
onClick={() => {
|
|
55
|
+
onClose()
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
Close
|
|
59
|
+
</Button>
|
|
60
|
+
</DialogActions>
|
|
61
|
+
</Dialog>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -2,9 +2,11 @@ import React, { useState } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { ErrorMessage } from '@jbrowse/core/ui'
|
|
4
4
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
|
|
5
|
-
import
|
|
5
|
+
import SettingsIcon from '@mui/icons-material/Settings'
|
|
6
|
+
import { Button, ButtonGroup, IconButton, Tooltip, Typography } from '@mui/material'
|
|
6
7
|
|
|
7
8
|
import LaunchOptionsDialog from './LaunchOptionsDialog'
|
|
9
|
+
import LaunchSettingsDialog from './LaunchSettingsDialog'
|
|
8
10
|
import SequenceMismatchNotice from './SequenceMismatchNotice'
|
|
9
11
|
import { getLaunchMissingReasons, safeLaunch } from '../utils/launchHelpers'
|
|
10
12
|
import {
|
|
@@ -58,6 +60,7 @@ export default function ProteinViewActions({
|
|
|
58
60
|
error,
|
|
59
61
|
}: ProteinViewActionsProps) {
|
|
60
62
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
63
|
+
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
61
64
|
const [launchError, setLaunchError] = useState<unknown>()
|
|
62
65
|
// Disable launch while loading — SWR's keepPreviousData would otherwise let
|
|
63
66
|
// a user click Launch on stale results (wrong UniProt ID) during a refetch.
|
|
@@ -155,6 +158,17 @@ export default function ProteinViewActions({
|
|
|
155
158
|
onAlignmentAlgorithmChange={onAlignmentAlgorithmChange}
|
|
156
159
|
/>
|
|
157
160
|
) : null}
|
|
161
|
+
<Tooltip title="Launch settings">
|
|
162
|
+
<IconButton
|
|
163
|
+
size="small"
|
|
164
|
+
aria-label="Launch settings"
|
|
165
|
+
onClick={() => {
|
|
166
|
+
setSettingsOpen(true)
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<SettingsIcon fontSize="small" />
|
|
170
|
+
</IconButton>
|
|
171
|
+
</Tooltip>
|
|
158
172
|
<Button
|
|
159
173
|
variant="contained"
|
|
160
174
|
color="secondary"
|
|
@@ -189,6 +203,12 @@ export default function ProteinViewActions({
|
|
|
189
203
|
onClose={closeMenu}
|
|
190
204
|
options={launchOptions}
|
|
191
205
|
/>
|
|
206
|
+
<LaunchSettingsDialog
|
|
207
|
+
open={settingsOpen}
|
|
208
|
+
onClose={() => {
|
|
209
|
+
setSettingsOpen(false)
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
192
212
|
</>
|
|
193
213
|
)
|
|
194
214
|
}
|
|
@@ -63,6 +63,7 @@ export default function useAlphaFoldDBSearch({
|
|
|
63
63
|
|
|
64
64
|
const {
|
|
65
65
|
isLoading: isAlphaFoldLoading,
|
|
66
|
+
isValidating: isAlphaFoldValidating,
|
|
66
67
|
error: alphaFoldError,
|
|
67
68
|
url: alphaFoldUrl,
|
|
68
69
|
confidenceUrl: alphaFoldConfidenceUrl,
|
|
@@ -93,6 +94,7 @@ export default function useAlphaFoldDBSearch({
|
|
|
93
94
|
plddtDocUrl: seqSearchConfidenceUrl,
|
|
94
95
|
structureSequence: seqSearchStructureSequence,
|
|
95
96
|
isLoading: isSequenceSearchLoading,
|
|
97
|
+
isValidating: isSequenceSearchValidating,
|
|
96
98
|
error: sequenceSearchError,
|
|
97
99
|
} = useAlphaFoldSequenceSearch({
|
|
98
100
|
sequence: userSelectedProteinSequence?.seq,
|
|
@@ -110,6 +112,14 @@ export default function useAlphaFoldDBSearch({
|
|
|
110
112
|
: alphaFoldStructureSequence
|
|
111
113
|
const finalUniprotId = isSequenceMode ? seqSearchUniprotId : uniprotId
|
|
112
114
|
|
|
115
|
+
// While a structure fetch is in flight, finalStructureSequence may still be
|
|
116
|
+
// the previous selection's sequence (keepPreviousData). Comparing that stale
|
|
117
|
+
// sequence to the freshly-selected transcript would give a wrong match, so
|
|
118
|
+
// the match is treated as unknown until the fetch settles.
|
|
119
|
+
const isStructureValidating = isSequenceMode
|
|
120
|
+
? isSequenceSearchValidating
|
|
121
|
+
: isAlphaFoldValidating
|
|
122
|
+
|
|
113
123
|
const loadingStatuses = [
|
|
114
124
|
isLookupLoading && 'Looking up UniProt ID',
|
|
115
125
|
isIsoformLoading && 'Loading protein sequences from transcript isoforms',
|
|
@@ -167,7 +177,9 @@ export default function useAlphaFoldDBSearch({
|
|
|
167
177
|
!!selectedTranscript &&
|
|
168
178
|
(isSequenceMode || !!(finalStructureSequence && finalUniprotId)),
|
|
169
179
|
sequencesMatch:
|
|
170
|
-
|
|
180
|
+
!isStructureValidating &&
|
|
181
|
+
userSelectedProteinSequence?.seq &&
|
|
182
|
+
finalStructureSequence
|
|
171
183
|
? stripStopCodon(userSelectedProteinSequence.seq) ===
|
|
172
184
|
finalStructureSequence
|
|
173
185
|
: undefined,
|
|
@@ -14,10 +14,12 @@ export default function useAlphaFoldData({
|
|
|
14
14
|
? getAlphaFoldConfidenceUrl(uniprotId)
|
|
15
15
|
: undefined
|
|
16
16
|
|
|
17
|
-
const { sequences, isLoading, error } =
|
|
17
|
+
const { sequences, isLoading, isValidating, error } =
|
|
18
|
+
useStructureFileSequence({ url })
|
|
18
19
|
|
|
19
20
|
return {
|
|
20
21
|
isLoading,
|
|
22
|
+
isValidating,
|
|
21
23
|
error,
|
|
22
24
|
url,
|
|
23
25
|
confidenceUrl,
|
|
@@ -41,19 +41,21 @@ export default function useAlphaFoldSequenceSearch({
|
|
|
41
41
|
return searchType === 'md5' ? md5(cleanSeq) : cleanSeq
|
|
42
42
|
}, [sequence, searchType])
|
|
43
43
|
|
|
44
|
-
const { data, error, isLoading } =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
const { data, error, isLoading, isValidating } =
|
|
45
|
+
useSWR<SequenceSummaryResponse>(
|
|
46
|
+
enabled && searchValue
|
|
47
|
+
? `https://alphafold.ebi.ac.uk/api/sequence/summary?id=${encodeURIComponent(searchValue)}&type=${searchType}`
|
|
48
|
+
: null,
|
|
49
|
+
jsonfetch,
|
|
50
|
+
{
|
|
51
|
+
...STATIC_SWR_OPTIONS,
|
|
52
|
+
keepPreviousData: true,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
54
55
|
|
|
55
56
|
return {
|
|
56
57
|
isLoading,
|
|
58
|
+
isValidating,
|
|
57
59
|
result: data,
|
|
58
60
|
uniprotId: data?.uniprotAccession,
|
|
59
61
|
cifUrl: data?.cifUrl,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_DATABASES,
|
|
@@ -23,21 +23,43 @@ export default function useFoldseekSearch() {
|
|
|
23
23
|
const [error, setError] = useState<unknown>()
|
|
24
24
|
const [statusMessage, setStatusMessage] = useState('')
|
|
25
25
|
|
|
26
|
+
// Aborts the in-flight request (3Di prediction or the up-to-3-minute Foldseek
|
|
27
|
+
// poll) when the dialog closes/unmounts, so it stops hitting the external API
|
|
28
|
+
// and stops updating dead state.
|
|
29
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
abortRef.current?.abort()
|
|
33
|
+
}
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
const startOperation = () => {
|
|
37
|
+
abortRef.current?.abort()
|
|
38
|
+
const controller = new AbortController()
|
|
39
|
+
abortRef.current = controller
|
|
40
|
+
return controller.signal
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
const predictStructure = async (aaSequence: string) => {
|
|
44
|
+
const signal = startOperation()
|
|
27
45
|
setIsPredicting(true)
|
|
28
46
|
setError(undefined)
|
|
29
47
|
setStatusMessage('Predicting 3Di structure...')
|
|
30
48
|
try {
|
|
31
|
-
const result = await predict3Di(aaSequence)
|
|
49
|
+
const result = await predict3Di({ aaSequence, signal })
|
|
32
50
|
setPredictData(result)
|
|
33
51
|
return result
|
|
34
52
|
} catch (e) {
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
if (!signal.aborted) {
|
|
54
|
+
console.error(e)
|
|
55
|
+
setError(e)
|
|
56
|
+
}
|
|
37
57
|
return undefined
|
|
38
58
|
} finally {
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
if (!signal.aborted) {
|
|
60
|
+
setIsPredicting(false)
|
|
61
|
+
setStatusMessage('')
|
|
62
|
+
}
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|
|
@@ -46,25 +68,40 @@ export default function useFoldseekSearch() {
|
|
|
46
68
|
di3Seq: string,
|
|
47
69
|
databases: FoldseekDatabaseId[] = DEFAULT_DATABASES,
|
|
48
70
|
) => {
|
|
71
|
+
const signal = startOperation()
|
|
49
72
|
setIsLoading(true)
|
|
50
73
|
setError(undefined)
|
|
51
74
|
setStatusMessage('Submitting search...')
|
|
52
75
|
try {
|
|
53
|
-
const ticket = await submitFoldseekSearch(
|
|
54
|
-
|
|
76
|
+
const ticket = await submitFoldseekSearch({
|
|
77
|
+
aaSequence: aaSeq,
|
|
78
|
+
di3Sequence: di3Seq,
|
|
79
|
+
databases,
|
|
80
|
+
signal,
|
|
81
|
+
})
|
|
82
|
+
const result = await waitForFoldseekResults({
|
|
83
|
+
ticketId: ticket.id,
|
|
84
|
+
onStatusChange: setStatusMessage,
|
|
85
|
+
signal,
|
|
86
|
+
})
|
|
55
87
|
setResults(result)
|
|
56
88
|
return result
|
|
57
89
|
} catch (e) {
|
|
58
|
-
|
|
59
|
-
|
|
90
|
+
if (!signal.aborted) {
|
|
91
|
+
console.error(e)
|
|
92
|
+
setError(e)
|
|
93
|
+
}
|
|
60
94
|
return undefined
|
|
61
95
|
} finally {
|
|
62
|
-
|
|
63
|
-
|
|
96
|
+
if (!signal.aborted) {
|
|
97
|
+
setIsLoading(false)
|
|
98
|
+
setStatusMessage('')
|
|
99
|
+
}
|
|
64
100
|
}
|
|
65
101
|
}
|
|
66
102
|
|
|
67
103
|
const reset = () => {
|
|
104
|
+
abortRef.current?.abort()
|
|
68
105
|
setResults(undefined)
|
|
69
106
|
setPredictData(undefined)
|
|
70
107
|
setError(undefined)
|
|
@@ -45,7 +45,7 @@ export default function useStructureFileSequence({
|
|
|
45
45
|
: url
|
|
46
46
|
? (['structure-url', url] as const)
|
|
47
47
|
: null
|
|
48
|
-
const { data, error, isLoading } = useSWR<string[] | undefined>(
|
|
48
|
+
const { data, error, isLoading, isValidating } = useSWR<string[] | undefined>(
|
|
49
49
|
key,
|
|
50
50
|
async () => {
|
|
51
51
|
const seq = await fetchSequences({ file, url })
|
|
@@ -60,5 +60,8 @@ export default function useStructureFileSequence({
|
|
|
60
60
|
},
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// isValidating distinguishes "fetching for the current key" from the stale
|
|
64
|
+
// data keepPreviousData keeps around during a key change. Consumers comparing
|
|
65
|
+
// this sequence against another need it to avoid matching against stale data.
|
|
66
|
+
return { error, isLoading, isValidating, sequences: data }
|
|
64
67
|
}
|
|
@@ -61,7 +61,13 @@ export interface FoldseekResult {
|
|
|
61
61
|
results: FoldseekDatabaseResult[]
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export async function predict3Di(
|
|
64
|
+
export async function predict3Di({
|
|
65
|
+
aaSequence,
|
|
66
|
+
signal,
|
|
67
|
+
}: {
|
|
68
|
+
aaSequence: string
|
|
69
|
+
signal?: AbortSignal
|
|
70
|
+
}) {
|
|
65
71
|
// Clean the sequence - remove FASTA header, whitespace, stop codons, and non-AA chars
|
|
66
72
|
const cleanSequence = aaSequence
|
|
67
73
|
.split('\n')
|
|
@@ -74,6 +80,7 @@ export async function predict3Di(aaSequence: string) {
|
|
|
74
80
|
|
|
75
81
|
const response = await fetch(
|
|
76
82
|
`https://3di.foldseek.com/predict/${encodeURIComponent(cleanSequence)}`,
|
|
83
|
+
{ signal },
|
|
77
84
|
)
|
|
78
85
|
if (!response.ok) {
|
|
79
86
|
throw new Error(
|
|
@@ -89,11 +96,17 @@ export async function predict3Di(aaSequence: string) {
|
|
|
89
96
|
return { aaSequence: cleanSequence, di3Sequence: cleanDi3 }
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
export async function submitFoldseekSearch(
|
|
93
|
-
aaSequence
|
|
94
|
-
di3Sequence
|
|
95
|
-
databases
|
|
96
|
-
|
|
99
|
+
export async function submitFoldseekSearch({
|
|
100
|
+
aaSequence,
|
|
101
|
+
di3Sequence,
|
|
102
|
+
databases,
|
|
103
|
+
signal,
|
|
104
|
+
}: {
|
|
105
|
+
aaSequence: string
|
|
106
|
+
di3Sequence: string
|
|
107
|
+
databases: FoldseekDatabaseId[]
|
|
108
|
+
signal?: AbortSignal
|
|
109
|
+
}) {
|
|
97
110
|
// Submit both AA and 3Di sequences (with trailing newline like working example)
|
|
98
111
|
const fastaContent = `>query\n${aaSequence}\n>3DI\n${di3Sequence}\n`
|
|
99
112
|
const params = new URLSearchParams()
|
|
@@ -110,20 +123,27 @@ export async function submitFoldseekSearch(
|
|
|
110
123
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
111
124
|
},
|
|
112
125
|
body: params,
|
|
126
|
+
signal,
|
|
113
127
|
})
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
// Read the body as text first so a non-JSON error page (e.g. a gateway/500
|
|
130
|
+
// HTML response) surfaces the real status instead of an opaque JSON
|
|
131
|
+
// SyntaxError that hides it.
|
|
132
|
+
const text = await response.text()
|
|
117
133
|
if (!response.ok) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`Foldseek submission failed: ${response.status} ${JSON.stringify(responseData)}`,
|
|
120
|
-
)
|
|
134
|
+
throw new Error(`Foldseek submission failed: ${response.status} ${text}`)
|
|
121
135
|
}
|
|
122
136
|
|
|
123
|
-
return
|
|
137
|
+
return JSON.parse(text) as FoldseekTicketResponse
|
|
124
138
|
}
|
|
125
139
|
|
|
126
|
-
export async function pollFoldseekStatus(
|
|
140
|
+
export async function pollFoldseekStatus({
|
|
141
|
+
ticketId,
|
|
142
|
+
signal,
|
|
143
|
+
}: {
|
|
144
|
+
ticketId: string
|
|
145
|
+
signal?: AbortSignal
|
|
146
|
+
}) {
|
|
127
147
|
// Use the /tickets endpoint (plural) with POST
|
|
128
148
|
const params = new URLSearchParams()
|
|
129
149
|
params.append('tickets[]', ticketId)
|
|
@@ -134,6 +154,7 @@ export async function pollFoldseekStatus(ticketId: string) {
|
|
|
134
154
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
135
155
|
},
|
|
136
156
|
body: params,
|
|
157
|
+
signal,
|
|
137
158
|
})
|
|
138
159
|
|
|
139
160
|
if (!response.ok) {
|
|
@@ -160,23 +181,36 @@ interface FoldseekApiResponse {
|
|
|
160
181
|
}[]
|
|
161
182
|
}
|
|
162
183
|
|
|
163
|
-
export async function getFoldseekResults(
|
|
164
|
-
ticketId
|
|
165
|
-
|
|
184
|
+
export async function getFoldseekResults({
|
|
185
|
+
ticketId,
|
|
186
|
+
signal,
|
|
187
|
+
}: {
|
|
188
|
+
ticketId: string
|
|
189
|
+
signal?: AbortSignal
|
|
190
|
+
}): Promise<FoldseekApiResponse> {
|
|
166
191
|
return jsonfetch<FoldseekApiResponse>(
|
|
167
192
|
`https://search.foldseek.com/api/result/${ticketId}/0`,
|
|
193
|
+
{ signal },
|
|
168
194
|
)
|
|
169
195
|
}
|
|
170
196
|
|
|
171
|
-
export async function waitForFoldseekResults(
|
|
172
|
-
ticketId
|
|
173
|
-
onStatusChange
|
|
174
|
-
|
|
197
|
+
export async function waitForFoldseekResults({
|
|
198
|
+
ticketId,
|
|
199
|
+
onStatusChange,
|
|
200
|
+
signal,
|
|
201
|
+
}: {
|
|
202
|
+
ticketId: string
|
|
203
|
+
onStatusChange?: (status: string) => void
|
|
204
|
+
signal?: AbortSignal
|
|
205
|
+
}) {
|
|
175
206
|
const maxAttempts = 180
|
|
176
207
|
let attempts = 0
|
|
177
208
|
|
|
178
209
|
while (attempts < maxAttempts) {
|
|
179
|
-
|
|
210
|
+
if (signal?.aborted) {
|
|
211
|
+
throw signal.reason
|
|
212
|
+
}
|
|
213
|
+
const status = await pollFoldseekStatus({ ticketId, signal })
|
|
180
214
|
|
|
181
215
|
if (status.status === 'ERROR') {
|
|
182
216
|
console.error('[Foldseek] Search error:', status)
|
|
@@ -187,7 +221,7 @@ export async function waitForFoldseekResults(
|
|
|
187
221
|
|
|
188
222
|
if (status.status === 'COMPLETE') {
|
|
189
223
|
onStatusChange?.('Fetching results...')
|
|
190
|
-
const apiResponse = await getFoldseekResults(ticketId)
|
|
224
|
+
const apiResponse = await getFoldseekResults({ ticketId, signal })
|
|
191
225
|
|
|
192
226
|
// Transform API response to our format
|
|
193
227
|
const results: FoldseekResult = {
|
|
@@ -204,7 +238,7 @@ export async function waitForFoldseekResults(
|
|
|
204
238
|
onStatusChange?.(
|
|
205
239
|
`Search ${status.status.toLowerCase()}... (${attempts + 1}s)`,
|
|
206
240
|
)
|
|
207
|
-
await timeout(1000)
|
|
241
|
+
await timeout(1000, signal)
|
|
208
242
|
attempts++
|
|
209
243
|
}
|
|
210
244
|
|
|
@@ -6,6 +6,7 @@ declare global {
|
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
import { getLaunchSideBySide, launchViewSideBySide } from './sideBySide'
|
|
9
10
|
import { getGeneDisplayName, getTranscriptDisplayName } from './util'
|
|
10
11
|
import { launchProteinAnnotationView } from '../components/launchProteinAnnotationView'
|
|
11
12
|
|
|
@@ -113,6 +114,7 @@ export function launch3DProteinView({
|
|
|
113
114
|
alignmentAlgorithm,
|
|
114
115
|
displayName,
|
|
115
116
|
connectedMsaViewId,
|
|
117
|
+
sideBySide,
|
|
116
118
|
}: LaunchViewParams & {
|
|
117
119
|
url?: string
|
|
118
120
|
data?: string
|
|
@@ -120,6 +122,9 @@ export function launch3DProteinView({
|
|
|
120
122
|
alignmentAlgorithm?: string
|
|
121
123
|
displayName?: string
|
|
122
124
|
connectedMsaViewId?: string
|
|
125
|
+
// explicit override; when undefined the launch-dialog localStorage preference
|
|
126
|
+
// decides (left genome | right protein)
|
|
127
|
+
sideBySide?: boolean
|
|
123
128
|
}) {
|
|
124
129
|
const snap = {
|
|
125
130
|
type: 'ProteinView',
|
|
@@ -138,7 +143,11 @@ export function launch3DProteinView({
|
|
|
138
143
|
displayName ??
|
|
139
144
|
formatViewName('Protein view', feature, selectedTranscript, uniprotId),
|
|
140
145
|
}
|
|
141
|
-
|
|
146
|
+
const proteinView = session.addView('ProteinView', snap)
|
|
147
|
+
if (sideBySide ?? getLaunchSideBySide()) {
|
|
148
|
+
launchViewSideBySide(session, proteinView.id)
|
|
149
|
+
}
|
|
150
|
+
return proteinView
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
export async function launch1DProteinView({
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AbstractSessionModel } from '@jbrowse/core/util'
|
|
2
|
+
|
|
3
|
+
// Self-contained launch preference (NOT the global/core preferences system):
|
|
4
|
+
// whether a protein view launched from a genome feature opens side-by-side with
|
|
5
|
+
// its connected genome view (left genome | right protein) instead of stacked.
|
|
6
|
+
const SIDE_BY_SIDE_KEY = 'proteinView-launchSideBySide'
|
|
7
|
+
|
|
8
|
+
// Default to side-by-side: a connected genome+protein pair reads best as a
|
|
9
|
+
// left/right split. Users can turn it off in the launch dialog's settings.
|
|
10
|
+
const DEFAULT_SIDE_BY_SIDE = true
|
|
11
|
+
|
|
12
|
+
export function getLaunchSideBySide() {
|
|
13
|
+
const stored = localStorage.getItem(SIDE_BY_SIDE_KEY)
|
|
14
|
+
return stored === null ? DEFAULT_SIDE_BY_SIDE : stored === 'true'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setLaunchSideBySide(value: boolean) {
|
|
18
|
+
localStorage.setItem(SIDE_BY_SIDE_KEY, value ? 'true' : 'false')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The workspaces split is driven by two session actions that only exist on the
|
|
22
|
+
// web/desktop session (MultipleViews + DockviewLayout mixins). Embedded sessions
|
|
23
|
+
// lack them, so feature-detect before using.
|
|
24
|
+
interface SessionWithWorkspaces {
|
|
25
|
+
setUseWorkspaces: (useWorkspaces: boolean) => void
|
|
26
|
+
setPendingMove: (move: { type: 'splitRight'; viewId: string }) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isSessionWithWorkspaces(
|
|
30
|
+
session: AbstractSessionModel,
|
|
31
|
+
): session is AbstractSessionModel & SessionWithWorkspaces {
|
|
32
|
+
return (
|
|
33
|
+
'setUseWorkspaces' in session &&
|
|
34
|
+
typeof session.setUseWorkspaces === 'function' &&
|
|
35
|
+
'setPendingMove' in session &&
|
|
36
|
+
typeof session.setPendingMove === 'function'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Place a freshly-added view to the right of the others in a workspaces (tiled)
|
|
42
|
+
* layout. Mirrors the "Move to split view" view-menu action: queue a splitRight
|
|
43
|
+
* pending move for this view, then enable workspaces so TiledViewsContainer
|
|
44
|
+
* consumes the move on mount (other views land in the left panel, this one in a
|
|
45
|
+
* new right panel). No-op on sessions without workspaces support.
|
|
46
|
+
*/
|
|
47
|
+
export function launchViewSideBySide(
|
|
48
|
+
session: AbstractSessionModel,
|
|
49
|
+
viewId: string,
|
|
50
|
+
) {
|
|
51
|
+
if (isSessionWithWorkspaces(session)) {
|
|
52
|
+
session.setPendingMove({ type: 'splitRight', viewId })
|
|
53
|
+
session.setUseWorkspaces(true)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type ConnectedViewSpec, resolveShortLaunch } from './resolveShortLaunch'
|
|
2
|
+
import {
|
|
3
|
+
getLaunchSideBySide,
|
|
4
|
+
launchViewSideBySide,
|
|
5
|
+
} from '../LaunchProteinView/utils/sideBySide'
|
|
2
6
|
|
|
3
7
|
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
4
8
|
import type { AbstractSessionModel } from '@jbrowse/core/util'
|
|
@@ -28,6 +32,7 @@ export default function LaunchProteinViewExtensionPointF(
|
|
|
28
32
|
showControls,
|
|
29
33
|
showHighlight,
|
|
30
34
|
zoomToBaseLevel,
|
|
35
|
+
sideBySide,
|
|
31
36
|
}: {
|
|
32
37
|
session: AbstractSessionModel
|
|
33
38
|
url?: string
|
|
@@ -43,6 +48,10 @@ export default function LaunchProteinViewExtensionPointF(
|
|
|
43
48
|
showControls?: boolean
|
|
44
49
|
showHighlight?: boolean
|
|
45
50
|
zoomToBaseLevel?: boolean
|
|
51
|
+
// when this launch creates its own connected genome view, place the
|
|
52
|
+
// protein view side-by-side (left genome | right protein). Explicit
|
|
53
|
+
// override; falls back to the launch-dialog localStorage preference.
|
|
54
|
+
sideBySide?: boolean
|
|
46
55
|
}) => {
|
|
47
56
|
// Short-URL form: `uniprotId` + `transcriptId` + `connectedView` (no
|
|
48
57
|
// explicit `url`/`feature`/sequence). Derive the structure URL, the
|
|
@@ -77,6 +86,9 @@ export default function LaunchProteinViewExtensionPointF(
|
|
|
77
86
|
// `connectedView` is supplied we create the LinearGenomeView here and wire
|
|
78
87
|
// its id, letting a single spec entry produce a connected genome+protein
|
|
79
88
|
// pair (e.g. hover a variant to highlight the residue).
|
|
89
|
+
// a connected view this launch created itself can be split beside the
|
|
90
|
+
// protein view; a pre-existing connectedViewId is left in place
|
|
91
|
+
const ownsConnectedView = !connectedViewId && !!connectedView
|
|
80
92
|
const resolvedConnectedViewId =
|
|
81
93
|
connectedViewId ??
|
|
82
94
|
(connectedView
|
|
@@ -86,7 +98,7 @@ export default function LaunchProteinViewExtensionPointF(
|
|
|
86
98
|
}).id
|
|
87
99
|
: undefined)
|
|
88
100
|
|
|
89
|
-
session.addView('ProteinView', {
|
|
101
|
+
const proteinView = session.addView('ProteinView', {
|
|
90
102
|
type: 'ProteinView',
|
|
91
103
|
alignmentAlgorithm,
|
|
92
104
|
displayName,
|
|
@@ -106,6 +118,10 @@ export default function LaunchProteinViewExtensionPointF(
|
|
|
106
118
|
},
|
|
107
119
|
],
|
|
108
120
|
})
|
|
121
|
+
|
|
122
|
+
if (ownsConnectedView && (sideBySide ?? getLaunchSideBySide())) {
|
|
123
|
+
launchViewSideBySide(session, proteinView.id)
|
|
124
|
+
}
|
|
109
125
|
},
|
|
110
126
|
)
|
|
111
127
|
}
|