sequenceserver 3.1.2 → 3.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequenceserver/api_errors.rb +18 -0
  3. data/lib/sequenceserver/blast/hit.rb +1 -2
  4. data/lib/sequenceserver/blast/job.rb +15 -5
  5. data/lib/sequenceserver/blast/report.rb +5 -1
  6. data/lib/sequenceserver/blast/tasks.rb +1 -1
  7. data/lib/sequenceserver/database.rb +5 -1
  8. data/lib/sequenceserver/job.rb +26 -10
  9. data/lib/sequenceserver/makeblastdb.rb +2 -1
  10. data/lib/sequenceserver/routes.rb +38 -13
  11. data/lib/sequenceserver/version.rb +1 -1
  12. data/lib/sequenceserver/zip_file_generator.rb +1 -1
  13. data/lib/sequenceserver.rb +1 -1
  14. data/public/404.html +8 -16
  15. data/public/css/app.css +794 -111
  16. data/public/css/app.min.css +1 -1
  17. data/public/css/sequenceserver.min.css +1 -8
  18. data/public/js/alignment_exporter.js +16 -28
  19. data/public/js/circos.js +103 -96
  20. data/public/{packages/circosJS@1.7.0.js → js/circosjs.js} +156 -778
  21. data/public/js/cloud_share_modal.js +95 -98
  22. data/public/js/collapse_preferences.js +13 -15
  23. data/public/js/databases.js +4 -3
  24. data/public/js/databases_tree.js +51 -30
  25. data/public/js/error_modal.js +35 -22
  26. data/public/js/exporter.js +2 -0
  27. data/public/js/form.js +55 -17
  28. data/public/js/grapher.js +94 -92
  29. data/public/js/hit.js +24 -22
  30. data/public/js/hits.js +283 -0
  31. data/public/js/hits_overview.js +8 -27
  32. data/public/js/hsp.js +140 -144
  33. data/public/js/jquery_world.js +25 -16
  34. data/public/js/kablammo.js +14 -10
  35. data/public/js/length_distribution.js +23 -37
  36. data/public/js/mailto.js +1 -3
  37. data/public/js/null_plugins/report_plugins.js +4 -5
  38. data/public/js/options.js +19 -25
  39. data/public/js/query.js +36 -35
  40. data/public/js/report/alignment_results.js +18 -0
  41. data/public/js/report/graphical_overview.js +38 -0
  42. data/public/js/report/report_header.js +28 -0
  43. data/public/js/report/run_summary.js +43 -0
  44. data/public/js/report.js +124 -348
  45. data/public/js/report_root.js +10 -8
  46. data/public/js/search.js +31 -11
  47. data/public/js/search_button.js +85 -78
  48. data/public/js/sequence.js +169 -180
  49. data/public/js/sequence_modal.js +58 -65
  50. data/public/js/share_url.js +5 -5
  51. data/public/js/sidebar.js +152 -89
  52. data/public/js/svgExporter.js +8 -3
  53. data/public/js/tests/alignment_exporter.spec.js +38 -0
  54. data/public/js/tests/cloud_share_modal.spec.js +75 -0
  55. data/public/js/tests/form.spec.js +1 -1
  56. data/public/js/tests/report.spec.js +37 -15
  57. data/public/js/tests/visualisation_helpers.spec.js +28 -0
  58. data/public/js/visualisation_helpers.js +24 -27
  59. data/public/packages/jquery-ui@1.13.3.js +19070 -0
  60. data/public/sequenceserver-report.min.js +3 -2481
  61. data/public/sequenceserver-report.min.js.LICENSE.txt +213 -0
  62. data/public/sequenceserver-report.min.js.map +1 -0
  63. data/public/sequenceserver-search.min.js +3 -2382
  64. data/public/sequenceserver-search.min.js.LICENSE.txt +262 -0
  65. data/public/sequenceserver-search.min.js.map +1 -0
  66. data/views/error.erb +1 -1
  67. data/views/layout.erb +122 -133
  68. data/views/report.erb +3 -2
  69. data/views/search.erb +10 -14
  70. metadata +21 -28
  71. data/public/config.js +0 -147
  72. data/public/css/fonts.css +0 -31
  73. data/public/css/grapher.css +0 -670
  74. data/public/css/sequenceserver.css +0 -527
  75. data/public/packages/jquery-ui@1.11.4.js +0 -16624
  76. data/public/vendor/github/mbostock/d3@3.5.6/d3.js +0 -9506
  77. data/public/vendor/github/mbostock/d3@3.5.6.js +0 -1
  78. data/views/search_layout.erb +0 -153
@@ -13,6 +13,7 @@ export default class SequenceModal extends React.Component {
13
13
  error_msgs: [],
14
14
  sequences: [],
15
15
  requestCompleted: false,
16
+ isModalVisible: false,
16
17
  };
17
18
  this.modalRef = createRef();
18
19
  }
@@ -20,98 +21,88 @@ export default class SequenceModal extends React.Component {
20
21
  // Lifecycle methods. //
21
22
 
22
23
  render() {
24
+ const { isModalVisible, requestCompleted } = this.state;
25
+
23
26
  return (
24
- <div className="modal sequence-viewer" ref={this.modalRef} tabIndex="-1">
25
- <div className="modal-dialog">
26
- <div className="modal-content">
27
- <div className="modal-header">
28
- <h3>View sequence</h3>
27
+ <div className="relative sequence-viewer-wrap">
28
+ <dialog ref={this.modalRef} className="sequence-viewer fixed p-4 w-full max-w-2xl bg-transparent focus:outline-none overflow-visible z-50">
29
+ <div className="relative flex flex-col rounded-lg bg-white shadow">
30
+ <div className="flex items-start justify-between rounded-t border-b p-5">
31
+ <h3 className="text-xl font-medium text-gray-900">
32
+ View sequence
33
+ </h3>
34
+ <button className="sequence-viewer-close ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-gray-400 hover:bg-gray-200" onClick={this.hide}>
35
+ <i className="fa-solid fa-xmark hover:text-black"></i>
36
+ </button>
37
+ </div>
38
+ <div className="sequence-viewer-content max-h-[80vh] overflow-y-scroll">
39
+ {(requestCompleted && this.resultsJSX()) || this.loadingJSX()}
29
40
  </div>
30
-
31
- {(this.state.requestCompleted && this.resultsJSX()) ||
32
- this.loadingJSX()}
33
41
  </div>
34
- </div>
42
+ </dialog>
35
43
  </div>
36
44
  );
37
45
  }
38
46
 
39
- /*
40
- * Returns jQuery reference to the main modal container.
41
- */
42
- modal() {
43
- return $(this.modalRef.current);
44
- }
45
-
46
47
  /**
47
48
  * Shows sequence viewer.
48
49
  */
49
- show(url) {
50
- this.setState({ requestCompleted: false }, () => {
51
- this.modal().modal("show");
52
- this.loadJSON(url);
53
- });
50
+ show = (url) => {
51
+ this.modalRef.current?.showModal();
52
+ this.setState({ requestCompleted: false });
53
+ this.loadJSON(url);
54
54
  }
55
55
 
56
56
  /**
57
57
  * Hide sequence viewer.
58
58
  */
59
- hide() {
60
- this.modal().modal("hide");
59
+ hide = () => {
60
+ this.modalRef.current?.close();
61
61
  }
62
62
 
63
63
  /**
64
64
  * Loads sequence using AJAX and updates modal state.
65
65
  */
66
- loadJSON(url) {
66
+ async loadJSON(url) {
67
67
  // Fetch sequence and update state.
68
- $.getJSON(url)
69
- .done(
70
- _.bind(function (response) {
71
- this.setState({
72
- sequences: response.sequences,
73
- error_msgs: response.error_msgs,
74
- requestCompleted: true,
75
- });
76
- }, this)
77
- )
78
- .fail((jqXHR, status, error) => {
79
- this.hide();
80
- this.props.showErrorModal(jqXHR.responseJSON);
68
+ try {
69
+ const response = await fetch(url);
70
+ const data = await response.json();
71
+ this.setState({
72
+ sequences: data.sequences,
73
+ error_msgs: data.error_msgs,
74
+ requestCompleted: true,
81
75
  });
76
+ } catch (error) {
77
+ console.log('Error fetching sequence:', error);
78
+ this.hide();
79
+ this.props.showErrorModal(error);
80
+ }
82
81
  }
83
82
 
84
83
  resultsJSX() {
85
84
  return (
86
- <div className="modal-body">
87
- {_.map(
88
- this.state.error_msgs,
89
- _.bind(function (error_msg) {
90
- return (
91
- <div className="fastan">
92
- <div className="section-header">
93
- <h4>{error_msg[0]}</h4>
94
- </div>
95
- <div className="section-content">
96
- <pre className="pre-reset">{error_msg[1]}</pre>
97
- </div>
98
- </div>
99
- );
100
- }, this)
101
- )}
102
- {_.map(
103
- this.state.sequences,
104
- _.bind(function (sequence) {
105
- return <SequenceViewer sequence={sequence} />;
106
- }, this)
107
- )}
85
+ <div className="pt-2 px-6 pb-6 mt-2">
86
+ {this.state.error_msgs.map((error_msg, index) => (
87
+ <div key={`error-message-${index}`} className="fastan">
88
+ <div className="section-header border-b border-seqorange pl-px table mb-0 w-full pb-2">
89
+ <h4 className="text-sm table-cell">{error_msg[0]}</h4>
90
+ </div>
91
+ <div className="pt-0 px-0 pb-px">
92
+ <pre className="m-0 p-0 rounded-none border-0 bg-inherit whitespace-pre-wrap break-keep">{error_msg[1]}</pre>
93
+ </div>
94
+ </div>
95
+ ))}
96
+ {this.state.sequences.map((sequence, index) => (
97
+ <SequenceViewer key={`sequence-viewer-${index}`} sequence={sequence} />
98
+ ))}
108
99
  </div>
109
100
  );
110
101
  }
111
102
 
112
103
  loadingJSX() {
113
104
  return (
114
- <div className="modal-body text-center">
105
+ <div className="my-4 text-center">
115
106
  <i className="fa fa-spinner fa-3x fa-spin"></i>
116
107
  </div>
117
108
  );
@@ -132,13 +123,13 @@ class SequenceViewer extends React.Component {
132
123
 
133
124
  return (
134
125
  <div className="fastan">
135
- <div className="section-header">
136
- <h4>
126
+ <div className="section-header border-b border-seqorange pl-px table mb-0 w-full pb-2">
127
+ <h4 className="text-sm table-cell">
137
128
  {this.props.sequence.id}
138
- <small>&nbsp; {this.props.sequence.title}</small>
129
+ <small className="text-inherit">&nbsp; {this.props.sequence.title}</small>
139
130
  </h4>
140
131
  </div>
141
- <div className="section-content">
132
+ <div className="fastan-content relative pt-0 px-0 pb-px">
142
133
  <div className={this.widgetClass} id={this.widgetID}></div>
143
134
  </div>
144
135
  </div>
@@ -160,6 +151,8 @@ class SequenceViewer extends React.Component {
160
151
  footer: false,
161
152
  },
162
153
  });
163
- widget.hideFormatSelector();
154
+ setTimeout(function() {
155
+ requestAnimationFrame(() => { widget.hideFormatSelector() }); // ensure React is done painting the DOM of the element before calling a function on it.
156
+ });
164
157
  }
165
158
  }
@@ -10,14 +10,14 @@ const ShareURLComponent = ({ querydb, program, queryLength, url }) => {
10
10
  };
11
11
 
12
12
  return (
13
- <div className="share-url-component">
14
- <input name="shareableUrl" type="text" value={url} readOnly />
15
- <div className="actions">
16
- <button className="btn btn-primary" onClick={copyToClipboard}>{copied ? 'Copied!' : 'Copy to Clipboard'}</button>
13
+ <div className="px-6 share-url-component">
14
+ <input className="w-full mb-3 pt-3" name="shareableUrl" type="text" value={url} readOnly />
15
+ <div className="py-4 flex justify-between items-center">
16
+ <button className="py-2 px-3 border border-transparent rounded-md shadow-sm text-white bg-seqblue hover:bg-seqorange focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-seqorange" onClick={copyToClipboard}>{copied ? 'Copied!' : 'Copy to Clipboard'}</button>
17
17
  <a href={asMailtoHref(querydb, program, queryLength, url, true)}>Share via email</a>
18
18
  </div>
19
19
  </div>
20
20
  );
21
21
  };
22
22
 
23
- export default ShareURLComponent;
23
+ export default ShareURLComponent;
data/public/js/sidebar.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Component } from 'react';
1
+ import React, { Component } from 'react';
2
2
  import _ from 'underscore';
3
3
 
4
4
  import downloadFASTA from './download_fasta';
@@ -32,6 +32,7 @@ export default class extends Component {
32
32
  this.copyURL = this.copyURL.bind(this);
33
33
  this.shareCloudInit = this.shareCloudInit.bind(this);
34
34
  this.sharingPanelJSX = this.sharingPanelJSX.bind(this);
35
+ this.cloudShareModal = React.createRef();
35
36
  this.timeout = null;
36
37
  this.queryElems = [];
37
38
  this.state = {
@@ -40,14 +41,6 @@ export default class extends Component {
40
41
  }
41
42
 
42
43
  componentDidMount() {
43
- /**
44
- * Fixes tooltips in the sidebar, allows tooltip display on click
45
- */
46
- $(function () {
47
- $('.sidebar [data-toggle="tooltip"]').tooltip({ placement: 'right' });
48
- $('#copyURL').tooltip({ title: 'Copied!', trigger: 'click', placement: 'right', delay: 0 });
49
- });
50
-
51
44
  //keep track of the current queryIndex so it doesn't get lost on page reload
52
45
  const urlMatch = window.location.href.match(/#Query_(\d+)/);
53
46
  if (urlMatch && urlMatch.length > 1) {
@@ -180,30 +173,30 @@ export default class extends Component {
180
173
  return false;
181
174
  }
182
175
 
183
-
184
-
185
-
186
176
  /**
187
177
  * Handles copying the URL into the user's clipboard. Modified from: https://stackoverflow.com/a/49618964/18117380
188
178
  * Hides the 'Copied!' tooltip after 3 seconds
189
179
  */
190
180
 
191
181
  copyURL() {
192
- var element = document.createElement('input');
193
- var url = window.location.href;
182
+ const element = document.createElement('input');
183
+ const url = window.location.href;
194
184
  document.body.appendChild(element);
195
185
  element.value = url;
196
186
  element.select();
197
187
  document.execCommand('copy');
198
188
  document.body.removeChild(element);
199
189
 
200
- setTimeout(function () {
201
- $('#copyURL')._tooltip('hide');
190
+ const tooltip = document.getElementById('tooltip');
191
+ tooltip.classList.remove('hidden');
192
+
193
+ setTimeout(() => {
194
+ tooltip.classList.add('hidden');
202
195
  }, 3000);
203
196
  }
204
197
 
205
198
  shareCloudInit() {
206
- this.refs.cloudShareModal.show();
199
+ this.cloudShareModal.current.show();
207
200
  }
208
201
 
209
202
  topPanelJSX() {
@@ -215,20 +208,20 @@ export default class extends Component {
215
208
  var rootURL = path.join('/');
216
209
  return (
217
210
  <div className="sidebar-top-panel">
218
- <div className="section-header-sidebar">
219
- <h4>
211
+ <div className="pl-px table mb-0 w-full">
212
+ <h4 className="text-sm font-bold mb-0 mt-0.5">
220
213
  {this.summaryString()}
221
214
  </h4>
222
215
  </div>
223
216
  {this.props.data.queries.length > 12 && this.queryIndexButtons()}
224
217
  <div>
225
- <a href={`${rootURL}/?job_id=${job_id}`}>
218
+ <a href={`${rootURL}/?job_id=${job_id}`} className="text-sm text-seqblue hover:text-seqorange cursor-pointer">
226
219
  <i className="fa fa-pencil"></i> Edit search
227
220
  </a>
228
- <span className="line">|</span>
221
+ <span className="text-seqorange px-1">|</span>
229
222
  <a href={`${rootURL}/`}
230
- onClick={this.clearSession}>
231
- <i className="fa fa-file-o"></i> New search
223
+ onClick={this.clearSession} className="text-sm text-seqblue hover:text-seqorange cursor-pointer">
224
+ <i className="fa-regular fa-file"></i> New search
232
225
  </a>
233
226
  </div>
234
227
  {this.props.shouldShowIndex && this.indexJSX()}
@@ -252,7 +245,7 @@ export default class extends Component {
252
245
  const buttonStyle = {
253
246
  outline: 'none', border: 'none', background: 'none'
254
247
  };
255
- const buttonClasses = 'btn-link nowrap-ellipsis hover-bold';
248
+ const buttonClasses = 'text-sm text-seqblue hover:text-seqorange hover:bg-gray-200';
256
249
 
257
250
  const handlePreviousBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex - 1);
258
251
  const handleNextBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex + 1);
@@ -263,15 +256,15 @@ export default class extends Component {
263
256
  );
264
257
  return <div style={{ display: 'flex', width: '100%', margin: '7px 0' }}>
265
258
  {this.state.queryIndex > 1 && <NavButton text="Previous Query" onClick={handlePreviousBtnClick} />}
266
- {this.state.queryIndex > 1 && this.state.queryIndex < this.props.data.queries.length && <span className="line">|</span>}
259
+ {this.state.queryIndex > 1 && this.state.queryIndex < this.props.data.queries.length && <span className="text-seqorange px-1">|</span>}
267
260
  {this.state.queryIndex < this.props.data.queries.length && <NavButton onClick={handleNextBtnClick} text="Next Query" />}
268
261
  </div>;
269
262
  }
270
263
  indexJSX() {
271
- return <ul className="nav hover-reset active-bold"> {
264
+ return <ul className="w-full"> {
272
265
  _.map(this.props.data.queries, (query) => {
273
266
  return <li key={'Side_bar_' + query.id}>
274
- <a className="btn-link nowrap-ellipsis hover-bold"
267
+ <a className="side-nav text-sm text-seqblue hover:text-seqorange focus:text-seqorange active:text-seqorange cursor-pointer hover-bold line-clamp-1 mb-1.5"
275
268
  title={'Query= ' + query.id + ' ' + query.title}
276
269
  href={'#Query_' + query.number}>
277
270
  {'Query= ' + query.id}
@@ -285,77 +278,113 @@ export default class extends Component {
285
278
  downloadsPanelJSX() {
286
279
  return (
287
280
  <div className="downloads">
288
- <div className="section-header-sidebar">
289
- <h4>
281
+ <div className="pl-px table mb-0 w-full">
282
+ <h4 className="text-sm font-bold mb-0 mt-2.5">
290
283
  Download FASTA, XML, TSV
291
284
  </h4>
292
285
  </div>
293
- <ul className="nav">
286
+ <ul>
294
287
  {
295
- !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
296
- <a href="#" className={`btn-link download-fasta-of-all ${!this.props.atLeastOneHit && 'disabled'}`}
297
- onClick={this.downloadFastaOfAll}>
298
- FASTA of all hits
288
+ !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li className="hover:bg-gray-200 mb-1">
289
+ <a
290
+ href="#"
291
+ className={`text-sm text-seqblue download-fasta-of-all hover:text-seqorange cursor-pointer py-0.5 px-0.5 ${!this.props.atLeastOneHit && 'disabled'}`}
292
+ onClick={this.props.atLeastOneHit ? this.downloadFastaOfAll : (e) => e.preventDefault()}>
293
+ FASTA of all hits
299
294
  </a>
300
295
  </li>
301
296
  }
302
297
  {
303
- !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
304
- <a href="#" className="btn-link download-fasta-of-selected disabled"
298
+ !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li className="mb-1">
299
+ <a
300
+ href="#"
301
+ className="flex text-sm download-fasta-of-selected text-seqblue disabled py-0.5 px-0.5"
305
302
  onClick={this.downloadFastaOfSelected}>
306
- FASTA of <span className="text-bold"></span> selected hit(s)
303
+ FASTA of <span className="font-bold px-0.5"></span> selected hit(s)
307
304
  </a>
308
305
  </li>
309
306
  }
310
- <li>
311
- <a href="#" className={`btn-link download-alignment-of-all ${!this.props.atLeastOneHit && 'disabled'}`}>
307
+ <li className="hover:bg-gray-200 mb-1">
308
+ <a href="#" className={`text-sm text-seqblue download-alignment-of-all hover:text-seqorange cursor-pointer py-0.5 px-0.5 ${!this.props.atLeastOneHit && 'disabled'}`}>
312
309
  Alignment of all hits
313
310
  </a>
314
311
  </li>
315
- <li>
316
- <a href="#" className="btn-link download-alignment-of-selected disabled">
317
- Alignment of <span className="text-bold"></span> selected hit(s)
312
+ <li className="mb-1">
313
+ <a href="#" className="flex text-sm download-alignment-of-selected text-seqblue disabled py-0.5 px-0.5">
314
+ Alignment of <span className="font-bold px-0.5"></span> selected hit(s)
318
315
  </a>
319
316
  </li>
320
317
  {
321
- !this.props.data.imported_xml && <li>
322
- <a className="btn-link download" data-toggle="tooltip"
323
- title="15 columns: query and subject ID; scientific
324
- name, alignment length, mismatches, gaps, identity,
325
- start and end coordinates, e value, bitscore, query
326
- coverage per subject and per HSP."
327
- href={'download/' + this.props.data.search_id + '.std_tsv'}>
328
- Standard tabular report
318
+ !this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
319
+ <a href={'download/' + this.props.data.search_id + '.std_tsv'}>
320
+ <div className="relative flex flex-col items-center group">
321
+ <div className="flex items-center w-full">
322
+ <span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Standard tabular report</span>
323
+ <div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
324
+ <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
325
+ <span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
326
+ 15 columns: query and subject ID; scientific
327
+ name, alignment length, mismatches, gaps, identity,
328
+ start and end coordinates, e value, bitscore, query
329
+ coverage per subject and per HSP.
330
+ </span>
331
+ </div>
332
+ </div>
333
+ </div>
329
334
  </a>
330
335
  </li>
331
336
  }
332
337
  {
333
- !this.props.data.imported_xml && <li>
334
- <a className="btn-link download" data-toggle="tooltip"
335
- title="44 columns: query and subject ID, GI,
336
- accessions, and length; alignment details;
337
- taxonomy details of subject sequence(s) and
338
- query coverage per subject and per HSP."
339
- href={'download/' + this.props.data.search_id + '.full_tsv'}>
340
- Full tabular report
338
+ !this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
339
+ <a href={'download/' + this.props.data.search_id + '.full_tsv'}>
340
+ <div className="relative flex flex-col items-center group">
341
+ <div className="flex items-center w-full">
342
+ <span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Full tabular report</span>
343
+ <div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
344
+ <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
345
+ <span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
346
+ 44 columns: query and subject ID, GI,
347
+ accessions, and length; alignment details;
348
+ taxonomy details of subject sequence(s) and
349
+ query coverage per subject and per HSP.
350
+ </span>
351
+ </div>
352
+ </div>
353
+ </div>
341
354
  </a>
342
355
  </li>
343
356
  }
344
357
  {
345
- !this.props.data.imported_xml && <li>
346
- <a className="btn-link download" data-toggle="tooltip"
347
- title="Results in XML format."
348
- href={'download/' + this.props.data.search_id + '.xml'}>
349
- Full XML report
358
+ !this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
359
+ <a href={'download/' + this.props.data.search_id + '.xml'}>
360
+ <div className="relative flex flex-col items-center group">
361
+ <div className="flex items-center w-full">
362
+ <span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Full XML report</span>
363
+ <div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
364
+ <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
365
+ <span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
366
+ Results in XML format.
367
+ </span>
368
+ </div>
369
+ </div>
370
+ </div>
350
371
  </a>
351
372
  </li>
352
373
  }
353
374
  {
354
- !this.props.data.imported_xml && <li>
355
- <a className="btn-link download" data-toggle="tooltip"
356
- title="Results in pairwise format."
357
- href={'download/' + this.props.data.search_id + '.pairwise'}>
358
- Full Pairwise report
375
+ !this.props.data.imported_xml && <li className="hover:bg-gray-200 mb-1">
376
+ <a href={'download/' + this.props.data.search_id + '.pairwise'}>
377
+ <div className="relative flex flex-col items-center group">
378
+ <div className="flex items-center w-full">
379
+ <span className="w-full text-sm text-seqblue hover:text-seqorange download cursor-pointer py-0.5 px-0.5">Full Text report</span>
380
+ <div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
381
+ <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
382
+ <span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
383
+ Results in text format.
384
+ </span>
385
+ </div>
386
+ </div>
387
+ </div>
359
388
  </a>
360
389
  </li>
361
390
  }
@@ -368,42 +397,76 @@ export default class extends Component {
368
397
  sharingPanelJSX() {
369
398
  return (
370
399
  <div className="sharing-panel">
371
- <div className="section-header-sidebar">
372
- <h4>
400
+ <div className="pl-px table mb-0 w-full">
401
+ <h4 className="text-sm font-bold mb-0 mt-2.5">
373
402
  Share results
374
403
  </h4>
375
404
  </div>
376
- <ul className="nav">
405
+ <ul>
377
406
  {!this.props.cloudSharingEnabled &&
378
- <li>
379
- <a id="copyURL" className="btn-link copy-URL cursor-pointer" data-toggle="tooltip"
380
- onClick={this.copyURL}>
381
- <i className="fa fa-copy"></i> Copy URL to clipboard
407
+ <li className="hover:text-seqorange hover:bg-gray-200">
408
+ <a id="copyURL" className="flex text-sm text-seqblue hover:text-seqorange copy-URL cursor-pointer py-0.5 px-0.5 w-full" onClick={this.copyURL}>
409
+ <div className="relative flex gap-2 items-center group w-full">
410
+ <i className="fa fa-copy"></i>
411
+ <div className="flex items-center">
412
+ <span className="w-full">Copy URL to clipboard</span>
413
+ <div id="tooltip" className="absolute hidden left-full ml-2 items-center">
414
+ <div className="flex items-center">
415
+ <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
416
+ <span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
417
+ Copied!
418
+ </span>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </div>
382
423
  </a>
383
424
  </li>
384
425
  }
385
426
  {!this.props.cloudSharingEnabled &&
386
- <li>
387
- <a id="sendEmail" className="btn-link email-URL cursor-pointer" data-toggle="tooltip"
388
- title="Send by email" href={asMailtoHref(this.props.data.querydb, this.props.data.program, this.props.data.queries.length, window.location.href)}
427
+ <li className="hover:text-seqorange hover:bg-gray-200">
428
+ <a id="sendEmail" className="flex text-sm text-seqblue hover:text-seqorange email-URL cursor-pointer py-0.5 px-0.5 w-full"
429
+ href={asMailtoHref(this.props.data.querydb, this.props.data.program, this.props.data.queries.length, window.location.href)}
389
430
  target="_blank" rel="noopener noreferrer">
390
- <i className="fa fa-envelope"></i> Send by email
431
+ <div className="relative flex gap-2 items-center group w-full">
432
+ <i className="fa fa-envelope"></i>
433
+ <div className="flex items-center w-full">
434
+ <span className="w-full">Send by email</span>
435
+ <div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
436
+ <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
437
+ <span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
438
+ Send by email
439
+ </span>
440
+ </div>
441
+ </div>
442
+ </div>
391
443
  </a>
392
444
  </li>
393
445
  }
394
446
  {this.props.cloudSharingEnabled &&
395
- <li>
396
- <button className="btn-link cloud-Post cursor-pointer" data-toggle="tooltip"
397
- title="Upload results to SequenceServer Cloud where it will become accessable
398
- to everyone who has a link." onClick={this.shareCloudInit}>
399
- <i className="fa fa-cloud"></i> Share to cloud
447
+ <li className="hover:text-seqorange hover:bg-gray-200">
448
+ <button className="flex text-sm text-seqblue hover:text-seqorange cloud-Post cursor-pointer py-0.5 px-0.5 w-full" onClick={this.shareCloudInit}>
449
+ <div className="relative flex gap-2 items-center group w-full">
450
+ <i className="fa fa-cloud"></i>
451
+ <div className="flex items-center">
452
+ <span className="w-full">Share to cloud</span>
453
+ <div className="absolute hidden left-full ml-2 items-center group-hover:flex tooltip-wrap">
454
+ <div className="w-0 h-0 border-y-8 border-r-8 border-t-transparent border-b-transparent border-r-black -mr-px"></div>
455
+ <span className="relative z-10 p-2 side-tooltip-text leading-4 text-center text-white whitespace-no-wrap bg-black shadow-lg rounded-md">
456
+ Results in pairwise format
457
+ Upload results to SequenceServer Cloud where it will become accessable
458
+ to everyone who has a link.
459
+ </span>
460
+ </div>
461
+ </div>
462
+ </div>
400
463
  </button>
401
464
  </li>
402
465
  }
403
466
  </ul>
404
467
  {
405
468
  <CloudShareModal
406
- ref="cloudShareModal"
469
+ ref={this.cloudShareModal}
407
470
  querydb={this.props.data.querydb}
408
471
  program={this.props.data.program}
409
472
  queryLength={this.props.data.queries.length}
@@ -415,14 +478,14 @@ export default class extends Component {
415
478
 
416
479
  render() {
417
480
  return (
418
- <div className="sidebar">
481
+ <div className="sidebar sticky top-0 print:hidden">
419
482
  {this.topPanelJSX()}
420
483
  {this.downloadsPanelJSX()}
421
484
  {this.sharingPanelJSX()}
422
485
  <div className="referral-panel">
423
- <div className="section-header-sidebar">
424
- <h4>Recommend SequenceServer</h4>
425
- <p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $100 per signup</a></p>
486
+ <div className="pl-px table mb-0 w-full text-sm">
487
+ <h4 className="font-bold mb-0 mt-2.5">Recommend SequenceServer</h4>
488
+ <p><a href="https://sequenceserver.com/referral-program" target="_blank" className="text-seqblue hover:text-seqorange">Earn up to $400 per signup</a></p>
426
489
  </div>
427
490
  </div>
428
491
  </div>
@@ -8,6 +8,7 @@
8
8
  *
9
9
  */
10
10
  import * as Exporter from './exporter';
11
+ import * as d3 from 'd3';
11
12
 
12
13
  /**
13
14
  * Exports the given <svg> DOM node as a .svg file.
@@ -44,7 +45,11 @@ var export_as_png = function (svg, filename) {
44
45
  Exporter.download_url(canvas.toDataURL('image/png'), filename);
45
46
  };
46
47
 
47
- img.src = 'data:image/svg+xml;base64,' + window.btoa(serialize_svg(svg));
48
+ var svgString = serialize_svg(svg);
49
+ var encodedSvg = encodeURIComponent(svgString).replace(/%([0-9A-F]{2})/gi, function(match, p1) {
50
+ return String.fromCharCode('0x' + p1);
51
+ });
52
+ img.src = 'data:image/svg+xml;base64,' + window.btoa(encodedSvg);
48
53
  };
49
54
 
50
55
  var serialize_svg = function(svg) {
@@ -61,8 +66,8 @@ var serialize_svg = function(svg) {
61
66
 
62
67
  svg.removeAttribute('xmlns');
63
68
  svg.removeAttribute('xlink');
64
- svg.setAttributeNS(d3.ns.prefix.xmlns, 'xmlns', d3.ns.prefix.svg);
65
- svg.setAttributeNS(d3.ns.prefix.xmlns, 'xmlns:xlink', d3.ns.prefix.xlink);
69
+ svg.setAttributeNS(d3.namespaces.xmlns, 'xmlns', d3.namespaces.svg);
70
+ svg.setAttributeNS(d3.namespaces.xmlns, 'xmlns:xlink', d3.namespaces.xlink);
66
71
 
67
72
  var source = (new XMLSerializer()).serializeToString(svg);
68
73
  var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC ' +