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
@@ -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
- var idx = 0;
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
- var fasta = '';
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
- var fasta = this.generate_fasta(hsps);
41
- var blob = new Blob([fasta], { type: 'text/fasta' });
42
- var filename = Exporter.sanitize_filename(filename_prefix) + '.txt';
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
- const blob_url = Exporter.generate_blob_url(blob, filename);
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 regex = /\/([^/]+)(?:\/|#|\?|$)/;
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
- fetch('/cloud_share', {
47
- method: 'POST',
48
- headers: {
49
- 'Content-Type': 'application/json'
50
- },
51
- body: JSON.stringify(requestData)
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
- _.map(
97
- errorMessages,
98
- _.bind(function (errorMessage) {
99
- return (
100
- <div className="fastan">
101
- <div className="section-content">
102
- <div className="modal-error">{errorMessage}</div>
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.refs.query.value(data['query']);
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.refs.button.state.methods[0]);
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.refs.query.isEmpty()) {
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.refs.button.setState({
169
- hasQuery: !this.refs.query.isEmpty(),
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="query" onSequenceTypeChanged={this.handleSequenceTypeChanged} onSequenceChanged={this.handleSequenceChanged}/>
210
+ <SearchQueryWidget ref={this.query} onSequenceTypeChanged={this.handleSequenceTypeChanged} onSequenceChanged={this.handleSequenceChanged}/>
209
211
 
210
212
  {this.useTreeWidget() ?
211
- <DatabasesTree ref="databases"
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 ref="databases" databases={this.state.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="button" onAlgoChanged={this.handleAlgoChanged} />
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 ref="grapher" className={cssClasses}>
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
- &nbsp;{Graph.name()}
54
+ &nbsp;{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(event) {
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(event) {
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;
@@ -1,5 +1,5 @@
1
1
  import $ from 'jquery';
2
- import '../packages/jquery-ui@1.11.4';
2
+ import '../packages/jquery-ui@1.13.3';
3
3
  import 'bootstrap';
4
4
 
5
5
  global.$ = $;
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) {
@@ -1,4 +1,5 @@
1
1
  import Histogram from 'histogram';
2
+ import chroma from 'chroma-js';
2
3
 
3
4
  class ReportPlugins {
4
5
  constructor(parent) {
data/public/js/options.js CHANGED
@@ -110,18 +110,14 @@ export class Options extends Component {
110
110
  }
111
111
 
112
112
  advancedParamsJSX() {
113
- if (this.state.paramsMode !== 'advanced') {
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
- <div className="w-full">
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 text-monospace"
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"