gramene-search 2.1.11 → 2.2.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/.claude/launch.json +11 -0
- package/.claude/settings.local.json +6 -1
- package/.env.example +6 -0
- package/.parcel-cache/13f2d5707e7af45c-RequestGraph +0 -0
- package/.parcel-cache/5ae0570a78c0dba3-AssetGraph +0 -0
- package/.parcel-cache/9ac092379278e465-BundleGraph +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/.parcel-cache/snapshot-13f2d5707e7af45c.txt +2 -2
- package/dist/index.js +2065 -154
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/bundles/api.js +10 -4
- package/src/bundles/exprViz.js +97 -1
- package/src/bundles/index.js +4 -1
- package/src/bundles/ontologyEnrichment.js +14 -1
- package/src/bundles/savedViews.js +335 -0
- package/src/bundles/uiViewState.js +174 -0
- package/src/bundles/viewSnapshot.js +313 -0
- package/src/bundles/views.js +22 -0
- package/src/components/Auth.js +23 -3
- package/src/components/SaveView.js +157 -0
- package/src/components/exprViz/ExprVizView.js +16 -11
- package/src/components/exprViz/ParallelCoordsPlot.js +15 -0
- package/src/components/results/GeneList.js +45 -15
- package/src/components/results/OntologyEnrichment.js +13 -6
- package/src/components/results/details/BAR.js +148 -0
- package/src/components/results/details/Expression.js +43 -11
- package/src/components/results/details/Homology.js +170 -32
- package/src/components/results/details/Pathways.js +4 -2
- package/src/components/results/details/Sequences.js +24 -8
- package/src/demo.js +22 -17
- package/src/index.js +2 -1
- package/src/suppressDevWarnings.js +13 -0
- package/src/utils/bootView.js +38 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState } from 'react'
|
|
2
2
|
import {connect} from "redux-bundler-react";
|
|
3
3
|
import {Tabs, Tab, Form, Row, Col} from 'react-bootstrap';
|
|
4
|
-
import BAR, {haveBAR} from "
|
|
4
|
+
import BAR, {haveBAR} from "./BAR";
|
|
5
5
|
|
|
6
6
|
function DynamicIframe(props) {
|
|
7
7
|
// Create a ref for the iframe element
|
|
@@ -31,16 +31,35 @@ function DynamicIframe(props) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const Detail = props => {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
34
|
+
const geneId = props.searchResult.id;
|
|
35
|
+
const gene = props.geneDocs[geneId];
|
|
36
|
+
// User-selected view state (active sub-tab, chosen GXA experiment, chosen eFP
|
|
37
|
+
// study) lives in the uiViewState bundle keyed by geneId, so the shareable-
|
|
38
|
+
// views snapshot can round-trip it. Fetched data + the dev-only Local API
|
|
39
|
+
// toggle stay in local state. Defaults match the old local-state initials.
|
|
40
|
+
const expr = (props.uiViewState && props.uiViewState.byGene[geneId]
|
|
41
|
+
&& props.uiViewState.byGene[geneId].expression) || {};
|
|
42
|
+
const activeTab = expr.activeTab || 'gene';
|
|
43
|
+
const atlasExperiment = expr.atlasExperiment || null;
|
|
44
|
+
const setActiveTab = (k) => props.doSetExpressionState({geneId, patch: {activeTab: k}});
|
|
45
|
+
const setAtlasExperiment = (v) => props.doSetExpressionState({geneId, patch: {atlasExperiment: v}});
|
|
36
46
|
const [atlasExperimentList, setAtlasExperimentList] = useState([]);
|
|
37
47
|
const [atlasFacets, setAtlasFacets] = useState(null);
|
|
38
48
|
const [isLocal, setIsLocal] = useState(false);
|
|
39
|
-
const [activeTab, setActiveTab] = useState('gene');
|
|
40
49
|
|
|
41
50
|
const handleLocalAPIChange = (event) => {
|
|
42
51
|
setIsLocal(event.target.checked);
|
|
43
52
|
};
|
|
53
|
+
// The expressionStudies resource is otherwise fetched only when a top-level
|
|
54
|
+
// expression view (exprViz/expression/export) is on — but this per-gene
|
|
55
|
+
// Expression detail also needs it (the Paralogs sub-tab's experiment list and
|
|
56
|
+
// the atlasExperiment selection both derive from it). Self-fetch on mount so
|
|
57
|
+
// opening the detail populates the studies even when no such view is enabled.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!props.expressionStudies && props.doFetchExpressionStudies) {
|
|
60
|
+
props.doFetchExpressionStudies();
|
|
61
|
+
}
|
|
62
|
+
}, [props.expressionStudies]);
|
|
44
63
|
useEffect(() => {
|
|
45
64
|
if (!props.expressionStudies) return;
|
|
46
65
|
const tid = Math.floor(gene.taxon_id / 1000);
|
|
@@ -58,12 +77,17 @@ const Detail = props => {
|
|
|
58
77
|
eList.forEach(e => {e.factors.forEach(factor => facets[e.type][factor] = 1);});
|
|
59
78
|
setAtlasExperimentList(eList);
|
|
60
79
|
setAtlasFacets(facets);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
// Only pick a default experiment when the user (or a restored snapshot)
|
|
81
|
+
// hasn't already chosen one — otherwise we'd clobber a saved selection
|
|
82
|
+
// the moment the studies list loads.
|
|
83
|
+
if (!atlasExperiment) {
|
|
84
|
+
let refExp = eList.filter(e => e.isRef);
|
|
85
|
+
if (refExp.length === 1) {
|
|
86
|
+
setAtlasExperiment(refExp[0]._id);
|
|
87
|
+
} else {
|
|
88
|
+
// no reference experiment - choose first
|
|
89
|
+
setAtlasExperiment(eList[0]._id);
|
|
90
|
+
}
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
}, [props.expressionStudies]);
|
|
@@ -91,6 +115,7 @@ const Detail = props => {
|
|
|
91
115
|
<Tab tabClassName="gxa" eventKey="paralogs" title={`Paralogs`} key="gxaparalogs">
|
|
92
116
|
<Form.Select aria-label='experiment selector'
|
|
93
117
|
placeholder='Select experiment'
|
|
118
|
+
value={atlasExperiment || ''}
|
|
94
119
|
onChange={(e) => setAtlasExperiment(e.target.value)}>
|
|
95
120
|
{ atlasExperimentList.map((e,idx) =>
|
|
96
121
|
<option key={idx} value={e._id}>{e.type}: {e.description || e._id}</option>
|
|
@@ -110,7 +135,11 @@ const Detail = props => {
|
|
|
110
135
|
{activeTab === "gene" && <DynamicIframe url={gene_url}/> }
|
|
111
136
|
</Tab>
|
|
112
137
|
{haveBAR(gene) &&
|
|
113
|
-
<Tab tabClassName="eFP" eventKey="eFP" title="eFP Browser" key="bar"
|
|
138
|
+
<Tab tabClassName="eFP" eventKey="eFP" title="eFP Browser" key="bar">
|
|
139
|
+
<BAR gene={gene}
|
|
140
|
+
study={expr.barStudy}
|
|
141
|
+
onStudyChange={v => props.doSetExpressionState({geneId, patch: {barStudy: v}})}/>
|
|
142
|
+
</Tab>
|
|
114
143
|
}
|
|
115
144
|
</Tabs>
|
|
116
145
|
};
|
|
@@ -118,7 +147,10 @@ const Detail = props => {
|
|
|
118
147
|
export default connect(
|
|
119
148
|
'selectGrameneParalogs',
|
|
120
149
|
'selectExpressionStudies',
|
|
150
|
+
'selectUiViewState',
|
|
121
151
|
'doRequestParalogs',
|
|
152
|
+
'doFetchExpressionStudies',
|
|
153
|
+
'doSetExpressionState',
|
|
122
154
|
//'doRequestParalogExpression',
|
|
123
155
|
Detail
|
|
124
156
|
);
|
|
@@ -2,7 +2,10 @@ import React from 'react'
|
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import {connect} from "redux-bundler-react";
|
|
4
4
|
import TreeVis from "gramene-genetree-vis";
|
|
5
|
-
|
|
5
|
+
// Subpath imports avoid gramene-trees-client/index.js, whose require chain
|
|
6
|
+
// fires a /swagger fetch at module load (see comment in bundles/api.js).
|
|
7
|
+
import taxonomy from "gramene-trees-client/src/taxonomy";
|
|
8
|
+
import genetree from "gramene-trees-client/src/genetree";
|
|
6
9
|
import {
|
|
7
10
|
TBrowse,
|
|
8
11
|
computePivotState,
|
|
@@ -28,15 +31,123 @@ const TBROWSE_ZONES = [treeZone, labelsZone, msaZone, neighborhoodZone, genomeZo
|
|
|
28
31
|
class Homology extends React.Component {
|
|
29
32
|
constructor(props) {
|
|
30
33
|
super(props);
|
|
31
|
-
|
|
34
|
+
// Async data caches stay in local state (per mount, per tree). The
|
|
35
|
+
// user-controlled view state (viewer toggle, resize height, tbrowse
|
|
36
|
+
// ViewState) is lifted into the uiViewState bundle, keyed by geneId.
|
|
37
|
+
//
|
|
38
|
+
// `*Status` fields track per-zone load lifecycle so the TBrowse
|
|
39
|
+
// toolbar can render loading/error affordances via its zoneStatus
|
|
40
|
+
// prop. They start `undefined` (no signal); flipped to 'loading'
|
|
41
|
+
// when the fetch kicks off and 'ready' / 'error' on settle.
|
|
42
|
+
this.state = {
|
|
43
|
+
neighborhood: null,
|
|
44
|
+
neighborhoodTreeId: null,
|
|
45
|
+
neighborhoodStatus: undefined,
|
|
46
|
+
geneStructures: null,
|
|
47
|
+
geneStructuresTreeId: null,
|
|
48
|
+
geneStructuresStatus: undefined,
|
|
49
|
+
};
|
|
32
50
|
if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) {
|
|
33
51
|
props.requestGene(props.searchResult.id)
|
|
34
52
|
}
|
|
35
|
-
this.taxonomy =
|
|
53
|
+
this.taxonomy = taxonomy.tree(Object.values(props.grameneTaxonomy))
|
|
54
|
+
}
|
|
55
|
+
// ----- uiViewState accessors (with sensible defaults) -----
|
|
56
|
+
getGeneId() { return this.props.searchResult.id; }
|
|
57
|
+
getHomologySlice() {
|
|
58
|
+
const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.getGeneId()];
|
|
59
|
+
return (slice && slice.homology) || {};
|
|
60
|
+
}
|
|
61
|
+
getViewer() { return this.getHomologySlice().viewer || 'treevis'; }
|
|
62
|
+
getHeight() {
|
|
63
|
+
const h = this.getHomologySlice().height;
|
|
64
|
+
return typeof h === 'number' ? h : 600;
|
|
65
|
+
}
|
|
66
|
+
setViewer(viewer) {
|
|
67
|
+
this.props.doSetHomologyViewer({geneId: this.getGeneId(), viewer});
|
|
68
|
+
}
|
|
69
|
+
setHeight(height) {
|
|
70
|
+
this.props.doSetHomologyHeight({geneId: this.getGeneId(), height});
|
|
71
|
+
}
|
|
72
|
+
setTbrowseViewState(viewState) {
|
|
73
|
+
this.props.doSetHomologyTbrowseViewState({geneId: this.getGeneId(), tbrowse: viewState});
|
|
74
|
+
}
|
|
75
|
+
// Seed the bundle with a pivot-computed initial tbrowse view state once the
|
|
76
|
+
// tree data is available and the user is actually looking at the tbrowse
|
|
77
|
+
// viewer. Called from lifecycle (not render) to avoid dispatching mid-render.
|
|
78
|
+
maybeSeedTbrowseViewState() {
|
|
79
|
+
if (this.getViewer() !== 'tbrowse') return;
|
|
80
|
+
if (this.getHomologySlice().tbrowse) return;
|
|
81
|
+
const id = this.getGeneId();
|
|
82
|
+
if (!this.props.geneDocs.hasOwnProperty(id)) return;
|
|
83
|
+
const gene = this.props.geneDocs[id];
|
|
84
|
+
if (!gene.homology) return;
|
|
85
|
+
const treeId = gene.homology.gene_tree.id;
|
|
86
|
+
const raw = this.props.grameneTrees[treeId];
|
|
87
|
+
if (!raw || !raw.taxon_id) return;
|
|
88
|
+
const adapted = fromGrameneGenetree([raw]);
|
|
89
|
+
const pivot = computePivotState(adapted.tree, gene._id);
|
|
90
|
+
this.setTbrowseViewState({
|
|
91
|
+
selectedNodeId: null,
|
|
92
|
+
collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
|
|
93
|
+
prunedNodeIds: [],
|
|
94
|
+
swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
|
|
95
|
+
compressedNodeIds: [],
|
|
96
|
+
nodeOfInterestId: pivot ? pivot.targetId : null,
|
|
97
|
+
// Use each zone's intended fr-share + initial visibility from its
|
|
98
|
+
// definition (tree:30 / labels:20 / msa:50 / neighborhood:30 /
|
|
99
|
+
// genome:32) instead of uniform 25 across the board. Gives the MSA
|
|
100
|
+
// a wider initial pane and the tree a narrower one.
|
|
101
|
+
//
|
|
102
|
+
// `defaultVisible ?? true` matches tbrowse's own buildInitialViewState
|
|
103
|
+
// logic — zones default to visible unless their definition opts out
|
|
104
|
+
// (e.g. neighborhood and genome opt out so they only appear once
|
|
105
|
+
// their async data lands; tbrowse's Layout auto-flips them on then).
|
|
106
|
+
zones: TBROWSE_ZONES.map(z => ({
|
|
107
|
+
id: z.id,
|
|
108
|
+
width: z.defaultWidth,
|
|
109
|
+
visible: z.defaultVisible ?? true
|
|
110
|
+
})),
|
|
111
|
+
zoneStates: {},
|
|
112
|
+
search: null,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
componentDidMount() {
|
|
116
|
+
this.maybeSeedTbrowseViewState();
|
|
117
|
+
this.maybeFetchTbrowseData();
|
|
118
|
+
}
|
|
119
|
+
componentDidUpdate() {
|
|
120
|
+
this.maybeSeedTbrowseViewState();
|
|
121
|
+
this.maybeFetchTbrowseData();
|
|
122
|
+
}
|
|
123
|
+
// Kick off the neighborhood + gene-structure fetches from lifecycle,
|
|
124
|
+
// not render. Each fetch internally dedupes on `_*FetchedFor === treeId`
|
|
125
|
+
// so calling on every commit is cheap. Gated on the user actually being
|
|
126
|
+
// on the tbrowse viewer + the tree data being loaded — otherwise we'd
|
|
127
|
+
// pay network cost for genes the user never looks at.
|
|
128
|
+
maybeFetchTbrowseData() {
|
|
129
|
+
if (this.getViewer() !== 'tbrowse') return;
|
|
130
|
+
const id = this.getGeneId();
|
|
131
|
+
if (!this.props.geneDocs.hasOwnProperty(id)) return;
|
|
132
|
+
const gene = this.props.geneDocs[id];
|
|
133
|
+
if (!gene.homology) return;
|
|
134
|
+
const treeId = gene.homology.gene_tree.id;
|
|
135
|
+
const raw = this.props.grameneTrees[treeId];
|
|
136
|
+
if (!raw || !raw.taxon_id) return;
|
|
137
|
+
// Lazily compute the adapted tbrowse data the same way renderTBrowse
|
|
138
|
+
// does, so fetchGeneStructures has the leaf ids without rerunning
|
|
139
|
+
// fromGrameneGenetree on every commit.
|
|
140
|
+
if (this._tbrowseTreeId !== treeId) {
|
|
141
|
+
this._tbrowseTreeId = treeId;
|
|
142
|
+
this._tbrowseData = fromGrameneGenetree([raw]);
|
|
143
|
+
}
|
|
144
|
+
this.fetchNeighborhood(treeId);
|
|
145
|
+
this.fetchGeneStructures(treeId, this._tbrowseData.tree);
|
|
36
146
|
}
|
|
37
147
|
fetchNeighborhood(treeId) {
|
|
38
148
|
if (this._neighborhoodFetchedFor === treeId) return;
|
|
39
149
|
this._neighborhoodFetchedFor = treeId;
|
|
150
|
+
this.setState({neighborhoodStatus: 'loading'});
|
|
40
151
|
const api = this.props.grameneAPI;
|
|
41
152
|
const url = new URL(`${api}/search`);
|
|
42
153
|
url.searchParams.set('fl', 'id,name,gene_tree,gene_idx,region,start,end,strand,biotype,system_name,description');
|
|
@@ -50,10 +161,19 @@ class Homology extends React.Component {
|
|
|
50
161
|
})
|
|
51
162
|
.then(json => {
|
|
52
163
|
if (this._neighborhoodFetchedFor !== treeId) return;
|
|
53
|
-
this.setState({
|
|
164
|
+
this.setState({
|
|
165
|
+
neighborhood: fromGrameneNeighborhood(json),
|
|
166
|
+
neighborhoodTreeId: treeId,
|
|
167
|
+
neighborhoodStatus: 'ready',
|
|
168
|
+
});
|
|
54
169
|
})
|
|
55
170
|
.catch(err => {
|
|
56
171
|
console.warn('tbrowse neighborhood fetch failed:', err);
|
|
172
|
+
// Surface the failure via the zoneStatus prop. Reset the
|
|
173
|
+
// dedupe key so a re-mount or tree-change can retry.
|
|
174
|
+
if (this._neighborhoodFetchedFor === treeId) {
|
|
175
|
+
this.setState({neighborhoodStatus: 'error'});
|
|
176
|
+
}
|
|
57
177
|
this._neighborhoodFetchedFor = null;
|
|
58
178
|
});
|
|
59
179
|
}
|
|
@@ -64,6 +184,7 @@ class Homology extends React.Component {
|
|
|
64
184
|
.filter(n => n.isLeaf && n.geneId)
|
|
65
185
|
.map(n => n.geneId);
|
|
66
186
|
if (ids.length === 0) return;
|
|
187
|
+
this.setState({geneStructuresStatus: 'loading'});
|
|
67
188
|
const api = this.props.grameneAPI;
|
|
68
189
|
const BATCH_SIZE = 50;
|
|
69
190
|
const batches = [];
|
|
@@ -84,21 +205,28 @@ class Homology extends React.Component {
|
|
|
84
205
|
.then(results => {
|
|
85
206
|
if (this._geneStructuresFetchedFor !== treeId) return;
|
|
86
207
|
const combined = [].concat(...results.map(r => Array.isArray(r) ? r : []));
|
|
87
|
-
this.setState({
|
|
208
|
+
this.setState({
|
|
209
|
+
geneStructures: fromGrameneGeneStructures(combined),
|
|
210
|
+
geneStructuresTreeId: treeId,
|
|
211
|
+
geneStructuresStatus: 'ready',
|
|
212
|
+
});
|
|
88
213
|
})
|
|
89
214
|
.catch(err => {
|
|
90
215
|
console.warn('tbrowse gene-structures fetch failed:', err);
|
|
216
|
+
if (this._geneStructuresFetchedFor === treeId) {
|
|
217
|
+
this.setState({geneStructuresStatus: 'error'});
|
|
218
|
+
}
|
|
91
219
|
this._geneStructuresFetchedFor = null;
|
|
92
220
|
});
|
|
93
221
|
}
|
|
94
222
|
startResize(e) {
|
|
95
223
|
e.preventDefault();
|
|
96
224
|
const startY = e.clientY;
|
|
97
|
-
const startHeight = this.
|
|
225
|
+
const startHeight = this.getHeight();
|
|
98
226
|
|
|
99
227
|
const onMouseMove = (moveEvent) => {
|
|
100
228
|
const newHeight = Math.max(200, startHeight + (moveEvent.clientY - startY));
|
|
101
|
-
this.
|
|
229
|
+
this.setHeight(newHeight);
|
|
102
230
|
};
|
|
103
231
|
|
|
104
232
|
const onMouseUp = () => {
|
|
@@ -122,7 +250,7 @@ class Homology extends React.Component {
|
|
|
122
250
|
renderTreeVis() {
|
|
123
251
|
return (
|
|
124
252
|
<>
|
|
125
|
-
<div className="gene-genetree" style={{height: this.
|
|
253
|
+
<div className="gene-genetree" style={{height: this.getHeight(), width: '100%'}}>
|
|
126
254
|
<TreeVis genetree={this.tree}
|
|
127
255
|
initialGeneOfInterest={this.gene}
|
|
128
256
|
genomesOfInterest={this.props.grameneGenomes.active}
|
|
@@ -140,32 +268,35 @@ class Homology extends React.Component {
|
|
|
140
268
|
}
|
|
141
269
|
renderTBrowse() {
|
|
142
270
|
const treeId = this.gene.homology.gene_tree.id;
|
|
271
|
+
// Adapted tbrowse data is computed lazily in maybeFetchTbrowseData
|
|
272
|
+
// (called from lifecycle). If the user just flipped to tbrowse and
|
|
273
|
+
// the lifecycle hasn't fired yet, compute it once here without
|
|
274
|
+
// calling any fetches — those will fire from componentDidUpdate.
|
|
143
275
|
if (this._tbrowseTreeId !== treeId) {
|
|
144
|
-
const raw = this.props.grameneTrees[treeId];
|
|
145
|
-
const adapted = fromGrameneGenetree([raw]);
|
|
146
276
|
this._tbrowseTreeId = treeId;
|
|
147
|
-
this._tbrowseData =
|
|
148
|
-
const pivot = computePivotState(adapted.tree, this.gene._id);
|
|
149
|
-
const zoneIds = TBROWSE_ZONES.map(z => z.id);
|
|
150
|
-
this._tbrowseInitialViewState = {
|
|
151
|
-
selectedNodeId: null,
|
|
152
|
-
collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
|
|
153
|
-
prunedNodeIds: [],
|
|
154
|
-
swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
|
|
155
|
-
compressedNodeIds: [],
|
|
156
|
-
nodeOfInterestId: pivot ? pivot.targetId : null,
|
|
157
|
-
zones: zoneIds.map(id => ({id, width: 25, visible: true})),
|
|
158
|
-
zoneStates: {},
|
|
159
|
-
search: null,
|
|
160
|
-
};
|
|
277
|
+
this._tbrowseData = fromGrameneGenetree([this.props.grameneTrees[treeId]]);
|
|
161
278
|
}
|
|
162
|
-
this.fetchNeighborhood(treeId);
|
|
163
|
-
this.fetchGeneStructures(treeId, this._tbrowseData.tree);
|
|
164
279
|
const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
|
|
165
280
|
const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
|
|
281
|
+
// Per-zone status for the TBrowse toolbar — pulses while a fetch
|
|
282
|
+
// is in flight, turns red on failure. Tracked per-tree so a
|
|
283
|
+
// tree-change resets any stale 'ready'/'error' from the prior gene.
|
|
284
|
+
const zoneStatus = {
|
|
285
|
+
neighborhood: this.state.neighborhoodTreeId === treeId
|
|
286
|
+
? this.state.neighborhoodStatus
|
|
287
|
+
: (this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined),
|
|
288
|
+
genome: this.state.geneStructuresTreeId === treeId
|
|
289
|
+
? this.state.geneStructuresStatus
|
|
290
|
+
: (this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined),
|
|
291
|
+
};
|
|
292
|
+
// Bundle-driven (controlled) view state. If we're rendering tbrowse before
|
|
293
|
+
// componentDidMount/Update has seeded the bundle slice, skip this turn and
|
|
294
|
+
// let the re-render with the seeded state do the work.
|
|
295
|
+
const tbrowseVS = this.getHomologySlice().tbrowse;
|
|
296
|
+
if (!tbrowseVS) return null;
|
|
166
297
|
return (
|
|
167
298
|
<>
|
|
168
|
-
<div className="gene-genetree" style={{height: this.
|
|
299
|
+
<div className="gene-genetree" style={{height: this.getHeight(), width: '100%'}}>
|
|
169
300
|
<TBrowse
|
|
170
301
|
tree={this._tbrowseData.tree}
|
|
171
302
|
taxonomy={this._tbrowseData.taxonomy}
|
|
@@ -177,7 +308,10 @@ class Homology extends React.Component {
|
|
|
177
308
|
geneStructures={geneStructures}
|
|
178
309
|
zones={TBROWSE_ZONES}
|
|
179
310
|
nodeOfInterest={this.gene._id}
|
|
180
|
-
|
|
311
|
+
viewState={tbrowseVS}
|
|
312
|
+
onViewStateChange={next => this.setTbrowseViewState(next)}
|
|
313
|
+
defaultOpenSections={{ zones: true, search: true }}
|
|
314
|
+
zoneStatus={zoneStatus}
|
|
181
315
|
/>
|
|
182
316
|
</div>
|
|
183
317
|
{this.renderResizeHandle()}
|
|
@@ -185,12 +319,12 @@ class Homology extends React.Component {
|
|
|
185
319
|
)
|
|
186
320
|
}
|
|
187
321
|
renderViewerToggle() {
|
|
188
|
-
const
|
|
322
|
+
const viewer = this.getViewer();
|
|
189
323
|
const btn = (id, label) => (
|
|
190
324
|
<button
|
|
191
325
|
key={id}
|
|
192
326
|
type="button"
|
|
193
|
-
onClick={() => this.
|
|
327
|
+
onClick={() => this.setViewer(id)}
|
|
194
328
|
style={{
|
|
195
329
|
padding: '4px 10px',
|
|
196
330
|
marginRight: 4,
|
|
@@ -336,7 +470,7 @@ class Homology extends React.Component {
|
|
|
336
470
|
else {
|
|
337
471
|
const tree = this.props.grameneTrees[treeId];
|
|
338
472
|
if (tree.hasOwnProperty('taxon_id')) {
|
|
339
|
-
this.tree =
|
|
473
|
+
this.tree = genetree.tree([this.props.grameneTrees[treeId]]);
|
|
340
474
|
this.orthologs = this.orthologList();
|
|
341
475
|
this.paralogs = this.paralogList();
|
|
342
476
|
}
|
|
@@ -349,7 +483,7 @@ class Homology extends React.Component {
|
|
|
349
483
|
</Description>
|
|
350
484
|
{this.tree && <Content key="content">
|
|
351
485
|
{this.renderViewerToggle()}
|
|
352
|
-
{this.
|
|
486
|
+
{this.getViewer() === 'tbrowse' ? this.renderTBrowse() : this.renderTreeVis()}
|
|
353
487
|
</Content>}
|
|
354
488
|
{this.tree && <Explore key="explore" explorations={this.explorations()}/>}
|
|
355
489
|
<Links key="links" links={this.links()}/>
|
|
@@ -365,7 +499,11 @@ export default connect(
|
|
|
365
499
|
'selectGrameneAPI',
|
|
366
500
|
'selectConfiguration',
|
|
367
501
|
'selectCuration',
|
|
502
|
+
'selectUiViewState',
|
|
368
503
|
'doRequestGrameneTree',
|
|
504
|
+
'doSetHomologyViewer',
|
|
505
|
+
'doSetHomologyHeight',
|
|
506
|
+
'doSetHomologyTbrowseViewState',
|
|
369
507
|
'doAcceptGrameneSuggestion',
|
|
370
508
|
'doReplaceGrameneFilters',
|
|
371
509
|
Homology
|
|
@@ -2,7 +2,9 @@ import React from 'react'
|
|
|
2
2
|
import {connect} from "redux-bundler-react";
|
|
3
3
|
import FlatToNested from 'flat-to-nested';
|
|
4
4
|
import {Explore, Links} from "./generic";
|
|
5
|
-
import
|
|
5
|
+
// Subpath import avoids gramene-trees-client/index.js, whose require chain
|
|
6
|
+
// fires a /swagger fetch at module load (see comment in bundles/api.js).
|
|
7
|
+
import taxonomy from "gramene-trees-client/src/taxonomy";
|
|
6
8
|
import TreeMenu from "react-simple-tree-menu";
|
|
7
9
|
import '../../../../node_modules/react-simple-tree-menu/dist/main.css';
|
|
8
10
|
|
|
@@ -101,7 +103,7 @@ function buildIframeSrcDoc() {
|
|
|
101
103
|
class Pathways extends React.Component {
|
|
102
104
|
constructor(props) {
|
|
103
105
|
super(props);
|
|
104
|
-
this.taxonomy =
|
|
106
|
+
this.taxonomy = taxonomy.tree(Object.values(props.grameneTaxonomy))
|
|
105
107
|
this.gene = props.geneDocs[props.searchResult.id];
|
|
106
108
|
this.iframeRef = React.createRef();
|
|
107
109
|
// srcDoc is constant — the per-instance pathway/reaction state is
|
|
@@ -169,22 +169,36 @@ const buildId = (gene, geneSeq, up, down) => {
|
|
|
169
169
|
return `${geneSeq.genome}|${gene._id}|${gene.location.region}:${gs}..${ge} ${extras.join('|')}`
|
|
170
170
|
};
|
|
171
171
|
const Detail = props => {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
const geneId = props.searchResult.id;
|
|
173
|
+
const gene = props.geneDocs[geneId];
|
|
174
|
+
// Sub-tab + selected isoform + flanking lengths live in the uiViewState
|
|
175
|
+
// bundle (keyed by geneId) so the shareable-views snapshot can round-trip
|
|
176
|
+
// them. Defaults match the old local-state initial values.
|
|
177
|
+
const slice = (props.uiViewState && props.uiViewState.byGene[geneId]
|
|
178
|
+
&& props.uiViewState.byGene[geneId].sequences) || {};
|
|
179
|
+
const tab = slice.tab || 'dna';
|
|
180
|
+
const tid = slice.tid || gene.gene_structure.canonical_transcript;
|
|
181
|
+
const upstream = slice.upstream || 0;
|
|
182
|
+
const downstream = slice.downstream || 0;
|
|
183
|
+
const setTab = (k) => props.doSetSequencesState({geneId, patch: {tab: k}});
|
|
184
|
+
const setTid = (v) => props.doSetSequencesState({geneId, patch: {tid: v}});
|
|
185
|
+
const setUpstream = (v) => props.doSetSequencesState({geneId, patch: {upstream: +v}});
|
|
186
|
+
const setDownstream = (v) => props.doSetSequencesState({geneId, patch: {downstream: +v}});
|
|
177
187
|
let geneSeq;
|
|
178
188
|
let rnaSeq;
|
|
179
189
|
let pepSeq;
|
|
180
|
-
|
|
190
|
+
// The *_SEQUENCE_REQUESTED reducer inserts a `{}` placeholder before the
|
|
191
|
+
// fetch resolves, so a key-only existence check passes while the body is
|
|
192
|
+
// still empty — leading to `seq.substring` of undefined downstream. Gate
|
|
193
|
+
// on the actual payload field instead.
|
|
194
|
+
if (props.geneSequences && props.geneSequences[gene._id] && props.geneSequences[gene._id].seq) {
|
|
181
195
|
geneSeq = props.geneSequences[gene._id];
|
|
182
196
|
}
|
|
183
197
|
else {
|
|
184
198
|
props.doRequestGeneSequence(gene)
|
|
185
199
|
return <pre>loading</pre>;
|
|
186
200
|
}
|
|
187
|
-
if (props.rnaSequences && props.rnaSequences[tid]) {
|
|
201
|
+
if (props.rnaSequences && props.rnaSequences[tid] && props.rnaSequences[tid].seq) {
|
|
188
202
|
rnaSeq = props.rnaSequences[tid]
|
|
189
203
|
}
|
|
190
204
|
else {
|
|
@@ -197,7 +211,7 @@ const Detail = props => {
|
|
|
197
211
|
let tl_id;
|
|
198
212
|
if (transcript.translation) {
|
|
199
213
|
tl_id = transcript.translation.id;
|
|
200
|
-
if (props.pepSequences && props.pepSequences[tl_id]) {
|
|
214
|
+
if (props.pepSequences && props.pepSequences[tl_id] && props.pepSequences[tl_id].seq) {
|
|
201
215
|
pepSeq = props.pepSequences[tl_id];
|
|
202
216
|
}
|
|
203
217
|
else {
|
|
@@ -329,9 +343,11 @@ export default connect(
|
|
|
329
343
|
'selectGeneSequences',
|
|
330
344
|
'selectRnaSequences',
|
|
331
345
|
'selectPepSequences',
|
|
346
|
+
'selectUiViewState',
|
|
332
347
|
'doRequestGeneSequence',
|
|
333
348
|
'doRequestRnaSequence',
|
|
334
349
|
'doRequestPepSequence',
|
|
350
|
+
'doSetSequencesState',
|
|
335
351
|
Detail
|
|
336
352
|
);
|
|
337
353
|
|
package/src/demo.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "react-router-dom";
|
|
22
22
|
import Feedback from './components/Feedback';
|
|
23
23
|
import MDView from 'gramene-mdview';
|
|
24
|
+
import bootViewFromUrl from './utils/bootView';
|
|
24
25
|
|
|
25
26
|
// const subsite = 'main';
|
|
26
27
|
const subsite = process.env.SUBSITE;
|
|
@@ -56,14 +57,6 @@ const panSites = [
|
|
|
56
57
|
targetTaxonId: 3702,
|
|
57
58
|
alertText: 'Main site',
|
|
58
59
|
showViews: true,
|
|
59
|
-
firebaseConfig: {
|
|
60
|
-
apiKey: "AIzaSyCyTJmxfWgfuhI6-8uqocSiE9KOWUlkgkk",
|
|
61
|
-
authDomain: "gramene-auth.firebaseapp.com",
|
|
62
|
-
projectId: "gramene-auth",
|
|
63
|
-
storageBucket: "gramene-auth.appspot.com",
|
|
64
|
-
messagingSenderId: "590873346270",
|
|
65
|
-
appId: "1:590873346270:web:f76a31a93619e69439824f"
|
|
66
|
-
},
|
|
67
60
|
details: {
|
|
68
61
|
sequences: true,
|
|
69
62
|
VEP: false,
|
|
@@ -153,20 +146,12 @@ const panSites = [
|
|
|
153
146
|
ensemblURL: 'https://ensembl.sorghumbase.org',
|
|
154
147
|
ensemblSite: 'https://ensembl.sorghumbase.org',
|
|
155
148
|
ensemblRest: 'https://data.gramene.org/pansite-ensembl-108',
|
|
156
|
-
grameneData: 'https://data.sorghumbase.org/
|
|
149
|
+
grameneData: 'https://data.sorghumbase.org/sorghum_v10',
|
|
157
150
|
ga: 'G-L5KXDCCZ16',
|
|
158
151
|
targetTaxonId: 4558001,
|
|
159
152
|
alertText: 'Click the search icon in the menu bar or type /',
|
|
160
153
|
showViews: true,
|
|
161
154
|
partialCompara: true,
|
|
162
|
-
firebaseConfig: {
|
|
163
|
-
apiKey: "AIzaSyCyTJmxfWgfuhI6-8uqocSiE9KOWUlkgkk",
|
|
164
|
-
authDomain: "gramene-auth.firebaseapp.com",
|
|
165
|
-
projectId: "gramene-auth",
|
|
166
|
-
storageBucket: "gramene-auth.appspot.com",
|
|
167
|
-
messagingSenderId: "590873346270",
|
|
168
|
-
appId: "1:590873346270:web:f76a31a93619e69439824f"
|
|
169
|
-
},
|
|
170
155
|
details: {
|
|
171
156
|
sequences: true,
|
|
172
157
|
VEP: true,
|
|
@@ -285,6 +270,23 @@ const panSites = [
|
|
|
285
270
|
}
|
|
286
271
|
}
|
|
287
272
|
];
|
|
273
|
+
|
|
274
|
+
// Firebase web config is loaded from FIREBASE_CONFIG_JSON in the build-time
|
|
275
|
+
// env (.env, never committed). If unset, the Auth panel mounts but stays
|
|
276
|
+
// inert. Attached to the sites that use gramene-auth.
|
|
277
|
+
const firebaseConfig = (() => {
|
|
278
|
+
const raw = process.env.FIREBASE_CONFIG_JSON;
|
|
279
|
+
if (!raw) return null;
|
|
280
|
+
try { return JSON.parse(raw); }
|
|
281
|
+
catch (e) { console.warn('FIREBASE_CONFIG_JSON is not valid JSON; Auth disabled.', e); return null; }
|
|
282
|
+
})();
|
|
283
|
+
if (firebaseConfig) {
|
|
284
|
+
['main', 'sorghum'].forEach(id => {
|
|
285
|
+
const site = panSites[subsitelut[id]];
|
|
286
|
+
if (site) site.firebaseConfig = firebaseConfig;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
288
290
|
const initialState = Object.assign({helpIsOn:false}, panSites[subsitelut[subsite]]);
|
|
289
291
|
|
|
290
292
|
const config = {
|
|
@@ -546,6 +548,9 @@ cache.getAll().then(initialData => {
|
|
|
546
548
|
window.store = store;
|
|
547
549
|
const config = store.selectConfiguration();
|
|
548
550
|
ReactGA.initialize(config.ga);
|
|
551
|
+
// If the URL has ?view=<hash>, hydrate the snapshot before the user
|
|
552
|
+
// interacts. Errors are non-fatal (see utils/bootView.js).
|
|
553
|
+
bootViewFromUrl(store);
|
|
549
554
|
if (initialData.hasOwnProperty('grameneMaps')) {
|
|
550
555
|
// check for hidden genomes
|
|
551
556
|
let notHidden = {};
|
package/src/index.js
CHANGED
|
@@ -3,5 +3,6 @@ import resultList from './components/resultList';
|
|
|
3
3
|
import resultSummary from './components/resultSummary';
|
|
4
4
|
import suggestions from './components/suggestions';
|
|
5
5
|
import {Status, Filters, Results, Views, Auth} from './components/geneSearchUI';
|
|
6
|
+
import bootViewFromUrl from './utils/bootView';
|
|
6
7
|
|
|
7
|
-
export { bundles, resultList, resultSummary, suggestions, Status, Filters, Results, Views, Auth };
|
|
8
|
+
export { bundles, resultList, resultSummary, suggestions, Status, Filters, Results, Views, Auth, bootViewFromUrl };
|
|
@@ -12,6 +12,19 @@ if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
|
12
12
|
'uses the legacy childContextTypes API',
|
|
13
13
|
'uses the legacy contextTypes API',
|
|
14
14
|
'findDOMNode is deprecated',
|
|
15
|
+
// Third-party libs (gramene-search-vis, gramene-mdview, Pathways, etc.)
|
|
16
|
+
// still use the pre-16.3 lifecycle methods. We can't fix them from here.
|
|
17
|
+
'componentWillReceiveProps has been renamed',
|
|
18
|
+
'componentWillMount has been renamed',
|
|
19
|
+
'componentWillUpdate has been renamed',
|
|
20
|
+
// Kept as a guard — react warns on any kebab-case inline-style property
|
|
21
|
+
// and Parcel's overlay turns that into a fatal runtime error. The known
|
|
22
|
+
// offender (BAR's max-width) was fixed when the component was absorbed
|
|
23
|
+
// from gramene-efp-browser, but the suppressor protects against any
|
|
24
|
+
// future regression in third-party libs.
|
|
25
|
+
'Unsupported style property',
|
|
26
|
+
// Stale-state setState after unmount in legacy class components.
|
|
27
|
+
"Can't perform a React state update on an unmounted component",
|
|
15
28
|
];
|
|
16
29
|
console.error = function (...args) {
|
|
17
30
|
const msg = typeof args[0] === 'string' ? args[0] : '';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Boot-time hydration of a shared view link.
|
|
2
|
+
//
|
|
3
|
+
// Entry points call `bootViewFromUrl(store)` once the store is created.
|
|
4
|
+
// Public views resolve anonymously. For private views, the initial call
|
|
5
|
+
// will 401; the Auth panel then re-invokes with `{user}` once Firebase has
|
|
6
|
+
// emitted its first signed-in state, and the fetch retries with a Bearer
|
|
7
|
+
// token. On success we apply the snapshot and strip `?view=` from the URL
|
|
8
|
+
// so subsequent user actions don't sit under a stale shared-state URL.
|
|
9
|
+
//
|
|
10
|
+
// All errors are non-fatal — they surface in state.savedViews.fetchError
|
|
11
|
+
// for any UI that wants to show them.
|
|
12
|
+
|
|
13
|
+
const PARAM = 'view';
|
|
14
|
+
|
|
15
|
+
export default function bootViewFromUrl(store, opts = {}) {
|
|
16
|
+
if (typeof window === 'undefined') return Promise.resolve(null);
|
|
17
|
+
const url = new URL(window.location.href);
|
|
18
|
+
const hash = url.searchParams.get(PARAM);
|
|
19
|
+
if (!hash) return Promise.resolve(null);
|
|
20
|
+
|
|
21
|
+
const { user = null } = opts;
|
|
22
|
+
|
|
23
|
+
return store.doFetchView({ hash, user })
|
|
24
|
+
.then(({ snapshot }) => {
|
|
25
|
+
store.doApplyViewSnapshot(snapshot);
|
|
26
|
+
url.searchParams.delete(PARAM);
|
|
27
|
+
window.history.replaceState({}, '', url.toString());
|
|
28
|
+
return { hash, applied: true };
|
|
29
|
+
})
|
|
30
|
+
.catch((err) => {
|
|
31
|
+
// 401 here on the anonymous pass is expected for private views — the
|
|
32
|
+
// Auth panel will retry once it has a user. Other errors (404, 5xx,
|
|
33
|
+
// network) are reported and leave the param so a manual refresh can
|
|
34
|
+
// retry too.
|
|
35
|
+
console.warn('bootViewFromUrl:', err.message || err);
|
|
36
|
+
return { hash, applied: false, error: err.message || String(err) };
|
|
37
|
+
});
|
|
38
|
+
}
|