sequenceserver 3.1.3 → 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 (68) 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/database.rb +5 -1
  7. data/lib/sequenceserver/job.rb +26 -10
  8. data/lib/sequenceserver/makeblastdb.rb +2 -1
  9. data/lib/sequenceserver/routes.rb +37 -12
  10. data/lib/sequenceserver/version.rb +1 -1
  11. data/lib/sequenceserver/zip_file_generator.rb +1 -1
  12. data/public/404.html +8 -16
  13. data/public/css/app.css +794 -111
  14. data/public/css/app.min.css +1 -1
  15. data/public/css/sequenceserver.min.css +1 -8
  16. data/public/js/circos.js +103 -96
  17. data/public/{packages/circosJS@1.7.0.js → js/circosjs.js} +156 -778
  18. data/public/js/cloud_share_modal.js +55 -58
  19. data/public/js/collapse_preferences.js +13 -15
  20. data/public/js/databases.js +4 -3
  21. data/public/js/databases_tree.js +51 -30
  22. data/public/js/error_modal.js +35 -22
  23. data/public/js/exporter.js +2 -0
  24. data/public/js/form.js +43 -7
  25. data/public/js/grapher.js +94 -92
  26. data/public/js/hit.js +21 -19
  27. data/public/js/hits.js +45 -38
  28. data/public/js/hits_overview.js +8 -27
  29. data/public/js/hsp.js +140 -144
  30. data/public/js/jquery_world.js +24 -15
  31. data/public/js/kablammo.js +14 -10
  32. data/public/js/length_distribution.js +23 -37
  33. data/public/js/null_plugins/report_plugins.js +3 -5
  34. data/public/js/options.js +17 -19
  35. data/public/js/query.js +36 -35
  36. data/public/js/report/alignment_results.js +18 -0
  37. data/public/js/report/graphical_overview.js +38 -0
  38. data/public/js/report/report_header.js +28 -0
  39. data/public/js/report/run_summary.js +43 -0
  40. data/public/js/report.js +62 -102
  41. data/public/js/report_root.js +3 -3
  42. data/public/js/search.js +5 -2
  43. data/public/js/search_button.js +85 -78
  44. data/public/js/sequence.js +11 -22
  45. data/public/js/sequence_modal.js +38 -37
  46. data/public/js/share_url.js +5 -5
  47. data/public/js/sidebar.js +148 -86
  48. data/public/js/svgExporter.js +8 -3
  49. data/public/js/tests/form.spec.js +1 -1
  50. data/public/js/tests/visualisation_helpers.spec.js +28 -0
  51. data/public/js/visualisation_helpers.js +24 -27
  52. data/public/sequenceserver-report.min.js +1 -1
  53. data/public/sequenceserver-report.min.js.LICENSE.txt +1 -88
  54. data/public/sequenceserver-report.min.js.map +1 -1
  55. data/public/sequenceserver-search.min.js +1 -1
  56. data/public/sequenceserver-search.min.js.LICENSE.txt +0 -30
  57. data/public/sequenceserver-search.min.js.map +1 -1
  58. data/views/error.erb +1 -1
  59. data/views/layout.erb +123 -130
  60. data/views/report.erb +3 -2
  61. data/views/search.erb +10 -14
  62. metadata +13 -26
  63. data/public/css/fonts.css +0 -31
  64. data/public/css/grapher.css +0 -670
  65. data/public/css/sequenceserver.css +0 -509
  66. data/public/vendor/github/mbostock/d3@3.5.6/d3.js +0 -9506
  67. data/public/vendor/github/mbostock/d3@3.5.6.js +0 -1
  68. 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,44 +21,43 @@ 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
  /**
@@ -66,29 +66,30 @@ export default class SequenceModal extends React.Component {
66
66
  async loadJSON(url) {
67
67
  // Fetch sequence and update state.
68
68
  try {
69
- const response = await $.getJSON(url);
69
+ const response = await fetch(url);
70
+ const data = await response.json();
70
71
  this.setState({
71
- sequences: response.sequences,
72
- error_msgs: response.error_msgs,
72
+ sequences: data.sequences,
73
+ error_msgs: data.error_msgs,
73
74
  requestCompleted: true,
74
75
  });
75
76
  } catch (error) {
76
77
  console.log('Error fetching sequence:', error);
77
78
  this.hide();
78
- this.props.showErrorModal(error.responseJSON);
79
+ this.props.showErrorModal(error);
79
80
  }
80
81
  }
81
82
 
82
83
  resultsJSX() {
83
84
  return (
84
- <div className="modal-body">
85
+ <div className="pt-2 px-6 pb-6 mt-2">
85
86
  {this.state.error_msgs.map((error_msg, index) => (
86
87
  <div key={`error-message-${index}`} className="fastan">
87
- <div className="section-header">
88
- <h4>{error_msg[0]}</h4>
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>
89
90
  </div>
90
- <div className="section-content">
91
- <pre className="pre-reset">{error_msg[1]}</pre>
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>
92
93
  </div>
93
94
  </div>
94
95
  ))}
@@ -101,7 +102,7 @@ export default class SequenceModal extends React.Component {
101
102
 
102
103
  loadingJSX() {
103
104
  return (
104
- <div className="modal-body text-center">
105
+ <div className="my-4 text-center">
105
106
  <i className="fa fa-spinner fa-3x fa-spin"></i>
106
107
  </div>
107
108
  );
@@ -122,13 +123,13 @@ class SequenceViewer extends React.Component {
122
123
 
123
124
  return (
124
125
  <div className="fastan">
125
- <div className="section-header">
126
- <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">
127
128
  {this.props.sequence.id}
128
- <small>&nbsp; {this.props.sequence.title}</small>
129
+ <small className="text-inherit">&nbsp; {this.props.sequence.title}</small>
129
130
  </h4>
130
131
  </div>
131
- <div className="section-content">
132
+ <div className="fastan-content relative pt-0 px-0 pb-px">
132
133
  <div className={this.widgetClass} id={this.widgetID}></div>
133
134
  </div>
134
135
  </div>
@@ -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
@@ -41,14 +41,6 @@ export default class extends Component {
41
41
  }
42
42
 
43
43
  componentDidMount() {
44
- /**
45
- * Fixes tooltips in the sidebar, allows tooltip display on click
46
- */
47
- $(function () {
48
- $('.sidebar [data-toggle="tooltip"]').tooltip({ placement: 'right' });
49
- $('#copyURL').tooltip({ title: 'Copied!', trigger: 'click', placement: 'right', delay: 0 });
50
- });
51
-
52
44
  //keep track of the current queryIndex so it doesn't get lost on page reload
53
45
  const urlMatch = window.location.href.match(/#Query_(\d+)/);
54
46
  if (urlMatch && urlMatch.length > 1) {
@@ -181,25 +173,25 @@ export default class extends Component {
181
173
  return false;
182
174
  }
183
175
 
184
-
185
-
186
-
187
176
  /**
188
177
  * Handles copying the URL into the user's clipboard. Modified from: https://stackoverflow.com/a/49618964/18117380
189
178
  * Hides the 'Copied!' tooltip after 3 seconds
190
179
  */
191
180
 
192
181
  copyURL() {
193
- var element = document.createElement('input');
194
- var url = window.location.href;
182
+ const element = document.createElement('input');
183
+ const url = window.location.href;
195
184
  document.body.appendChild(element);
196
185
  element.value = url;
197
186
  element.select();
198
187
  document.execCommand('copy');
199
188
  document.body.removeChild(element);
200
189
 
201
- setTimeout(function () {
202
- $('#copyURL')._tooltip('hide');
190
+ const tooltip = document.getElementById('tooltip');
191
+ tooltip.classList.remove('hidden');
192
+
193
+ setTimeout(() => {
194
+ tooltip.classList.add('hidden');
203
195
  }, 3000);
204
196
  }
205
197
 
@@ -216,20 +208,20 @@ export default class extends Component {
216
208
  var rootURL = path.join('/');
217
209
  return (
218
210
  <div className="sidebar-top-panel">
219
- <div className="section-header-sidebar">
220
- <h4>
211
+ <div className="pl-px table mb-0 w-full">
212
+ <h4 className="text-sm font-bold mb-0 mt-0.5">
221
213
  {this.summaryString()}
222
214
  </h4>
223
215
  </div>
224
216
  {this.props.data.queries.length > 12 && this.queryIndexButtons()}
225
217
  <div>
226
- <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">
227
219
  <i className="fa fa-pencil"></i> Edit search
228
220
  </a>
229
- <span className="line">|</span>
221
+ <span className="text-seqorange px-1">|</span>
230
222
  <a href={`${rootURL}/`}
231
- onClick={this.clearSession}>
232
- <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
233
225
  </a>
234
226
  </div>
235
227
  {this.props.shouldShowIndex && this.indexJSX()}
@@ -253,7 +245,7 @@ export default class extends Component {
253
245
  const buttonStyle = {
254
246
  outline: 'none', border: 'none', background: 'none'
255
247
  };
256
- const buttonClasses = 'btn-link nowrap-ellipsis hover-bold';
248
+ const buttonClasses = 'text-sm text-seqblue hover:text-seqorange hover:bg-gray-200';
257
249
 
258
250
  const handlePreviousBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex - 1);
259
251
  const handleNextBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex + 1);
@@ -264,15 +256,15 @@ export default class extends Component {
264
256
  );
265
257
  return <div style={{ display: 'flex', width: '100%', margin: '7px 0' }}>
266
258
  {this.state.queryIndex > 1 && <NavButton text="Previous Query" onClick={handlePreviousBtnClick} />}
267
- {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>}
268
260
  {this.state.queryIndex < this.props.data.queries.length && <NavButton onClick={handleNextBtnClick} text="Next Query" />}
269
261
  </div>;
270
262
  }
271
263
  indexJSX() {
272
- return <ul className="nav hover-reset active-bold"> {
264
+ return <ul className="w-full"> {
273
265
  _.map(this.props.data.queries, (query) => {
274
266
  return <li key={'Side_bar_' + query.id}>
275
- <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"
276
268
  title={'Query= ' + query.id + ' ' + query.title}
277
269
  href={'#Query_' + query.number}>
278
270
  {'Query= ' + query.id}
@@ -286,77 +278,113 @@ export default class extends Component {
286
278
  downloadsPanelJSX() {
287
279
  return (
288
280
  <div className="downloads">
289
- <div className="section-header-sidebar">
290
- <h4>
281
+ <div className="pl-px table mb-0 w-full">
282
+ <h4 className="text-sm font-bold mb-0 mt-2.5">
291
283
  Download FASTA, XML, TSV
292
284
  </h4>
293
285
  </div>
294
- <ul className="nav">
286
+ <ul>
295
287
  {
296
- !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
297
- <a href="#" className={`btn-link download-fasta-of-all ${!this.props.atLeastOneHit && 'disabled'}`}
298
- onClick={this.downloadFastaOfAll}>
299
- 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
300
294
  </a>
301
295
  </li>
302
296
  }
303
297
  {
304
- !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
305
- <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"
306
302
  onClick={this.downloadFastaOfSelected}>
307
- FASTA of <span className="text-bold"></span> selected hit(s)
303
+ FASTA of <span className="font-bold px-0.5"></span> selected hit(s)
308
304
  </a>
309
305
  </li>
310
306
  }
311
- <li>
312
- <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'}`}>
313
309
  Alignment of all hits
314
310
  </a>
315
311
  </li>
316
- <li>
317
- <a href="#" className="btn-link download-alignment-of-selected disabled">
318
- 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)
319
315
  </a>
320
316
  </li>
321
317
  {
322
- !this.props.data.imported_xml && <li>
323
- <a className="btn-link download" data-toggle="tooltip"
324
- title="15 columns: query and subject ID; scientific
325
- name, alignment length, mismatches, gaps, identity,
326
- start and end coordinates, e value, bitscore, query
327
- coverage per subject and per HSP."
328
- href={'download/' + this.props.data.search_id + '.std_tsv'}>
329
- 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>
330
334
  </a>
331
335
  </li>
332
336
  }
333
337
  {
334
- !this.props.data.imported_xml && <li>
335
- <a className="btn-link download" data-toggle="tooltip"
336
- title="44 columns: query and subject ID, GI,
337
- accessions, and length; alignment details;
338
- taxonomy details of subject sequence(s) and
339
- query coverage per subject and per HSP."
340
- href={'download/' + this.props.data.search_id + '.full_tsv'}>
341
- 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>
342
354
  </a>
343
355
  </li>
344
356
  }
345
357
  {
346
- !this.props.data.imported_xml && <li>
347
- <a className="btn-link download" data-toggle="tooltip"
348
- title="Results in XML format."
349
- href={'download/' + this.props.data.search_id + '.xml'}>
350
- 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>
351
371
  </a>
352
372
  </li>
353
373
  }
354
374
  {
355
- !this.props.data.imported_xml && <li>
356
- <a className="btn-link download" data-toggle="tooltip"
357
- title="Results in text format."
358
- href={'download/' + this.props.data.search_id + '.pairwise'}>
359
- Full Text 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>
360
388
  </a>
361
389
  </li>
362
390
  }
@@ -369,35 +397,69 @@ export default class extends Component {
369
397
  sharingPanelJSX() {
370
398
  return (
371
399
  <div className="sharing-panel">
372
- <div className="section-header-sidebar">
373
- <h4>
400
+ <div className="pl-px table mb-0 w-full">
401
+ <h4 className="text-sm font-bold mb-0 mt-2.5">
374
402
  Share results
375
403
  </h4>
376
404
  </div>
377
- <ul className="nav">
405
+ <ul>
378
406
  {!this.props.cloudSharingEnabled &&
379
- <li>
380
- <a id="copyURL" className="btn-link copy-URL cursor-pointer" data-toggle="tooltip"
381
- onClick={this.copyURL}>
382
- <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>
383
423
  </a>
384
424
  </li>
385
425
  }
386
426
  {!this.props.cloudSharingEnabled &&
387
- <li>
388
- <a id="sendEmail" className="btn-link email-URL cursor-pointer" data-toggle="tooltip"
389
- 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)}
390
430
  target="_blank" rel="noopener noreferrer">
391
- <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>
392
443
  </a>
393
444
  </li>
394
445
  }
395
446
  {this.props.cloudSharingEnabled &&
396
- <li>
397
- <button className="btn-link cloud-Post cursor-pointer" data-toggle="tooltip"
398
- title="Upload results to SequenceServer Cloud where it will become accessable
399
- to everyone who has a link." onClick={this.shareCloudInit}>
400
- <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>
401
463
  </button>
402
464
  </li>
403
465
  }
@@ -416,14 +478,14 @@ export default class extends Component {
416
478
 
417
479
  render() {
418
480
  return (
419
- <div className="sidebar">
481
+ <div className="sidebar sticky top-0 print:hidden">
420
482
  {this.topPanelJSX()}
421
483
  {this.downloadsPanelJSX()}
422
484
  {this.sharingPanelJSX()}
423
485
  <div className="referral-panel">
424
- <div className="section-header-sidebar">
425
- <h4>Recommend SequenceServer</h4>
426
- <p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $400 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>
427
489
  </div>
428
490
  </div>
429
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 ' +
@@ -12,7 +12,7 @@ export const setMockJSONResult = (result) => {
12
12
  global.$.getJSON = (_, cb) => cb(result);
13
13
  };
14
14
 
15
- describe('ADVANCED PARAMETERS', () => {
15
+ describe('PARAMETERS', () => {
16
16
  let csrfMetaTag;
17
17
 
18
18
  beforeEach(() => {
@@ -0,0 +1,28 @@
1
+ // Import the necessary functions
2
+ import { tick_formatter, formatNumberUnits } from '../visualisation_helpers';
3
+
4
+ describe('tick_formatter', () => {
5
+ test('formats tick for amino_acid sequence', () => {
6
+ const formatter = tick_formatter(null, 'amino_acid');
7
+ expect(formatter(1_000)).toBe('1 kaa');
8
+ expect(formatter(25_286_936)).toBe('25.3 Maa');
9
+ });
10
+
11
+ test('formats tick for nucleic_acid sequence', () => {
12
+ const formatter = tick_formatter(null, 'nucleic_acid');
13
+ expect(formatter(1_000)).toBe('1 kbp');
14
+ expect(formatter(25_965_000)).toBe('26 Mbp');
15
+ });
16
+ });
17
+
18
+ describe('formatNumberUnits', () => {
19
+ test('formats number with units correctly', () => {
20
+ expect(formatNumberUnits('1.9k')).toBe('1.9 k');
21
+ expect(formatNumberUnits('1.5k')).toBe('1.5 k');
22
+ });
23
+
24
+ test('rounds up correctly based on threshold', () => {
25
+ expect(formatNumberUnits('2.95k')).toBe('3 k');
26
+ expect(formatNumberUnits('2.94k')).toBe('2.9 k');
27
+ });
28
+ });