sequenceserver 3.1.1 → 3.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/sequenceserver/api_errors.rb +24 -0
- data/lib/sequenceserver/blast/tasks.rb +1 -1
- data/lib/sequenceserver/blast.rb +6 -0
- data/lib/sequenceserver/database.rb +13 -0
- data/lib/sequenceserver/routes.rb +1 -1
- data/lib/sequenceserver/sequence.rb +1 -2
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver.rb +1 -1
- data/public/css/app.min.css +1 -1
- data/public/css/sequenceserver.css +0 -18
- data/public/css/sequenceserver.min.css +2 -2
- data/public/js/alignment_exporter.js +16 -28
- data/public/js/cloud_share_modal.js +42 -42
- data/public/js/form.js +12 -10
- data/public/js/grapher.js +4 -4
- data/public/js/hit.js +3 -3
- data/public/js/hits.js +276 -0
- data/public/js/jquery_world.js +1 -1
- data/public/js/mailto.js +1 -3
- data/public/js/null_plugins/report_plugins.js +1 -0
- data/public/js/options.js +2 -6
- data/public/js/query.js +1 -1
- data/public/js/report.js +68 -252
- data/public/js/report_root.js +7 -5
- data/public/js/search.js +28 -11
- data/public/js/sequence.js +158 -158
- data/public/js/sequence_modal.js +28 -36
- data/public/js/sidebar.js +7 -6
- data/public/js/tests/alignment_exporter.spec.js +38 -0
- data/public/js/tests/cloud_share_modal.spec.js +75 -0
- data/public/js/tests/report.spec.js +37 -15
- data/public/packages/jquery-ui@1.13.3.js +19070 -0
- data/public/sequenceserver-report.min.js +3 -2481
- data/public/sequenceserver-report.min.js.LICENSE.txt +300 -0
- data/public/sequenceserver-report.min.js.map +1 -0
- data/public/sequenceserver-search.min.js +3 -2382
- data/public/sequenceserver-search.min.js.LICENSE.txt +292 -0
- data/public/sequenceserver-search.min.js.map +1 -0
- data/views/layout.erb +3 -7
- data/views/search.erb +1 -1
- data/views/search_layout.erb +1 -1
- metadata +11 -5
- data/public/config.js +0 -147
- data/public/packages/jquery-ui@1.11.4.js +0 -16624
@@ -1,5 +1,6 @@
|
|
1
1
|
import * as Exporter from './exporter';
|
2
2
|
import _ from 'underscore';
|
3
|
+
|
3
4
|
export default class AlignmentExporter {
|
4
5
|
constructor() {
|
5
6
|
this.prepare_alignments_for_export = this.prepare_alignments_for_export.bind(this);
|
@@ -7,49 +8,36 @@ export default class AlignmentExporter {
|
|
7
8
|
}
|
8
9
|
|
9
10
|
wrap_string(str, width) {
|
10
|
-
|
11
|
-
var wrapped = '';
|
12
|
-
while(true) {
|
13
|
-
wrapped += str.substring(idx, idx + width);
|
14
|
-
idx += width;
|
15
|
-
if(idx < str.length) {
|
16
|
-
wrapped += '\n';
|
17
|
-
} else {
|
18
|
-
break;
|
19
|
-
}
|
20
|
-
}
|
21
|
-
return wrapped;
|
11
|
+
return str.match(new RegExp(`.{1,${width}}`, 'g')).join('\n');
|
22
12
|
}
|
23
13
|
|
24
14
|
generate_fasta(hsps) {
|
15
|
+
let fasta = '';
|
25
16
|
|
26
|
-
|
17
|
+
hsps.map(hsp => {
|
18
|
+
fasta += `>${hsp.query_id}:${hsp.qstart}-${hsp.qend}\n`;
|
19
|
+
fasta += `${hsp.qseq}\n`;
|
20
|
+
fasta += `>${hsp.query_id}:${hsp.qstart}-${hsp.qend}_alignment_${hsp.hit_id}:${hsp.sstart}-${hsp.send}\n`;
|
21
|
+
fasta += `${hsp.midline}\n`;
|
22
|
+
fasta += `>${hsp.hit_id}:${hsp.sstart}-${hsp.send}\n`;
|
23
|
+
fasta += `${hsp.sseq}\n`;
|
24
|
+
});
|
27
25
|
|
28
|
-
_.each(hsps, _.bind(function (hsp) {
|
29
|
-
fasta += '>'+hsp.query_id+':'+hsp.qstart+'-'+hsp.qend+'\n';
|
30
|
-
fasta += hsp.qseq+'\n';
|
31
|
-
fasta += '>'+hsp.query_id+':'+hsp.qstart+'-'+hsp.qend+'_alignment_'+hsp.hit_id+':'+hsp.sstart+'-'+hsp.send+'\n';
|
32
|
-
fasta += hsp.midline+'\n';
|
33
|
-
fasta += '>'+hsp.hit_id+':'+hsp.sstart+'-'+hsp.send+'\n';
|
34
|
-
fasta += hsp.sseq+'\n';
|
35
|
-
}, this));
|
36
26
|
return fasta;
|
37
27
|
}
|
38
28
|
|
39
29
|
get_alignments_download_metadata(hsps, filename_prefix){
|
40
|
-
|
41
|
-
|
42
|
-
|
30
|
+
const fasta = this.generate_fasta(hsps);
|
31
|
+
const blob = new Blob([fasta], { type: 'text/fasta' });
|
32
|
+
const filename = Exporter.sanitize_filename(filename_prefix) + '.txt';
|
43
33
|
return {filename, blob};
|
44
34
|
}
|
45
35
|
|
46
|
-
|
47
36
|
prepare_alignments_for_export(hsps, filename_prefix) {
|
48
37
|
const { filename, blob } = this.get_alignments_download_metadata(hsps, filename_prefix);
|
49
|
-
|
50
|
-
return blob_url;
|
38
|
+
return Exporter.generate_blob_url(blob, filename);
|
51
39
|
}
|
52
|
-
|
40
|
+
|
53
41
|
export_alignments(hsps, filename_prefix) {
|
54
42
|
const { filename, blob } = this.get_alignments_download_metadata(hsps, filename_prefix);
|
55
43
|
Exporter.download_blob(blob, filename);
|
@@ -28,13 +28,11 @@ export default class CloudShareModal extends React.Component {
|
|
28
28
|
this.setState({ [name]: inputValue });
|
29
29
|
}
|
30
30
|
|
31
|
-
handleSubmit = (e) => {
|
31
|
+
handleSubmit = async (e) => {
|
32
32
|
e.preventDefault();
|
33
33
|
|
34
34
|
const { email } = this.state;
|
35
|
-
const
|
36
|
-
const match = window.location.pathname.match(regex);
|
37
|
-
const jobId = match[1];
|
35
|
+
const jobId = this.getJobIdFromPath();
|
38
36
|
|
39
37
|
this.setState({ formState: 'loading' });
|
40
38
|
|
@@ -43,33 +41,40 @@ export default class CloudShareModal extends React.Component {
|
|
43
41
|
sender_email: email
|
44
42
|
};
|
45
43
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
.then(response => response.json())
|
54
|
-
.then(data => {
|
55
|
-
if (data.shareable_url) {
|
56
|
-
// Successful response
|
57
|
-
this.setState({ formState: 'results', shareableurl: data.shareable_url });
|
58
|
-
} else if (data.errors) {
|
59
|
-
// Error response with specific error messages
|
60
|
-
const errorMessages = data.errors;
|
61
|
-
this.setState({ formState: 'error', errorMessages });
|
62
|
-
} else {
|
63
|
-
// Generic error message
|
64
|
-
throw new Error('Unknown error submitting form');
|
65
|
-
}
|
66
|
-
})
|
67
|
-
.catch(error => {
|
68
|
-
this.setState({
|
69
|
-
formState: 'error',
|
70
|
-
errorMessages: [error.message]
|
71
|
-
});
|
44
|
+
try {
|
45
|
+
const response = await fetch('/cloud_share', {
|
46
|
+
method: 'POST',
|
47
|
+
headers: {
|
48
|
+
'Content-Type': 'application/json'
|
49
|
+
},
|
50
|
+
body: JSON.stringify(requestData)
|
72
51
|
});
|
52
|
+
|
53
|
+
if (!response.ok) {
|
54
|
+
throw new Error('Network response was not ok');
|
55
|
+
}
|
56
|
+
|
57
|
+
const data = await response.json();
|
58
|
+
|
59
|
+
if (data.shareable_url) {
|
60
|
+
this.setState({ formState: 'results', shareableurl: data.shareable_url });
|
61
|
+
} else if (data.errors) {
|
62
|
+
this.setState({ formState: 'error', errorMessages: data.errors });
|
63
|
+
} else {
|
64
|
+
throw new Error('Unknown error submitting form');
|
65
|
+
}
|
66
|
+
} catch (error) {
|
67
|
+
this.setState({
|
68
|
+
formState: 'error',
|
69
|
+
errorMessages: [error.message]
|
70
|
+
});
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
getJobIdFromPath = () => {
|
75
|
+
const regex = /\/([^/]+)(?:\/|#|\?|$)/;
|
76
|
+
const match = window.location.pathname.match(regex);
|
77
|
+
return match ? match[1] : match;
|
73
78
|
}
|
74
79
|
|
75
80
|
renderLoading() {
|
@@ -93,18 +98,13 @@ export default class CloudShareModal extends React.Component {
|
|
93
98
|
return (
|
94
99
|
<>
|
95
100
|
{
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
</div>
|
104
|
-
</div>
|
105
|
-
);
|
106
|
-
}, this)
|
107
|
-
)
|
101
|
+
errorMessages.map((errorMessage, index) => (
|
102
|
+
<div key={`fastan-${index}`} className="fastan">
|
103
|
+
<div className="section-content">
|
104
|
+
<div className="modal-error">{errorMessage}</div>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
))
|
108
108
|
}
|
109
109
|
{this.renderForm()}
|
110
110
|
</>
|
data/public/js/form.js
CHANGED
@@ -34,6 +34,8 @@ export class Form extends Component {
|
|
34
34
|
this.handleAlgoChanged = this.handleAlgoChanged.bind(this);
|
35
35
|
this.handleFormSubmission = this.handleFormSubmission.bind(this);
|
36
36
|
this.formRef = createRef();
|
37
|
+
this.query = createRef();
|
38
|
+
this.button = createRef();
|
37
39
|
this.setButtonState = this.setButtonState.bind(this);
|
38
40
|
}
|
39
41
|
|
@@ -65,7 +67,7 @@ export class Form extends Component {
|
|
65
67
|
* (if any).
|
66
68
|
*/
|
67
69
|
if (data['query']) {
|
68
|
-
this.
|
70
|
+
this.query.current.value(data['query']);
|
69
71
|
}
|
70
72
|
|
71
73
|
setTimeout(function () {
|
@@ -96,7 +98,7 @@ export class Form extends Component {
|
|
96
98
|
evt.preventDefault();
|
97
99
|
const form = this.formRef.current;
|
98
100
|
const formData = new FormData(form);
|
99
|
-
formData.append('method', this.
|
101
|
+
formData.append('method', this.button.current.state.methods[0]);
|
100
102
|
fetch(window.location.href, {
|
101
103
|
method: 'POST',
|
102
104
|
body: formData
|
@@ -118,7 +120,7 @@ export class Form extends Component {
|
|
118
120
|
var database_type = this.databaseType;
|
119
121
|
var sequence_type = this.sequenceType;
|
120
122
|
|
121
|
-
if (this.
|
123
|
+
if (this.query.current.isEmpty()) {
|
122
124
|
return [];
|
123
125
|
}
|
124
126
|
|
@@ -165,8 +167,8 @@ export class Form extends Component {
|
|
165
167
|
}
|
166
168
|
|
167
169
|
setButtonState() {
|
168
|
-
this.
|
169
|
-
hasQuery: !this.
|
170
|
+
this.button.current.setState({
|
171
|
+
hasQuery: !this.query.current.isEmpty(),
|
170
172
|
hasDatabases: !!this.databaseType,
|
171
173
|
methods: this.determineBlastMethods()
|
172
174
|
});
|
@@ -205,16 +207,16 @@ export class Form extends Component {
|
|
205
207
|
<form id="blast" ref={this.formRef} onSubmit={this.handleFormSubmission}>
|
206
208
|
<input type="hidden" name="_csrf" value={document.querySelector('meta[name="_csrf"]').content} />
|
207
209
|
<div className="px-4">
|
208
|
-
<SearchQueryWidget ref=
|
210
|
+
<SearchQueryWidget ref={this.query} onSequenceTypeChanged={this.handleSequenceTypeChanged} onSequenceChanged={this.handleSequenceChanged}/>
|
209
211
|
|
210
212
|
{this.useTreeWidget() ?
|
211
|
-
<DatabasesTree
|
213
|
+
<DatabasesTree
|
212
214
|
databases={this.state.databases} tree={this.state.tree}
|
213
215
|
preSelectedDbs={this.state.preSelectedDbs}
|
214
216
|
onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
|
215
217
|
onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
|
216
218
|
:
|
217
|
-
<Databases
|
219
|
+
<Databases databases={this.state.databases}
|
218
220
|
preSelectedDbs={this.state.preSelectedDbs}
|
219
221
|
onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
|
220
222
|
onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
|
@@ -234,7 +236,7 @@ export class Form extends Component {
|
|
234
236
|
<label className="block my-4 md:my-2">
|
235
237
|
<input type="checkbox" id="toggleNewTab" /> Open results in new tab
|
236
238
|
</label>
|
237
|
-
<SearchButton ref=
|
239
|
+
<SearchButton ref={this.button} onAlgoChanged={this.handleAlgoChanged} />
|
238
240
|
</div>
|
239
241
|
|
240
242
|
</form>
|
@@ -305,4 +307,4 @@ class MixedNotification extends Component {
|
|
305
307
|
</div>
|
306
308
|
);
|
307
309
|
}
|
308
|
-
}
|
310
|
+
}
|
data/public/js/grapher.js
CHANGED
@@ -17,7 +17,7 @@ export default function Grapher(Graph) {
|
|
17
17
|
return class extends React.Component {
|
18
18
|
constructor(props) {
|
19
19
|
super(props);
|
20
|
-
this.name = Graph.name();
|
20
|
+
this.name = Graph.name(this.props);
|
21
21
|
this.collapsePreferences = new CollapsePreferences(this);
|
22
22
|
let isCollapsed = this.collapsePreferences.preferenceStoredAsCollapsed();
|
23
23
|
this.state = { collapsed: Graph.canCollapse() && (this.props.collapsed || isCollapsed) };
|
@@ -30,12 +30,12 @@ export default function Grapher(Graph) {
|
|
30
30
|
|
31
31
|
render() {
|
32
32
|
// Do not render when Graph.name() is null
|
33
|
-
if (Graph.name() === null) {
|
33
|
+
if (Graph.name(this.props) === null) {
|
34
34
|
return null;
|
35
35
|
} else {
|
36
36
|
var cssClasses = Graph.className() + ' grapher';
|
37
37
|
return (
|
38
|
-
<div
|
38
|
+
<div className={cssClasses}>
|
39
39
|
{this.header()}
|
40
40
|
{this.svgContainerJSX()}
|
41
41
|
</div>
|
@@ -51,7 +51,7 @@ export default function Grapher(Graph) {
|
|
51
51
|
onClick={() => this.collapsePreferences.toggleCollapse()}
|
52
52
|
>
|
53
53
|
{this.collapsePreferences.renderCollapseIcon()}
|
54
|
-
{Graph.name()}
|
54
|
+
{Graph.name(this.props)}
|
55
55
|
</h4>
|
56
56
|
{!this.state.collapsed && this.graphLinksJSX()}
|
57
57
|
</div>;
|
data/public/js/hit.js
CHANGED
@@ -80,14 +80,14 @@ export default class extends Component {
|
|
80
80
|
return `get_sequence/?sequence_ids=${sequenceIDs}&database_ids=${databaseIDs}`;
|
81
81
|
}
|
82
82
|
|
83
|
-
downloadFASTA(
|
83
|
+
downloadFASTA(_event) {
|
84
84
|
var sequenceIDs = [this.sequenceID()];
|
85
85
|
downloadFASTA(sequenceIDs, this.databaseIDs());
|
86
86
|
}
|
87
87
|
|
88
88
|
// Event-handler for exporting alignments.
|
89
89
|
// Calls relevant method on AlignmentExporter defined in alignment_exporter.js.
|
90
|
-
downloadAlignment(
|
90
|
+
downloadAlignment(_event) {
|
91
91
|
var hsps = _.map(this.props.hit.hsps, _.bind(function (hsp) {
|
92
92
|
hsp.query_id = this.props.query.id;
|
93
93
|
hsp.hit_id = this.props.hit.id;
|
@@ -246,4 +246,4 @@ export default class extends Component {
|
|
246
246
|
</div>
|
247
247
|
);
|
248
248
|
}
|
249
|
-
}
|
249
|
+
}
|
data/public/js/hits.js
ADDED
@@ -0,0 +1,276 @@
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
2
|
+
import { Component } from 'react';
|
3
|
+
import _ from 'underscore';
|
4
|
+
|
5
|
+
import { ReportQuery } from './query';
|
6
|
+
import Hit from './hit';
|
7
|
+
import HSP from './hsp';
|
8
|
+
import AlignmentExporter from './alignment_exporter';
|
9
|
+
/* eslint-enable no-unused-vars */
|
10
|
+
|
11
|
+
class Hits extends Component {
|
12
|
+
constructor(props) {
|
13
|
+
super(props);
|
14
|
+
this.numUpdates = 0;
|
15
|
+
this.nextQuery = 0;
|
16
|
+
this.nextHit = 0;
|
17
|
+
this.nextHSP = 0;
|
18
|
+
this.maxHSPs = 3; // max HSPs to render in a cycle
|
19
|
+
this.state = props.state;
|
20
|
+
this.prepareAlignmentOfSelectedHits = this.prepareAlignmentOfSelectedHits.bind(this);
|
21
|
+
}
|
22
|
+
|
23
|
+
componentDidMount() {
|
24
|
+
this.componentDidUpdate(this.props, this.state);
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Called for the first time after as BLAST results have been retrieved from
|
29
|
+
* the server and added to this.state by fetchResults. Only summary overview
|
30
|
+
* and circos would have been rendered at this point. At this stage we kick
|
31
|
+
* start iteratively adding 1 HSP to the page every 25 milli-seconds.
|
32
|
+
*/
|
33
|
+
componentDidUpdate(prevProps, prevState) {
|
34
|
+
// Log to console how long the last update take?
|
35
|
+
// console.log((Date.now() - this.lastTimeStamp) / 1000);
|
36
|
+
|
37
|
+
// Lock sidebar in its position on the first update.
|
38
|
+
if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {
|
39
|
+
this.affixSidebar();
|
40
|
+
}
|
41
|
+
|
42
|
+
// Queue next update if we have not rendered all results yet.
|
43
|
+
if (this.nextQuery < this.state.queries.length) {
|
44
|
+
// setTimeout is used to clear call stack and space out
|
45
|
+
// the updates giving the browser a chance to respond
|
46
|
+
// to user interactions.
|
47
|
+
setTimeout(() => this.updateState(), 25);
|
48
|
+
} else {
|
49
|
+
this.props.componentFinishedUpdating();
|
50
|
+
}
|
51
|
+
|
52
|
+
this.props.plugins.componentDidUpdate(prevProps, prevState);
|
53
|
+
}
|
54
|
+
|
55
|
+
/* eslint complexity: ["error", 6] */
|
56
|
+
/* ---------------------
|
57
|
+
* Push next slice of results to React for rendering.
|
58
|
+
*/
|
59
|
+
updateState() {
|
60
|
+
var results = { items: [], numHSPsProcessed: 0 };
|
61
|
+
this.processQueries(results);
|
62
|
+
|
63
|
+
// Push the components to react for rendering.
|
64
|
+
this.numUpdates++;
|
65
|
+
this.lastTimeStamp = Date.now();
|
66
|
+
this.setState({
|
67
|
+
results: this.state.results.concat(results.items),
|
68
|
+
veryBig: this.numUpdates >= 250,
|
69
|
+
});
|
70
|
+
}
|
71
|
+
|
72
|
+
processQueries(results) {
|
73
|
+
while (this.nextQuery < this.state.queries.length) {
|
74
|
+
var query = this.state.queries[this.nextQuery];
|
75
|
+
|
76
|
+
// We may see a query multiple times during rendering because only
|
77
|
+
// 3 hsps are rendered in each cycle, but we want to create the
|
78
|
+
// corresponding Query component only the first time we see it.
|
79
|
+
if (this.nextHit == 0 && this.nextHSP == 0) {
|
80
|
+
results.items.push(this.renderReportQuery(query));
|
81
|
+
results.items.push(...this.props.plugins.queryResults(query));
|
82
|
+
}
|
83
|
+
|
84
|
+
this.processHits(results, query);
|
85
|
+
this.itterateLoops(['nextQuery', 'nextHit'], query.hits.length);
|
86
|
+
if (results.numHSPsProcessed == this.maxHSPs) break;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
processHits(results, query) {
|
91
|
+
while (this.nextHit < query.hits.length) {
|
92
|
+
var hit = query.hits[this.nextHit];
|
93
|
+
// We may see a hit multiple times during rendering because only
|
94
|
+
// 10 hsps are rendered in each cycle, but we want to create the
|
95
|
+
// corresponding Hit component only the first time we see it.
|
96
|
+
if (this.nextHSP == 0) results.items.push(this.renderHit(query, hit));
|
97
|
+
|
98
|
+
this.processHSPS(results, query, hit);
|
99
|
+
this.itterateLoops(['nextHit', 'nextHSP'], hit.hsps.length);
|
100
|
+
if (results.numHSPsProcessed == this.maxHSPs) break;
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
processHSPS(results, query, hit) {
|
105
|
+
while (this.nextHSP < hit.hsps.length) {
|
106
|
+
// Get nextHSP and increment the counter.
|
107
|
+
var hsp = hit.hsps[this.nextHSP++];
|
108
|
+
results.items.push(
|
109
|
+
this.renderHsp(query, hit, hsp)
|
110
|
+
);
|
111
|
+
results.numHSPsProcessed++;
|
112
|
+
if (results.numHSPsProcessed == this.maxHSPs) break;
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
/*
|
117
|
+
* this function check if 2nd argument is reach end of it
|
118
|
+
*/
|
119
|
+
itterateLoops(args, length) {
|
120
|
+
if (this[args[1]] != length) return;
|
121
|
+
|
122
|
+
this[args[0]]++;
|
123
|
+
this[args[1]] = 0;
|
124
|
+
}
|
125
|
+
|
126
|
+
renderHsp(query, hit, hsp) {
|
127
|
+
return (
|
128
|
+
<HSP
|
129
|
+
key={
|
130
|
+
'Query_' +
|
131
|
+
query.number +
|
132
|
+
'_Hit_' +
|
133
|
+
hit.number +
|
134
|
+
'_HSP_' +
|
135
|
+
hsp.number
|
136
|
+
}
|
137
|
+
query={query}
|
138
|
+
hit={hit}
|
139
|
+
hsp={hsp}
|
140
|
+
algorithm={this.state.program}
|
141
|
+
showHSPNumbers={hit.hsps.length > 1}
|
142
|
+
{...this.props}
|
143
|
+
/>
|
144
|
+
);
|
145
|
+
}
|
146
|
+
|
147
|
+
renderHit(query, hit) {
|
148
|
+
return (
|
149
|
+
<Hit
|
150
|
+
key={'Query_' + query.number + '_Hit_' + hit.number}
|
151
|
+
query={query}
|
152
|
+
hit={hit}
|
153
|
+
algorithm={this.state.program}
|
154
|
+
querydb={this.state.querydb}
|
155
|
+
selectHit={this.selectHit}
|
156
|
+
imported_xml={this.state.imported_xml}
|
157
|
+
non_parse_seqids={this.state.non_parse_seqids}
|
158
|
+
showQueryCrumbs={this.state.queries.length > 1}
|
159
|
+
showHitCrumbs={query.hits.length > 1}
|
160
|
+
veryBig={this.state.veryBig}
|
161
|
+
onChange={this.prepareAlignmentOfSelectedHits}
|
162
|
+
{...this.props}
|
163
|
+
/>
|
164
|
+
);
|
165
|
+
}
|
166
|
+
|
167
|
+
renderReportQuery(query) {
|
168
|
+
return (
|
169
|
+
<ReportQuery
|
170
|
+
key={'Query_' + query.id}
|
171
|
+
query={query}
|
172
|
+
program={this.state.program}
|
173
|
+
querydb={this.state.querydb}
|
174
|
+
showQueryCrumbs={this.state.queries.length > 1}
|
175
|
+
non_parse_seqids={this.state.non_parse_seqids}
|
176
|
+
imported_xml={this.state.imported_xml}
|
177
|
+
veryBig={this.state.veryBig}
|
178
|
+
/>
|
179
|
+
);
|
180
|
+
}
|
181
|
+
|
182
|
+
/**
|
183
|
+
* Affixes the sidebar.
|
184
|
+
*/
|
185
|
+
affixSidebar() {
|
186
|
+
var $sidebar = $('.sidebar');
|
187
|
+
var sidebarOffset = $sidebar.offset();
|
188
|
+
if (sidebarOffset) {
|
189
|
+
$sidebar.affix({
|
190
|
+
offset: {
|
191
|
+
top: sidebarOffset.top,
|
192
|
+
},
|
193
|
+
});
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
/* eslint complexity: ["error", 6] */
|
198
|
+
/* -----------------------------------
|
199
|
+
* Event-handler when hit is selected
|
200
|
+
* Adds glow to hit component.
|
201
|
+
* Updates number of Fasta that can be downloaded
|
202
|
+
*/
|
203
|
+
selectHit(id) {
|
204
|
+
var checkbox = $('#' + id);
|
205
|
+
var num_checked = $('.hit-links :checkbox:checked').length;
|
206
|
+
|
207
|
+
if (!checkbox || !checkbox.val()) return;
|
208
|
+
|
209
|
+
var $hit = $(checkbox.data('target'));
|
210
|
+
|
211
|
+
// Highlight selected hit and enable 'Download FASTA/Alignment of
|
212
|
+
// selected' links.
|
213
|
+
if (checkbox.is(':checked')) {
|
214
|
+
$hit.addClass('glow');
|
215
|
+
$hit.next('.hsp').addClass('glow');
|
216
|
+
$('.download-fasta-of-selected').enable();
|
217
|
+
$('.download-alignment-of-selected').enable();
|
218
|
+
} else {
|
219
|
+
$hit.removeClass('glow');
|
220
|
+
$hit.next('.hsp').removeClass('glow');
|
221
|
+
$('.download-fasta-of-selected').attr('href', '#').removeAttr('download');
|
222
|
+
}
|
223
|
+
|
224
|
+
var $a = $('.download-fasta-of-selected');
|
225
|
+
var $b = $('.download-alignment-of-selected');
|
226
|
+
|
227
|
+
if (num_checked >= 1) {
|
228
|
+
$a.find('.text-bold').html(num_checked);
|
229
|
+
$b.find('.text-bold').html(num_checked);
|
230
|
+
}
|
231
|
+
|
232
|
+
if (num_checked == 0) {
|
233
|
+
$a.addClass('disabled').find('.text-bold').html('');
|
234
|
+
$b.addClass('disabled').find('.text-bold').html('');
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
prepareAlignmentOfSelectedHits() {
|
239
|
+
var sequence_ids = $('.hit-links :checkbox:checked').map(function () {
|
240
|
+
return this.value;
|
241
|
+
}).get();
|
242
|
+
|
243
|
+
if(!sequence_ids.length){
|
244
|
+
// remove attributes from link if sequence_ids array is empty
|
245
|
+
$('.download-alignment-of-selected').attr('href', '#').removeAttr('download');
|
246
|
+
return;
|
247
|
+
|
248
|
+
}
|
249
|
+
if(this.state.alignment_blob_url){
|
250
|
+
// always revoke existing url if any because this method will always create a new url
|
251
|
+
window.URL.revokeObjectURL(this.state.alignment_blob_url);
|
252
|
+
}
|
253
|
+
var hsps_arr = [];
|
254
|
+
var aln_exporter = new AlignmentExporter();
|
255
|
+
const self = this;
|
256
|
+
_.each(this.state.queries, _.bind(function (query) {
|
257
|
+
_.each(query.hits, function (hit) {
|
258
|
+
if (_.indexOf(sequence_ids, hit.id) != -1) {
|
259
|
+
hsps_arr = hsps_arr.concat(self.props.populate_hsp_array(hit, query.id));
|
260
|
+
}
|
261
|
+
});
|
262
|
+
}, this));
|
263
|
+
const filename = 'alignment-' + sequence_ids.length + '_hits.txt';
|
264
|
+
const blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename);
|
265
|
+
// set required download attributes for link
|
266
|
+
$('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename);
|
267
|
+
// track new url for future removal
|
268
|
+
this.setState({alignment_blob_url: blob_url});
|
269
|
+
}
|
270
|
+
|
271
|
+
render() {
|
272
|
+
return this.state.results;
|
273
|
+
}
|
274
|
+
}
|
275
|
+
|
276
|
+
export default Hits;
|
data/public/js/jquery_world.js
CHANGED
data/public/js/mailto.js
CHANGED
@@ -5,9 +5,7 @@ export default function asMailtoHref(querydb, program, numQueries, url, isOpenAc
|
|
5
5
|
}
|
6
6
|
|
7
7
|
function formatDatabases(querydb) {
|
8
|
-
return querydb
|
9
|
-
.slice(0, 15)
|
10
|
-
.map(db => ' ' + db.title);
|
8
|
+
return querydb ? querydb.slice(0, 15).map(db => ' ' + db.title) : "";
|
11
9
|
}
|
12
10
|
|
13
11
|
function composeEmail(dbsArr, program, numQueries, url, isOpenAccess) {
|
data/public/js/options.js
CHANGED
@@ -110,18 +110,14 @@ export class Options extends Component {
|
|
110
110
|
}
|
111
111
|
|
112
112
|
advancedParamsJSX() {
|
113
|
-
|
114
|
-
return null;
|
115
|
-
}
|
116
|
-
|
117
|
-
let classNames = 'flex-grow block px-4 py-1 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 text-base';
|
113
|
+
let classNames = 'flex-grow block px-4 py-1 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 text-base font-mono';
|
118
114
|
|
119
115
|
if (this.state.textValue) {
|
120
116
|
classNames += ' bg-yellow-100';
|
121
117
|
}
|
122
118
|
|
123
119
|
return(
|
124
|
-
|
120
|
+
<div className={this.state.paramsMode !== 'advanced' ? 'w-full hidden' : 'w-full'}>
|
125
121
|
<div className="flex items-end">
|
126
122
|
<label className="flex items-center" htmlFor="advanced">
|
127
123
|
Advanced parameters
|
data/public/js/query.js
CHANGED
@@ -348,7 +348,7 @@ export class SearchQueryWidget extends Component {
|
|
348
348
|
className="sequence">
|
349
349
|
<textarea
|
350
350
|
id="sequence" ref={this.textareaRef}
|
351
|
-
className="block w-full p-4 text-gray-900 border border-gray-300 rounded-l-lg rounded-tr-lg bg-gray-50 text-base
|
351
|
+
className="block w-full p-4 text-gray-900 border border-gray-300 rounded-l-lg rounded-tr-lg bg-gray-50 text-base font-mono"
|
352
352
|
name="sequence" value={this.state.value}
|
353
353
|
rows="6"
|
354
354
|
required="required"
|