sequenceserver 3.1.1 → 3.1.3

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequenceserver/api_errors.rb +24 -0
  3. data/lib/sequenceserver/blast/tasks.rb +1 -1
  4. data/lib/sequenceserver/blast.rb +6 -0
  5. data/lib/sequenceserver/database.rb +13 -0
  6. data/lib/sequenceserver/routes.rb +1 -1
  7. data/lib/sequenceserver/sequence.rb +1 -2
  8. data/lib/sequenceserver/version.rb +1 -1
  9. data/lib/sequenceserver.rb +1 -1
  10. data/public/css/app.min.css +1 -1
  11. data/public/css/sequenceserver.css +0 -18
  12. data/public/css/sequenceserver.min.css +2 -2
  13. data/public/js/alignment_exporter.js +16 -28
  14. data/public/js/cloud_share_modal.js +42 -42
  15. data/public/js/form.js +12 -10
  16. data/public/js/grapher.js +4 -4
  17. data/public/js/hit.js +3 -3
  18. data/public/js/hits.js +276 -0
  19. data/public/js/jquery_world.js +1 -1
  20. data/public/js/mailto.js +1 -3
  21. data/public/js/null_plugins/report_plugins.js +1 -0
  22. data/public/js/options.js +2 -6
  23. data/public/js/query.js +1 -1
  24. data/public/js/report.js +68 -252
  25. data/public/js/report_root.js +7 -5
  26. data/public/js/search.js +28 -11
  27. data/public/js/sequence.js +158 -158
  28. data/public/js/sequence_modal.js +28 -36
  29. data/public/js/sidebar.js +7 -6
  30. data/public/js/tests/alignment_exporter.spec.js +38 -0
  31. data/public/js/tests/cloud_share_modal.spec.js +75 -0
  32. data/public/js/tests/report.spec.js +37 -15
  33. data/public/packages/jquery-ui@1.13.3.js +19070 -0
  34. data/public/sequenceserver-report.min.js +3 -2481
  35. data/public/sequenceserver-report.min.js.LICENSE.txt +300 -0
  36. data/public/sequenceserver-report.min.js.map +1 -0
  37. data/public/sequenceserver-search.min.js +3 -2382
  38. data/public/sequenceserver-search.min.js.LICENSE.txt +292 -0
  39. data/public/sequenceserver-search.min.js.map +1 -0
  40. data/views/layout.erb +3 -7
  41. data/views/search.erb +1 -1
  42. data/views/search_layout.erb +1 -1
  43. metadata +11 -5
  44. data/public/config.js +0 -147
  45. data/public/packages/jquery-ui@1.11.4.js +0 -16624
data/public/js/report.js CHANGED
@@ -3,10 +3,8 @@ import React, { Component } from 'react';
3
3
  import _ from 'underscore';
4
4
 
5
5
  import Sidebar from './sidebar';
6
+ import Hits from './hits';
6
7
  import Circos from './circos';
7
- import { ReportQuery } from './query';
8
- import Hit from './hit';
9
- import HSP from './hsp';
10
8
  import AlignmentExporter from './alignment_exporter';
11
9
  import ReportPlugins from 'report_plugins';
12
10
 
@@ -21,11 +19,6 @@ class Report extends Component {
21
19
  super(props);
22
20
  // Properties below are internal state used to render results in small
23
21
  // slices (see updateState).
24
- this.numUpdates = 0;
25
- this.nextQuery = 0;
26
- this.nextHit = 0;
27
- this.nextHSP = 0;
28
- this.maxHSPs = 3; // max HSPs to render in a cycle
29
22
  this.state = {
30
23
  user_warning: null,
31
24
  download_links: [],
@@ -34,8 +27,8 @@ class Report extends Component {
34
27
  program: '',
35
28
  program_version: '',
36
29
  submitted_at: '',
37
- queries: [],
38
30
  results: [],
31
+ queries: [],
39
32
  querydb: [],
40
33
  params: [],
41
34
  stats: [],
@@ -43,7 +36,6 @@ class Report extends Component {
43
36
  allQueriesLoaded: false,
44
37
  cloud_sharing_enabled: false,
45
38
  };
46
- this.prepareAlignmentOfSelectedHits = this.prepareAlignmentOfSelectedHits.bind(this);
47
39
  this.prepareAlignmentOfAllHits = this.prepareAlignmentOfAllHits.bind(this);
48
40
  this.setStateFromJSON = this.setStateFromJSON.bind(this);
49
41
  this.plugins = new ReportPlugins(this);
@@ -58,31 +50,66 @@ class Report extends Component {
58
50
  }
59
51
 
60
52
  pollPeriodically(path, callback, errCallback) {
61
- var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
53
+ var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
62
54
  function poll() {
63
- $.getJSON(path).complete(function (jqXHR) {
64
- switch (jqXHR.status) {
65
- case 202:
66
- var interval;
67
- if (intervals.length === 1) {
68
- interval = intervals[0];
55
+ fetch(path)
56
+ .then(response => {
57
+ // Handle HTTP status codes
58
+ if (!response.ok) throw response;
59
+
60
+ return response.text().then(data => {
61
+ if (data) {
62
+ data = parseJSON(data);
63
+ };
64
+ return { status: response.status, data }
65
+ });
66
+ })
67
+ .then(({ status, data }) => {
68
+ switch (status) {
69
+ case 202:
70
+ var interval;
71
+ if (intervals.length === 1) {
72
+ interval = intervals[0];
73
+ } else {
74
+ interval = intervals.shift();
75
+ }
76
+ setTimeout(poll, interval);
77
+ break;
78
+ case 200:
79
+ callback(data);
80
+ break;
81
+ }
82
+ })
83
+ .catch(error => {
84
+ if (error.text) {
85
+ error.text().then(errData => {
86
+ errData = parseJSON(errData);
87
+ switch (error.status) {
88
+ case 400:
89
+ case 422:
90
+ case 500:
91
+ errCallback(errData);
92
+ break;
93
+ default:
94
+ console.error("Unhandled error:", error.status);
95
+ }
96
+ });
69
97
  } else {
70
- interval = intervals.shift();
98
+ console.error("Network error:", error);
71
99
  }
72
- setTimeout(poll, interval);
73
- break;
74
- case 200:
75
- callback(jqXHR.responseJSON);
76
- break;
77
- case 400:
78
- case 422:
79
- case 500:
80
- errCallback(jqXHR.responseJSON);
81
- break;
82
- }
83
- });
100
+ });
84
101
  }
85
102
 
103
+ function parseJSON(str) {
104
+ let parsedJson = str;
105
+ try {
106
+ parsedJson = JSON.parse(str);
107
+ } catch (e) {
108
+ console.error("Error parsing JSON:", e);
109
+ }
110
+
111
+ return parsedJson;
112
+ }
86
113
  poll();
87
114
  }
88
115
 
@@ -113,139 +140,6 @@ class Report extends Component {
113
140
  this.toggleTable();
114
141
  }
115
142
 
116
- /**
117
- * Called for the first time after as BLAST results have been retrieved from
118
- * the server and added to this.state by fetchResults. Only summary overview
119
- * and circos would have been rendered at this point. At this stage we kick
120
- * start iteratively adding 1 HSP to the page every 25 milli-seconds.
121
- */
122
- componentDidUpdate(prevProps, prevState) {
123
- // Log to console how long the last update take?
124
- // console.log((Date.now() - this.lastTimeStamp) / 1000);
125
-
126
- // Lock sidebar in its position on the first update.
127
- if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {
128
- this.affixSidebar();
129
- }
130
-
131
- // Queue next update if we have not rendered all results yet.
132
- if (this.nextQuery < this.state.queries.length) {
133
- // setTimeout is used to clear call stack and space out
134
- // the updates giving the browser a chance to respond
135
- // to user interactions.
136
- setTimeout(() => this.updateState(), 25);
137
- } else {
138
- this.componentFinishedUpdating();
139
- }
140
-
141
- this.plugins.componentDidUpdate(prevProps, prevState);
142
- }
143
-
144
- /**
145
- * Push next slice of results to React for rendering.
146
- */
147
- updateState() {
148
- var results = [];
149
- var numHSPsProcessed = 0;
150
- while (this.nextQuery < this.state.queries.length) {
151
- var query = this.state.queries[this.nextQuery];
152
-
153
- // We may see a query multiple times during rendering because only
154
- // 3 hsps are rendered in each cycle, but we want to create the
155
- // corresponding Query component only the first time we see it.
156
- if (this.nextHit == 0 && this.nextHSP == 0) {
157
- results.push(
158
- <ReportQuery
159
- key={'Query_' + query.id}
160
- query={query}
161
- program={this.state.program}
162
- querydb={this.state.querydb}
163
- showQueryCrumbs={this.state.queries.length > 1}
164
- non_parse_seqids={this.state.non_parse_seqids}
165
- imported_xml={this.state.imported_xml}
166
- veryBig={this.state.veryBig}
167
- />
168
- );
169
-
170
- results.push(...this.plugins.queryResults(query));
171
- }
172
-
173
- while (this.nextHit < query.hits.length) {
174
- var hit = query.hits[this.nextHit];
175
- // We may see a hit multiple times during rendering because only
176
- // 10 hsps are rendered in each cycle, but we want to create the
177
- // corresponding Hit component only the first time we see it.
178
- if (this.nextHSP == 0) {
179
- results.push(
180
- <Hit
181
- key={'Query_' + query.number + '_Hit_' + hit.number}
182
- query={query}
183
- hit={hit}
184
- algorithm={this.state.program}
185
- querydb={this.state.querydb}
186
- selectHit={this.selectHit}
187
- imported_xml={this.state.imported_xml}
188
- non_parse_seqids={this.state.non_parse_seqids}
189
- showQueryCrumbs={this.state.queries.length > 1}
190
- showHitCrumbs={query.hits.length > 1}
191
- veryBig={this.state.veryBig}
192
- onChange={this.prepareAlignmentOfSelectedHits}
193
- {...this.props}
194
- />
195
- );
196
- }
197
-
198
- while (this.nextHSP < hit.hsps.length) {
199
- // Get nextHSP and increment the counter.
200
- var hsp = hit.hsps[this.nextHSP++];
201
- results.push(
202
- <HSP
203
- key={
204
- 'Query_' +
205
- query.number +
206
- '_Hit_' +
207
- hit.number +
208
- '_HSP_' +
209
- hsp.number
210
- }
211
- query={query}
212
- hit={hit}
213
- hsp={hsp}
214
- algorithm={this.state.program}
215
- showHSPNumbers={hit.hsps.length > 1}
216
- {...this.props}
217
- />
218
- );
219
- numHSPsProcessed++;
220
- if (numHSPsProcessed == this.maxHSPs) break;
221
- }
222
- // Are we here because we have iterated over all hsps of a hit,
223
- // or because of the break clause in the inner loop?
224
- if (this.nextHSP == hit.hsps.length) {
225
- this.nextHit = this.nextHit + 1;
226
- this.nextHSP = 0;
227
- }
228
- if (numHSPsProcessed == this.maxHSPs) break;
229
- }
230
-
231
- // Are we here because we have iterated over all hits of a query,
232
- // or because of the break clause in the inner loop?
233
- if (this.nextHit == query.hits.length) {
234
- this.nextQuery = this.nextQuery + 1;
235
- this.nextHit = 0;
236
- }
237
- if (numHSPsProcessed == this.maxHSPs) break;
238
- }
239
-
240
- // Push the components to react for rendering.
241
- this.numUpdates++;
242
- this.lastTimeStamp = Date.now();
243
- this.setState({
244
- results: this.state.results.concat(results),
245
- veryBig: this.numUpdates >= 250,
246
- });
247
- }
248
-
249
143
  /**
250
144
  * Called after all results have been rendered.
251
145
  */
@@ -282,6 +176,7 @@ class Report extends Component {
282
176
  );
283
177
  }
284
178
 
179
+ /* eslint-disable */
285
180
  /**
286
181
  * Return results JSX.
287
182
  */
@@ -300,12 +195,20 @@ class Report extends Component {
300
195
  <div className="col-md-9">
301
196
  {this.overviewJSX()}
302
197
  {this.circosJSX()}
303
- {this.plugins.generateStats()}
198
+ {this.plugins.generateStats(this.state.queries)}
304
199
  {this.state.results}
200
+ <Hits
201
+ state={this.state}
202
+ componentFinishedUpdating={(_) => this.componentFinishedUpdating(_)}
203
+ populate_hsp_array={this.populate_hsp_array.bind(this)}
204
+ plugins={this.plugins}
205
+ {...this.props}
206
+ />
305
207
  </div>
306
208
  </div>
307
209
  );
308
210
  }
211
+ /* eslint-enable */
309
212
 
310
213
 
311
214
  warningJSX() {
@@ -477,20 +380,7 @@ class Report extends Component {
477
380
  );
478
381
  }
479
382
 
480
- /**
481
- * Affixes the sidebar.
482
- */
483
- affixSidebar() {
484
- var $sidebar = $('.sidebar');
485
- var sidebarOffset = $sidebar.offset();
486
- if (sidebarOffset) {
487
- $sidebar.affix({
488
- offset: {
489
- top: sidebarOffset.top,
490
- },
491
- });
492
- }
493
- }
383
+
494
384
 
495
385
  /**
496
386
  * For the query in viewport, highlights corresponding entry in the index.
@@ -499,84 +389,10 @@ class Report extends Component {
499
389
  $('body').scrollspy({ target: '.sidebar' });
500
390
  }
501
391
 
502
- /**
503
- * Event-handler when hit is selected
504
- * Adds glow to hit component.
505
- * Updates number of Fasta that can be downloaded
506
- */
507
- selectHit(id) {
508
- var checkbox = $('#' + id);
509
- var num_checked = $('.hit-links :checkbox:checked').length;
510
-
511
- if (!checkbox || !checkbox.val()) {
512
- return;
513
- }
514
-
515
- var $hit = $(checkbox.data('target'));
516
-
517
- // Highlight selected hit and enable 'Download FASTA/Alignment of
518
- // selected' links.
519
- if (checkbox.is(':checked')) {
520
- $hit.addClass('glow');
521
- $hit.next('.hsp').addClass('glow');
522
- $('.download-fasta-of-selected').enable();
523
- $('.download-alignment-of-selected').enable();
524
- } else {
525
- $hit.removeClass('glow');
526
- $hit.next('.hsp').removeClass('glow');
527
- $('.download-fasta-of-selected').attr('href', '#').removeAttr('download');
528
- }
529
-
530
- var $a = $('.download-fasta-of-selected');
531
- var $b = $('.download-alignment-of-selected');
532
-
533
- if (num_checked >= 1) {
534
- $a.find('.text-bold').html(num_checked);
535
- $b.find('.text-bold').html(num_checked);
536
- }
537
-
538
- if (num_checked == 0) {
539
- $a.addClass('disabled').find('.text-bold').html('');
540
- $b.addClass('disabled').find('.text-bold').html('');
541
- }
542
- }
543
392
  populate_hsp_array(hit, query_id){
544
393
  return hit.hsps.map(hsp => Object.assign(hsp, {hit_id: hit.id, query_id}));
545
394
  }
546
395
 
547
- prepareAlignmentOfSelectedHits() {
548
- var sequence_ids = $('.hit-links :checkbox:checked').map(function () {
549
- return this.value;
550
- }).get();
551
-
552
- if(!sequence_ids.length){
553
- // remove attributes from link if sequence_ids array is empty
554
- $('.download-alignment-of-selected').attr('href', '#').removeAttr('download');
555
- return;
556
-
557
- }
558
- if(this.state.alignment_blob_url){
559
- // always revoke existing url if any because this method will always create a new url
560
- window.URL.revokeObjectURL(this.state.alignment_blob_url);
561
- }
562
- var hsps_arr = [];
563
- var aln_exporter = new AlignmentExporter();
564
- const self = this;
565
- _.each(this.state.queries, _.bind(function (query) {
566
- _.each(query.hits, function (hit) {
567
- if (_.indexOf(sequence_ids, hit.id) != -1) {
568
- hsps_arr = hsps_arr.concat(self.populate_hsp_array(hit, query.id));
569
- }
570
- });
571
- }, this));
572
- const filename = 'alignment-' + sequence_ids.length + '_hits.txt';
573
- const blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename);
574
- // set required download attributes for link
575
- $('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename);
576
- // track new url for future removal
577
- this.setState({alignment_blob_url: blob_url});
578
- }
579
-
580
396
  prepareAlignmentOfAllHits() {
581
397
  // Get number of hits and array of all hsps.
582
398
  var num_hits = 0;
@@ -17,6 +17,8 @@ class Page extends Component {
17
17
  this.showErrorModal = this.showErrorModal.bind(this);
18
18
  this.getCharacterWidth = this.getCharacterWidth.bind(this);
19
19
  this.hspChars = createRef();
20
+ this.sequenceModal = createRef();
21
+ this.errorModal = createRef();
20
22
  }
21
23
  componentDidMount() {
22
24
  var job_id = location.pathname.split('/').pop();
@@ -24,11 +26,11 @@ class Page extends Component {
24
26
  }
25
27
 
26
28
  showSequenceModal(url) {
27
- this.refs.sequenceModal.show(url);
29
+ this.sequenceModal.current.show(url);
28
30
  }
29
31
 
30
32
  showErrorModal(errorData, beforeShow) {
31
- this.refs.errorModal.show(errorData, beforeShow);
33
+ this.errorModal.current.show(errorData, beforeShow);
32
34
  }
33
35
 
34
36
  getCharacterWidth() {
@@ -60,11 +62,11 @@ class Page extends Component {
60
62
  <canvas id="png-exporter" hidden></canvas>
61
63
 
62
64
  <SequenceModal
63
- ref="sequenceModal"
65
+ ref={this.sequenceModal}
64
66
  showErrorModal={(...args) => this.showErrorModal(...args)}
65
67
  />
66
68
 
67
- <ErrorModal ref="errorModal" />
69
+ <ErrorModal ref={this.errorModal} />
68
70
  </div>
69
71
  );
70
72
  }
@@ -72,4 +74,4 @@ class Page extends Component {
72
74
 
73
75
 
74
76
  const root = createRoot(document.getElementById('view'));
75
- root.render(<Page />);
77
+ root.render(<Page />);
data/public/js/search.js CHANGED
@@ -1,32 +1,49 @@
1
- import "./jquery_world";
2
- import React, { Component } from "react";
3
- import { createRoot } from "react-dom/client";
4
- import { DnD } from "./dnd";
5
- import { Form } from "./form";
6
- import { SearchHeaderPlugin } from "search_header_plugin";
1
+ import './jquery_world';
2
+ import React, { Component } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { DnD } from './dnd';
5
+ import { Form } from './form';
6
+ import { SearchHeaderPlugin } from 'search_header_plugin';
7
7
 
8
8
  /**
9
9
  * Clear sessionStorage on reload.
10
10
  */
11
- if (performance.navigation.type == performance.navigation.TYPE_RELOAD) {
11
+ const navigationEntry = performance.getEntriesByType('navigation')[0];
12
+ if (navigationEntry && navigationEntry.type === 'reload') {
12
13
  sessionStorage.clear();
13
14
  history.replaceState(null, "", location.href.split("?")[0]);
14
15
  }
15
16
 
16
17
  class Page extends Component {
18
+ constructor(props) {
19
+ super(props);
20
+ this.dnd = React.createRef();
21
+ this.form = React.createRef();
22
+ }
23
+
17
24
  componentDidMount() {
18
- this.refs.dnd.setState({ query: this.refs.form.refs.query });
25
+ this.dnd.current.setState({ query: this.form.current.query.current })
19
26
  }
27
+
20
28
  render() {
21
29
  return (
22
30
  <div>
23
31
  <SearchHeaderPlugin />
24
- <DnD ref="dnd" />
25
- <Form ref="form" />
32
+ <DnD ref={this.dnd} />
33
+ <Form ref={this.form} />
26
34
  </div>
27
35
  );
28
36
  }
29
37
  }
30
38
 
31
- const root = createRoot(document.getElementById("view"));
39
+ const root = createRoot(document.getElementById('view'));
32
40
  root.render(<Page />);
41
+
42
+ document.addEventListener('DOMContentLoaded', () => {
43
+ const closeButton = document.querySelector('.js--close-help');
44
+ if (closeButton) {
45
+ closeButton.addEventListener('click', function() {
46
+ document.querySelector('[data-help-modal]').classList.add('hidden');
47
+ });
48
+ }
49
+ });