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
@@ -15,6 +15,7 @@ export default class CloudShareModal extends React.Component {
15
15
  email: '',
16
16
  agreeToTos: false,
17
17
  shareableurl: '',
18
+ isModalVisible: false,
18
19
  };
19
20
  this.modalRef = createRef();
20
21
  }
@@ -28,13 +29,11 @@ export default class CloudShareModal extends React.Component {
28
29
  this.setState({ [name]: inputValue });
29
30
  }
30
31
 
31
- handleSubmit = (e) => {
32
+ handleSubmit = async (e) => {
32
33
  e.preventDefault();
33
34
 
34
35
  const { email } = this.state;
35
- const regex = /\/([^/]+)(?:\/|#|\?|$)/;
36
- const match = window.location.pathname.match(regex);
37
- const jobId = match[1];
36
+ const jobId = this.getJobIdFromPath();
38
37
 
39
38
  this.setState({ formState: 'loading' });
40
39
 
@@ -43,40 +42,55 @@ export default class CloudShareModal extends React.Component {
43
42
  sender_email: email
44
43
  };
45
44
 
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
- });
45
+ try {
46
+ const response = await fetch('/cloud_share', {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/json'
50
+ },
51
+ body: JSON.stringify(requestData)
72
52
  });
53
+
54
+ if (!response.ok) {
55
+ throw new Error('Network response was not ok');
56
+ }
57
+
58
+ const data = await response.json();
59
+
60
+ if (data.shareable_url) {
61
+ this.setState({ formState: 'results', shareableurl: data.shareable_url });
62
+ } else if (data.errors) {
63
+ this.setState({ formState: 'error', errorMessages: data.errors });
64
+ } else {
65
+ throw new Error('Unknown error submitting form');
66
+ }
67
+ } catch (error) {
68
+ this.setState({
69
+ formState: 'error',
70
+ errorMessages: [error.message]
71
+ });
72
+ }
73
+ }
74
+
75
+ getJobIdFromPath = () => {
76
+ const regex = /\/([^/]+)(?:\/|#|\?|$)/;
77
+ const match = window.location.pathname.match(regex);
78
+ return match ? match[1] : match;
79
+ }
80
+
81
+ show = () => {
82
+ this.modalRef.current?.showModal();
83
+ }
84
+
85
+ hide = () => {
86
+ this.modalRef.current?.close();
73
87
  }
74
88
 
75
89
  renderLoading() {
76
90
  return (
77
- <div className="text-center">
91
+ <div className="text-center pt-3">
78
92
  <i className="fa fa-spinner fa-3x fa-spin"></i>
79
- <p>Uploading the job to SequenceServer Cloud, please wait...</p>
93
+ <p className="my-3">Uploading the job to SequenceServer Cloud, please wait...</p>
80
94
  </div>
81
95
  );
82
96
  }
@@ -93,18 +107,13 @@ export default class CloudShareModal extends React.Component {
93
107
  return (
94
108
  <>
95
109
  {
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
- )
110
+ errorMessages.map((errorMessage, index) => (
111
+ <div key={`fastan-${index}`} className="fastan">
112
+ <div className="pt-0 px-0 pb-px">
113
+ <div className="text-danger text-lg border border-danger p-2 my-2">{errorMessage}</div>
114
+ </div>
115
+ </div>
116
+ ))
108
117
  }
109
118
  {this.renderForm()}
110
119
  </>
@@ -117,46 +126,52 @@ export default class CloudShareModal extends React.Component {
117
126
 
118
127
  return(
119
128
  <form onSubmit={this.handleSubmit}>
120
- <div className="form-group">
121
- <label htmlFor="emailInput">Your Email Address</label>
129
+ <div className="px-6 py-4 text-sm">
130
+ <label htmlFor="emailInput" className="text-seqblue hover:text-orange cursor-pointer mb-0">Your Email Address</label>
122
131
  <input
123
132
  type="email"
124
133
  id="emailInput"
125
- className="form-control"
134
+ className="block w-full rounded-md border-0 py-1.5 px-3 mb-3 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-seqblue sm:text-sm sm:leading-6"
126
135
  placeholder="person@example.com"
127
136
  name="email"
128
137
  value={email}
129
138
  required="required"
130
139
  onChange={this.handleChange}
131
140
  />
141
+ <p className="mb-3">
142
+ By submitting this form you agree to upload this SequenceServer result set to <a href="https://sequenceserver.com/cloud/" target="_bank" className="text-seqblue hover:text-seqorange">SenquenceServer Cloud</a>
143
+ , where it will become available on the internet to everyone with the link. You also agree that your email address will be stored on SequenceServer databases as proof of authentication for support and similar purposes.
144
+ </p>
145
+ <div className="form-check">
146
+ <input
147
+ type="checkbox"
148
+ id="tosCheckbox"
149
+ className="form-check-input"
150
+ name="agreeToTos"
151
+ checked={agreeToTos}
152
+ onChange={this.handleChange}
153
+ />
154
+ <label htmlFor="tosCheckbox" className="pl-2">
155
+ &nbsp;I agree to the <b><a href="https://sequenceserver.com/cloud/terms_and_conditions" target="_blank" className="text-seqblue hover:text-seqorange">Terms and Conditions of Service</a></b>
156
+ </label>
157
+ </div>
132
158
  </div>
133
- <p>
134
- By submitting this form you agree to upload this SequenceServer result set to <a href="https://sequenceserver.com/cloud/" target="_bank">SenquenceServer Cloud</a>
135
- , where it will become available on the internet to everyone with the link. You also agree that your email address will be stored on SequenceServer databases as proof of authentication for support and similar purposes.
136
- </p>
137
-
138
- <div className="form-group form-check">
139
- <input
140
- type="checkbox"
141
- id="tosCheckbox"
142
- className="form-check-input"
143
- name="agreeToTos"
144
- checked={agreeToTos}
145
- onChange={this.handleChange}
146
- />
147
- <label htmlFor="tosCheckbox" className="form-check-label">
148
- &nbsp;I agree to the <b><a href="https://sequenceserver.com/cloud/terms_and_conditions" target="_blank">Terms and Conditions of Service</a></b>
149
- </label>
159
+ <div className="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
160
+ <button
161
+ type="submit"
162
+ style={{ backgroundColor: isSubmitDisabled ? '#C74F13' : '#1B557A' }}
163
+ className='border-seqblue py-2 px-3 rounded-md text-white'
164
+ disabled={isSubmitDisabled}
165
+ >
166
+ Submit
167
+ </button>
150
168
  </div>
151
- <button type="submit" className="btn btn-primary" disabled={isSubmitDisabled}>
152
- Submit
153
- </button>
154
169
  </form>
155
170
  )
156
171
  }
157
172
 
158
173
  render() {
159
- const { formState } = this.state;
174
+ const { formState, isModalVisible } = this.state;
160
175
 
161
176
  let content;
162
177
  switch (formState) {
@@ -176,41 +191,23 @@ export default class CloudShareModal extends React.Component {
176
191
  }
177
192
 
178
193
  return (
179
- <div className="modal cloud-share" ref={this.modalRef} tabIndex="-1">
180
- <div className="modal-dialog">
181
- <div className="modal-content">
182
- <div className="modal-header">
183
- <h3>Share to SequenceServer Cloud</h3>
194
+ <div className="relative">
195
+ <dialog ref={this.modalRef} className="fixed p-4 w-full max-w-2xl bg-transparent focus:outline-none">
196
+ <div className="relative flex flex-col rounded-lg bg-white shadow">
197
+ <div className="flex items-start justify-between rounded-t border-b p-5">
198
+ <h3 className="text-xl font-medium text-gray-900">
199
+ Share to SequenceServer Cloud
200
+ </h3>
201
+ <button className="ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200" onClick={this.hide}>
202
+ <i className="fa-solid fa-xmark hover:text-black"></i>
203
+ </button>
184
204
  </div>
185
- <div className="modal-body">
205
+ <div className="dialog-content">
186
206
  {content}
187
207
  </div>
188
208
  </div>
189
- </div>
209
+ </dialog>
190
210
  </div>
191
211
  );
192
212
  }
193
-
194
- /*
195
- * Returns jQuery reference to the main modal container.
196
- */
197
- modal() {
198
- return $(this.modalRef.current);
199
- }
200
-
201
- /**
202
- * Shows share dialogue.
203
- */
204
- show() {
205
- this.setState({ requestCompleted: false }, () => {
206
- this.modal().modal("show");
207
- });
208
- }
209
-
210
- /**
211
- * Hide share dialogue.
212
- */
213
- hide() {
214
- this.modal().modal("hide");
215
- }
216
213
  }
@@ -1,26 +1,24 @@
1
1
  export default class CollapsePreferences {
2
- constructor(component) {
2
+ constructor(component, defaultCollapsed = false) {
3
3
  this.component = component;
4
- this.collapsePreferences = JSON.parse(localStorage.getItem('collapsePreferences')) || [];
4
+ this.defaultCollapsed = defaultCollapsed;
5
+ this.storageKey = 'sequenceServerCollapseStates';
6
+ this.collapseStates = JSON.parse(localStorage.getItem(this.storageKey)) || {};
5
7
  }
6
8
 
7
9
  toggleCollapse() {
8
10
  let currentlyCollapsed = this.component.state.collapsed;
9
-
10
11
  this.component.setState({ collapsed: !currentlyCollapsed });
11
12
 
12
- let collapsePreferences = JSON.parse(localStorage.getItem('collapsePreferences')) || [];
13
-
14
- if (currentlyCollapsed) {
15
- localStorage.setItem('collapsePreferences', JSON.stringify(collapsePreferences.filter((name) => name !== this.component.name)));
16
- } else {
17
- let uniqueCollapsePreferences = [... new Set(collapsePreferences.concat([this.component.name]))];
18
- localStorage.setItem('collapsePreferences', JSON.stringify(uniqueCollapsePreferences));
19
- }
13
+ this.collapseStates[this.component.name] = !currentlyCollapsed;
14
+ localStorage.setItem(this.storageKey, JSON.stringify(this.collapseStates));
20
15
  }
21
16
 
22
17
  preferenceStoredAsCollapsed() {
23
- return this.collapsePreferences.includes(this.component.name);
18
+ if (this.component.name in this.collapseStates) {
19
+ return this.collapseStates[this.component.name];
20
+ }
21
+ return this.defaultCollapsed;
24
22
  }
25
23
 
26
24
  renderCollapseIcon() {
@@ -28,10 +26,10 @@ export default class CollapsePreferences {
28
26
  }
29
27
 
30
28
  minusIcon() {
31
- return <i className="fa fa-minus-square-o"></i>;
29
+ return <i className="print:!hidden fa-regular fa-square-minus"></i>;
32
30
  }
33
31
 
34
32
  plusIcon() {
35
- return <i className="fa fa-plus-square-o"></i>;
33
+ return <i className="print:!hidden fa-regular fa-square-plus"></i>;
36
34
  }
37
- }
35
+ }
@@ -76,7 +76,7 @@ export class Databases extends Component {
76
76
 
77
77
  // Toggle button.
78
78
  var toggleState = '[Select all]';
79
- var toggleClass = 'px-2 text-sm';
79
+ var toggleClass = 'px-2 text-base md:text-lg';
80
80
  var toggleShown = this.databases(category).length > 1;
81
81
  var toggleDisabled = this.state.type && this.state.type !== category;
82
82
  if (toggleShown && toggleDisabled) {
@@ -94,7 +94,7 @@ export class Databases extends Component {
94
94
  <div className={columnClass} key={'DB_' + category}>
95
95
  <div>
96
96
  <div className="border-b border-seqorange mb-2">
97
- <h4 style={{ display: 'inline' }} className="font-medium">
97
+ <h4 className="font-medium inline text-base md:text-lg">
98
98
  {panelTitle}
99
99
  </h4>
100
100
  <button
@@ -113,7 +113,7 @@ export class Databases extends Component {
113
113
  this.databases(category),
114
114
  _.bind(function (database, index) {
115
115
  return (
116
- <li key={'DB_' + category + index}>
116
+ <li key={'DB_' + category + index} className="text-base md:text-lg leading-tight md:leading-tight">
117
117
  {this.renderDatabase(database)}
118
118
  </li>
119
119
  );
@@ -152,6 +152,7 @@ export class Databases extends Component {
152
152
  data-type={database.type}
153
153
  disabled={isDisabled}
154
154
  checked={isChecked}
155
+ className="checkbox-database"
155
156
  onChange={_.bind(function () {
156
157
  this.handleDatabaseSelectionClick(database);
157
158
  }, this)}
@@ -16,29 +16,37 @@ export default class extends Databases {
16
16
  var tree_id = '#' + category + '_database_tree';
17
17
  // hack that is needed to sync the selected tree db with the hidden main db
18
18
  window.jstree_node_change_timeout = null;
19
+ window.jstree_rapid_timeout = null
20
+ const _this = this;
19
21
 
20
- // when a tree database gets selected
21
- $(tree_id).on('select_node.jstree deselect_node.jstree', function (e, data) {
22
- if (window.jstree_node_change_timeout) {
23
- clearTimeout(window.jstree_node_change_timeout);
24
- window.jstree_node_change_timeout = null;
25
- }
22
+ $(tree_id).on('select_node.jstree deselect_node.jstree', function (_, _data) {
23
+ if (window.jstree_node_change_timeout) clearTimeout(window.jstree_node_change_timeout);
24
+ if (window.jstree_rapid_timeout) clearTimeout(window.jstree_rapid_timeout);
25
+
26
+ // Set a timeout to handle jsTree node changes with some delay to avoid rapid re-triggering
27
+ window.jstree_rapid_timeout = setTimeout(function () {
28
+ // Set another timeout to handle the retrieval and processing of selected nodes
29
+ window.jstree_node_change_timeout = setTimeout(function () {
30
+ // Filter out the current category to get a list of other categories
31
+ const otherCategories = _this.categories().filter(item => item !== category);
32
+
33
+ // Uncheck all nodes in the trees of the other categories
34
+ otherCategories.forEach((value) => $(`#${value}_database_tree`).jstree('uncheck_all'));
35
+ // Get all selected nodes from the current tree.
36
+ // Note: This will also include folders. To filter only database nodes, ensure the ID length is 32.
37
+ // These IDs correspond to specific elements in the hidden main form.
38
+ const selected = $(tree_id).jstree('get_selected').filter(selected => selected.length === 32);
39
+
40
+ // Find database entries in the current category whose IDs match the selected nodes
41
+ const foundDatabases = _this.databases(category).filter(db => selected.includes(db.id));
26
42
 
27
- window.jstree_node_change_timeout = setTimeout(function () {
28
- // uncheck all input
29
- $('div#database_list input[type="checkbox"]:checked').click();
30
- setTimeout(function () {
31
- // get all selected tree dbs. Also includes folders. Therefore, the id must have a length of 32
32
- // this id is used to find the corresponding element from the hidden main form
33
- var selected = $(tree_id).jstree('get_selected').filter(selected => selected.length == 32);
34
- $.each(selected, function (index, value) {
35
- // select hidden element to trigger original sequenceserver behavior, like blast algorithm, ...
36
- $('input[value="' + value + '"]').click();
37
- });
38
- }, 100);
39
- }, 100);
43
+ // Update the selected databases in the view model with the found databases
44
+ _this.selectDatabases(foundDatabases);
45
+ }, 100); // Delay to ensure jsTree updates are processed
46
+ }, 100); // Delay to prevent rapid changes causing multiple triggers
40
47
  });
41
48
 
49
+
42
50
  $(tree_id).jstree({
43
51
  'core': {
44
52
  'data': this.props.tree[category]
@@ -50,6 +58,10 @@ export default class extends Databases {
50
58
  });
51
59
  }
52
60
 
61
+ selectDatabases(databases) {
62
+ this.setState({ currentlySelectedDatabases: databases });
63
+ }
64
+
53
65
  handleTreeSearch(category, tree_id, search_id) {
54
66
  var search_for = $('#' + search_id).val();
55
67
  $('#' + tree_id).jstree(true).search(search_for);
@@ -59,15 +71,18 @@ export default class extends Databases {
59
71
  // Panel name and column width.
60
72
  var panelTitle = category[0].toUpperCase() +
61
73
  category.substring(1).toLowerCase() + ' databases';
62
- var columnClass = this.categories().length === 1 ? 'col-md-12' :
63
- 'col-md-6';
74
+ var columnClass = this.categories().length === 1 ? 'col-span-2' : '';
64
75
 
65
76
  // Toggle button.
66
77
  var toggleState = '[Select all]';
67
- var toggleClass = 'btn-link';
78
+ var toggleClass = 'px-2 text-base md:text-lg hidden';
68
79
  var toggleShown = this.databases(category).length > 1;
69
80
  var toggleDisabled = this.state.type && this.state.type !== category;
70
- if (toggleShown && toggleDisabled) toggleClass += ' disabled';
81
+ if (toggleShown && toggleDisabled) {
82
+ toggleClass += ' text-gray-400';
83
+ } else {
84
+ toggleClass += ' text-seqblue';
85
+ }
71
86
  if (!toggleShown) toggleClass += ' hidden';
72
87
  if (this.nselected() === this.databases(category).length) {
73
88
  toggleState = '[Deselect all]';
@@ -76,22 +91,28 @@ export default class extends Databases {
76
91
  // JSX.
77
92
  return (
78
93
  <div className={columnClass} key={'DB_' + category}>
79
- <div className='panel panel-default' id='database_list'>
80
- <div className='panel-heading'>
81
- <h4 style={{ display: 'inline' }}>{panelTitle}</h4> &nbsp;&nbsp;
94
+ <div>
95
+ <div className="border-b border-seqorange mb-2" id="database_list">
96
+ <h4 className="font-medium text-base md:text-lg">{panelTitle}</h4>
82
97
  {
83
98
  this.renderDatabaseSearch(category)
84
99
  }
85
- <button type='button' className={toggleClass} disabled={toggleDisabled} style={{ display: 'none' }}
86
- onClick={function () { this.handleToggle(toggleState, category); }.bind(this)}>
100
+ <button
101
+ type="button"
102
+ className={toggleClass}
103
+ disabled={toggleDisabled}
104
+ onClick={function () {
105
+ this.handleToggle(toggleState, category);
106
+ }.bind(this)}
107
+ >
87
108
  {toggleState}
88
109
  </button>
89
110
  </div>
90
- <ul className={'list-group databases ' + category} style={{ display: 'none' }}>
111
+ <ul className={'databases hidden text-lg ' + category}>
91
112
  {
92
113
  _.map(this.databases(category), _.bind(function (database, index) {
93
114
  return (
94
- <li className='list-group-item' key={'DB_' + category + index}>
115
+ <li key={'DB_' + category + index}>
95
116
  {this.renderDatabase(database)}
96
117
  </li>
97
118
  );
@@ -2,7 +2,7 @@ import React, { createRef } from 'react';
2
2
 
3
3
  /**
4
4
  * Takes errorData object with title, message, and more_info keys as props. The
5
- * component displays a bootstrap modal when mounted. errorData.title is used
5
+ * component displays a tailwind modal when mounted. errorData.title is used
6
6
  * to set modal title. errorData.message is inserted as HTML text in modal
7
7
  * body. And errorData.more_info is shown using a pre tag in modal body.
8
8
  *
@@ -13,46 +13,59 @@ export default class ErrorModal extends React.Component {
13
13
 
14
14
  constructor(props) {
15
15
  super(props);
16
- this.state = { errorData: {} };
17
- this.modal = createRef();
16
+ this.state = {
17
+ errorData: {},
18
+ isModalVisible: false
19
+ };
20
+ this.modalRef = createRef();
18
21
  }
19
22
 
20
- // HTML for Bootstrap modal.
21
23
  render() {
24
+ const { isModalVisible, errorData } = this.state;
25
+
22
26
  return (
23
- <div id="error" ref={this.modal} className="modal fade"
24
- data-keyboard="false" data-backdrop="static">
25
- <div className="modal-dialog modal-lg">
26
- <div className="modal-content">
27
- <div className="modal-header">
28
- <h3>{this.state.errorData.title}</h3>
27
+ <div className="relative">
28
+ <dialog ref={this.modalRef} className="fixed p-4 w-full max-w-2xl bg-transparent focus:outline-none">
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
+ {errorData.title}
33
+ </h3>
34
+ <button className="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>
29
37
  </div>
30
-
31
- <div className="modal-body">
32
- <p dangerouslySetInnerHTML={{ __html: this.state.errorData.message}}></p>
33
-
38
+ <div className="pt-2 px-6 pb-6 mt-2 text-sm">
39
+ <p dangerouslySetInnerHTML={{ __html: errorData.message }} className="mb-4"></p>
34
40
  {
35
- this.state.errorData.more_info &&
36
- <pre className="pre-scrollable">
37
- {this.state.errorData.more_info}
38
- </pre>
41
+ errorData.more_info &&
42
+ <pre className="p-2 bg-slate-200 overflow-auto max-h-56">{errorData.more_info}</pre>
39
43
  }
40
44
  </div>
41
45
  </div>
42
- </div>
46
+ </dialog>
43
47
  </div>
44
48
  );
45
49
  }
50
+
46
51
  /**
47
52
  * Shows error viewer.
48
53
  */
49
- show (errorData, beforeShow) {
50
- this.setState({errorData: errorData});
54
+ show = (errorData, beforeShow) => {
55
+ this.setState({ errorData: errorData });
56
+
51
57
  // Caller can specify an amount of time to wait for before showing the
52
58
  // modal. This is helpful if the caller wants to finish some work
53
59
  // before showing error modal.
54
60
  setTimeout(() => {
55
- $(this.modal.current).modal('show');
61
+ this.modalRef.current?.showModal();
56
62
  }, beforeShow || 0);
57
63
  }
64
+
65
+ /**
66
+ * Hide dialogue.
67
+ */
68
+ hide = () => {
69
+ this.modalRef.current?.close();
70
+ }
58
71
  }
@@ -1,3 +1,5 @@
1
+ var d3 = require('d3');
2
+
1
3
  export function download_url(url, filename) {
2
4
  var a = d3.select('body').append('a')
3
5
  .attr('download', filename)