sequenceserver 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequenceserver +2 -2
  3. data/lib/sequenceserver/api_errors.rb +1 -1
  4. data/lib/sequenceserver/blast/job.rb +20 -3
  5. data/lib/sequenceserver/blast/report.rb +74 -86
  6. data/lib/sequenceserver/blast/tasks.rb +38 -0
  7. data/lib/sequenceserver/config.rb +54 -20
  8. data/lib/sequenceserver/makeblastdb.rb +16 -2
  9. data/lib/sequenceserver/report.rb +0 -6
  10. data/lib/sequenceserver/routes.rb +32 -21
  11. data/lib/sequenceserver/version.rb +1 -1
  12. data/lib/sequenceserver.rb +1 -1
  13. data/public/css/app.css +121 -0
  14. data/public/css/app.min.css +1 -0
  15. data/public/css/sequenceserver.css +0 -148
  16. data/public/css/sequenceserver.min.css +3 -3
  17. data/public/js/circos.js +2 -2
  18. data/public/js/collapse_preferences.js +37 -0
  19. data/public/js/databases.js +65 -37
  20. data/public/js/databases_tree.js +2 -1
  21. data/public/js/dnd.js +37 -50
  22. data/public/js/form.js +78 -50
  23. data/public/js/grapher.js +23 -37
  24. data/public/js/hits_overview.js +2 -2
  25. data/public/js/kablammo.js +2 -2
  26. data/public/js/length_distribution.js +3 -3
  27. data/public/js/null_plugins/grapher/histogram.js +25 -0
  28. data/public/js/null_plugins/options.js +3 -0
  29. data/public/js/null_plugins/query_stats.js +11 -0
  30. data/public/js/null_plugins/report_plugins.js +6 -1
  31. data/public/js/null_plugins/search_header_plugin.js +4 -0
  32. data/public/js/options.js +161 -56
  33. data/public/js/query.js +85 -59
  34. data/public/js/report.js +1 -1
  35. data/public/js/search.js +2 -0
  36. data/public/js/search_button.js +67 -56
  37. data/public/js/sidebar.js +1 -1
  38. data/public/js/tests/database.spec.js +5 -5
  39. data/public/js/tests/{advanced_parameters.spec.js → form.spec.js} +35 -1
  40. data/public/js/tests/mock_data/databases.json +5 -5
  41. data/public/js/tests/mocks/circos.js +6 -0
  42. data/public/js/tests/report.spec.js +4 -3
  43. data/public/js/tests/search_query.spec.js +5 -6
  44. data/public/sequenceserver-report.min.js +45 -23
  45. data/public/sequenceserver-search.min.js +57 -13
  46. data/public/sequenceserver_logo.webp +0 -0
  47. data/views/blastn_options.erb +66 -66
  48. data/views/blastp_options.erb +59 -59
  49. data/views/blastx_options.erb +68 -68
  50. data/views/layout.erb +60 -3
  51. data/views/search.erb +33 -38
  52. data/views/search_layout.erb +152 -0
  53. data/views/tblastn_options.erb +57 -57
  54. data/views/tblastx_options.erb +64 -64
  55. metadata +31 -22
  56. data/lib/sequenceserver/makeblastdb-modified-with-cache.rb +0 -345
  57. data/public/SequenceServer_logo.png +0 -0
data/public/js/circos.js CHANGED
@@ -2,7 +2,7 @@ import d3 from 'd3';
2
2
  import Circos from '../packages/circosJS@1.7.0';
3
3
  import _ from 'underscore';
4
4
 
5
- import Grapher from './grapher';
5
+ import Grapher from 'grapher';
6
6
  import * as Helpers from './visualisation_helpers';
7
7
  import Utils from './utils';
8
8
 
@@ -19,7 +19,7 @@ class Graph {
19
19
  return 'circos';
20
20
  }
21
21
 
22
- static collapseId(props) {
22
+ static graphId(props) {
23
23
  return 'circos-collapse';
24
24
  }
25
25
 
@@ -0,0 +1,37 @@
1
+ export default class CollapsePreferences {
2
+ constructor(component) {
3
+ this.component = component;
4
+ this.collapsePreferences = JSON.parse(localStorage.getItem('collapsePreferences')) || [];
5
+ }
6
+
7
+ toggleCollapse() {
8
+ let currentlyCollapsed = this.component.state.collapsed;
9
+
10
+ this.component.setState({ collapsed: !currentlyCollapsed });
11
+
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
+ }
20
+ }
21
+
22
+ preferenceStoredAsCollapsed() {
23
+ return this.collapsePreferences.includes(this.component.name);
24
+ }
25
+
26
+ renderCollapseIcon() {
27
+ return this.component.state.collapsed ? this.plusIcon() : this.minusIcon();
28
+ }
29
+
30
+ minusIcon() {
31
+ return <i className="fa fa-minus-square-o"></i>;
32
+ }
33
+
34
+ plusIcon() {
35
+ return <i className="fa fa-plus-square-o"></i>;
36
+ }
37
+ }
@@ -4,30 +4,43 @@ import _ from 'underscore';
4
4
  export class Databases extends Component {
5
5
  constructor(props) {
6
6
  super(props);
7
- this.state = { type: '' };
7
+ this.state = {
8
+ type: '',
9
+ currentlySelectedDatabases: [],
10
+ };
11
+
8
12
  this.preSelectedDbs = this.props.preSelectedDbs;
9
13
  this.databases = this.databases.bind(this);
10
14
  this.nselected = this.nselected.bind(this);
11
15
  this.categories = this.categories.bind(this);
12
- this.handleClick = this.handleClick.bind(this);
13
16
  this.handleToggle = this.handleToggle.bind(this);
14
17
  this.renderDatabases = this.renderDatabases.bind(this);
15
18
  this.renderDatabase = this.renderDatabase.bind(this);
16
19
  }
17
- componentDidUpdate() {
18
- if (this.databases() && this.databases().length === 1) {
19
- $('.databases').find('input').prop('checked', true);
20
- this.handleClick(this.databases()[0]);
20
+
21
+ componentDidUpdate(_prevProps, prevState) {
22
+ // If there's only one database, select it.
23
+ if (this.databases() && this.databases().length === 1 && this.state.currentlySelectedDatabases.length === 0) {
24
+ this.setState({currentlySelectedDatabases: this.databases()});
21
25
  }
22
26
 
23
- if (this.preSelectedDbs) {
24
- var selectors = this.preSelectedDbs.map((db) => `input[value=${db.id}]`);
25
- $(selectors.join(',')).prop('checked', true);
26
- this.handleClick(this.preSelectedDbs[0]);
27
+ if (this.preSelectedDbs && this.preSelectedDbs.length !== 0) {
28
+ this.setState({currentlySelectedDatabases: this.preSelectedDbs});
27
29
  this.preSelectedDbs = null;
28
30
  }
29
- this.props.onDatabaseTypeChanged(this.state.type);
31
+ const type = this.state.currentlySelectedDatabases[0] ? this.state.currentlySelectedDatabases[0].type : '';
32
+ if (type != this.state.type) {
33
+ this.setState({ type: type });
34
+ this.props.onDatabaseTypeChanged(type);
35
+ }
36
+
37
+ if (prevState.currentlySelectedDatabases !== this.state.currentlySelectedDatabases) {
38
+ // Call the prop function with the new state
39
+ this.props.onDatabaseSelectionChanged(this.state.currentlySelectedDatabases);
40
+ }
41
+
30
42
  }
43
+
31
44
  databases(category) {
32
45
  var databases = this.props.databases;
33
46
  if (category) {
@@ -38,40 +51,39 @@ export class Databases extends Component {
38
51
  }
39
52
 
40
53
  nselected() {
41
- return $('input[name="databases[]"]:checked').length;
54
+ return this.state.currentlySelectedDatabases.length;
42
55
  }
43
56
 
44
57
  categories() {
45
58
  return _.uniq(_.map(this.props.databases, _.iteratee('type'))).sort();
46
59
  }
47
60
 
48
- handleClick(database) {
49
- var type = this.nselected() ? database.type : '';
50
- if (type != this.state.type) this.setState({ type: type });
51
- }
52
-
53
61
  handleToggle(toggleState, type) {
54
62
  switch (toggleState) {
55
63
  case '[Select all]':
56
- $(`.${type} .database input:not(:checked)`).click();
64
+ this.setState({ currentlySelectedDatabases: this.databases(type) });
57
65
  break;
58
66
  case '[Deselect all]':
59
- $(`.${type} .database input:checked`).click();
67
+ this.setState({ currentlySelectedDatabases: [] });
60
68
  break;
61
69
  }
62
- this.forceUpdate();
63
70
  }
71
+
64
72
  renderDatabases(category) {
65
73
  // Panel name and column width.
66
74
  var panelTitle = category[0].toUpperCase() + category.substring(1).toLowerCase() + ' databases';
67
- var columnClass = this.categories().length === 1 ? 'col-md-12' : 'col-md-6';
75
+ var columnClass = this.categories().length === 1 ? 'col-span-2' : '';
68
76
 
69
77
  // Toggle button.
70
78
  var toggleState = '[Select all]';
71
- var toggleClass = 'btn-link';
79
+ var toggleClass = 'px-2 text-sm';
72
80
  var toggleShown = this.databases(category).length > 1;
73
81
  var toggleDisabled = this.state.type && this.state.type !== category;
74
- if (toggleShown && toggleDisabled) toggleClass += ' disabled';
82
+ if (toggleShown && toggleDisabled) {
83
+ toggleClass += ' text-gray-400';
84
+ } else {
85
+ toggleClass += ' text-seqblue';
86
+ }
75
87
  if (!toggleShown) toggleClass += ' hidden';
76
88
  if (this.nselected() === this.databases(category).length) {
77
89
  toggleState = '[Deselect all]';
@@ -80,9 +92,11 @@ export class Databases extends Component {
80
92
  // JSX.
81
93
  return (
82
94
  <div className={columnClass} key={'DB_' + category}>
83
- <div className="panel panel-default">
84
- <div className="panel-heading">
85
- <h4 style={{ display: 'inline' }}>{panelTitle}</h4> &nbsp;&nbsp;
95
+ <div>
96
+ <div className="border-b border-seqorange mb-2">
97
+ <h4 style={{ display: 'inline' }} className="font-medium">
98
+ {panelTitle}
99
+ </h4>
86
100
  <button
87
101
  type="button"
88
102
  className={toggleClass}
@@ -94,15 +108,12 @@ export class Databases extends Component {
94
108
  {toggleState}
95
109
  </button>
96
110
  </div>
97
- <ul className={'list-group databases ' + category}>
111
+ <ul className={'databases ' + category}>
98
112
  {_.map(
99
113
  this.databases(category),
100
114
  _.bind(function (database, index) {
101
115
  return (
102
- <li
103
- className="list-group-item"
104
- key={'DB_' + category + index}
105
- >
116
+ <li key={'DB_' + category + index}>
106
117
  {this.renderDatabase(database)}
107
118
  </li>
108
119
  );
@@ -114,20 +125,37 @@ export class Databases extends Component {
114
125
  );
115
126
  }
116
127
 
128
+ handleDatabaseSelectionClick(database) {
129
+ const isSelected = this.state.currentlySelectedDatabases.some(db => db.id === database.id);
130
+
131
+ if (isSelected) {
132
+ this.setState(prevState => ({
133
+ currentlySelectedDatabases: prevState.currentlySelectedDatabases.filter(db => db.id !== database.id)
134
+ }));
135
+ } else {
136
+ this.setState(prevState => ({
137
+ currentlySelectedDatabases: [...prevState.currentlySelectedDatabases, database]
138
+ }));
139
+ }
140
+ }
141
+
117
142
  renderDatabase(database) {
118
- var disabled = this.state.type && this.state.type !== database.type;
143
+ const isDisabled = this.state.type && this.state.type !== database.type;
144
+ const isChecked = this.state.currentlySelectedDatabases.some(db => db.id === database.id);
119
145
 
120
146
  return (
121
- <label className={(disabled && 'disabled database') || 'database'}>
147
+ <label className={(isDisabled && 'database text-gray-400') || 'database text-seqblue'}>
122
148
  <input
123
149
  type="checkbox"
124
150
  name="databases[]"
125
151
  value={database.id}
126
152
  data-type={database.type}
127
- disabled={disabled}
153
+ disabled={isDisabled}
154
+ checked={isChecked}
128
155
  onChange={_.bind(function () {
129
- this.handleClick(database);
156
+ this.handleDatabaseSelectionClick(database);
130
157
  }, this)}
158
+
131
159
  />
132
160
  {' ' + (database.title || database.name)}
133
161
  </label>
@@ -136,9 +164,9 @@ export class Databases extends Component {
136
164
 
137
165
  render() {
138
166
  return (
139
- <div className="form-group databases-container">
167
+ <div className="my-6 grid md:grid-cols-2 gap-4">
140
168
  {_.map(this.categories(), this.renderDatabases)}
141
169
  </div>
142
170
  );
143
171
  }
144
- }
172
+ }
@@ -102,6 +102,7 @@ export default class extends Databases {
102
102
  {
103
103
  this.renderDatabaseTree(category)
104
104
  }
105
+ <link rel="stylesheet" media="screen,print" type="text/css" href="vendor/github/vakata/jstree@3.3.8/dist/themes/default/style.min.css"/>
105
106
  </div>
106
107
  );
107
108
  }
@@ -111,7 +112,7 @@ export default class extends Databases {
111
112
  var search_id = tree_id + '_search';
112
113
 
113
114
  return (
114
- <input type='text' id={search_id} class='input'
115
+ <input type='text' id={search_id} className='border rounded px-1' placeholder='Search...'
115
116
  onKeyUp=
116
117
  {
117
118
  _.bind(function () {
data/public/js/dnd.js CHANGED
@@ -102,59 +102,46 @@ export class DnD extends Component {
102
102
  render() {
103
103
  return (
104
104
  <div
105
- className="dnd-overlay"
105
+ className="dnd-overlay absolute left-0 top-0 w-full h-full bg-gray-200 bg-opacity-75 z-40"
106
106
  style={{ display: 'none' }}>
107
- <div
108
- className="container dnd-overlay-container">
107
+ <div className="flex flex-col space-y-4 h-full items-center justify-center dnd-overlay-container text-2xl">
108
+ <p
109
+ className="dnd-overlay-drop flex items-center space-x-4"
110
+ style={{ display: 'none' }}>
111
+ <i className="fa fa-2x fa-file"></i>
112
+ Drop query sequence file here
113
+ </p>
114
+ <p
115
+ className="dnd-overlay-overwrite flex items-center space-x-4"
116
+ style={{ display: 'none' }}>
117
+ <i className="fa fa-2x fa-file"></i>
118
+ <span className="text-red-800">Overwrite</span>&nbps;query sequence file
119
+ </p>
120
+
109
121
  <div
110
- className="row">
122
+ className="dnd-errors text-red-800">
123
+ <div
124
+ className="dnd-error row"
125
+ id="dnd-multi-notification"
126
+ style={{ display: 'none' }}>
127
+
128
+ One file at a time please.
129
+ </div>
130
+
111
131
  <div
112
- className="col-md-offset-2 col-md-10">
113
- <p
114
- className="dnd-overlay-drop"
115
- style={{ display: 'none' }}>
116
- <i className="fa fa-2x fa-file-o"></i>
117
- Drop query sequence file here
118
- </p>
119
- <p
120
- className="dnd-overlay-overwrite"
121
- style={{ display: 'none' }}>
122
- <i className="fa fa-2x fa-file-o"></i>
123
- <span style={{ color: 'red' }}>Overwrite</span> query sequence file
124
- </p>
125
-
126
- <div
127
- className="dnd-errors">
128
- <div
129
- className="dnd-error row"
130
- id="dnd-multi-notification"
131
- style={{ display: 'none' }}>
132
- <div
133
- className="col-md-6 col-md-offset-3">
134
- One file at a time please.
135
- </div>
136
- </div>
137
-
138
- <div
139
- className="dnd-error row"
140
- id="dnd-large-file-notification"
141
- style={{ display: 'none' }}>
142
- <div
143
- className="col-md-6 col-md-offset-3">
144
- Too big a file. Can only do less than 250 MB. &gt;_&lt;
145
- </div>
146
- </div>
147
-
148
- <div
149
- className="dnd-error row"
150
- id="dnd-format-notification"
151
- style={{ display: 'none' }}>
152
- <div
153
- className="col-md-6 col-md-offset-3">
154
- Only FASTA files please.
155
- </div>
156
- </div>
157
- </div>
132
+ className="dnd-error row"
133
+ id="dnd-large-file-notification"
134
+ style={{ display: 'none' }}>
135
+
136
+ Too big a file. Can only do less than 250 MB. &gt;_&lt;
137
+ </div>
138
+
139
+ <div
140
+ className="dnd-error row"
141
+ id="dnd-format-notification"
142
+ style={{ display: 'none' }}>
143
+
144
+ Only FASTA files please.
158
145
  </div>
159
146
  </div>
160
147
  </div>
data/public/js/form.js CHANGED
@@ -4,7 +4,8 @@ import { SearchQueryWidget } from './query';
4
4
  import DatabasesTree from './databases_tree';
5
5
  import { Databases } from './databases';
6
6
  import _ from 'underscore';
7
- import { Options } from './options';
7
+ import { Options } from 'options';
8
+ import QueryStats from 'query_stats';
8
9
 
9
10
  /**
10
11
  * Search form.
@@ -16,15 +17,24 @@ export class Form extends Component {
16
17
  constructor(props) {
17
18
  super(props);
18
19
  this.state = {
19
- databases: [], preDefinedOpts: {}, tree: {}
20
+ databases: [],
21
+ preSelectedDbs: [],
22
+ currentlySelectedDbs: [],
23
+ preDefinedOpts: {},
24
+ tree: {},
25
+ residuesInQuerySequence: 0,
26
+ blastMethod: ''
20
27
  };
21
28
  this.useTreeWidget = this.useTreeWidget.bind(this);
22
- this.determineBlastMethod = this.determineBlastMethod.bind(this);
29
+ this.determineBlastMethods = this.determineBlastMethods.bind(this);
23
30
  this.handleSequenceTypeChanged = this.handleSequenceTypeChanged.bind(this);
31
+ this.handleSequenceChanged = this.handleSequenceChanged.bind(this);
24
32
  this.handleDatabaseTypeChanged = this.handleDatabaseTypeChanged.bind(this);
33
+ this.handleDatabaseSelectionChanged = this.handleDatabaseSelectionChanged.bind(this);
25
34
  this.handleAlgoChanged = this.handleAlgoChanged.bind(this);
26
35
  this.handleFormSubmission = this.handleFormSubmission.bind(this);
27
36
  this.formRef = createRef();
37
+ this.setButtonState = this.setButtonState.bind(this);
28
38
  }
29
39
 
30
40
  componentDidMount() {
@@ -47,7 +57,8 @@ export class Form extends Component {
47
57
  tree: data['tree'],
48
58
  databases: data['database'],
49
59
  preSelectedDbs: data['preSelectedDbs'],
50
- preDefinedOpts: data['options']
60
+ preDefinedOpts: data['options'],
61
+ blastTaskMap: data['blastTaskMap']
51
62
  });
52
63
 
53
64
  /* Pre-populate the form with server sent query sequences
@@ -103,7 +114,7 @@ export class Form extends Component {
103
114
  });
104
115
  }
105
116
 
106
- determineBlastMethod() {
117
+ determineBlastMethods() {
107
118
  var database_type = this.databaseType;
108
119
  var sequence_type = this.sequenceType;
109
120
 
@@ -138,76 +149,93 @@ export class Form extends Component {
138
149
  return [];
139
150
  }
140
151
 
152
+ handleSequenceChanged(residuesInQuerySequence) {
153
+ if(residuesInQuerySequence !== this.state.residuesInQuerySequence)
154
+ this.setState({ residuesInQuerySequence: residuesInQuerySequence});
155
+ }
156
+
141
157
  handleSequenceTypeChanged(type) {
142
158
  this.sequenceType = type;
143
- this.refs.button.setState({
144
- hasQuery: !this.refs.query.isEmpty(),
145
- hasDatabases: !!this.databaseType,
146
- methods: this.determineBlastMethod()
147
- });
159
+ this.setButtonState();
148
160
  }
149
161
 
150
162
  handleDatabaseTypeChanged(type) {
151
163
  this.databaseType = type;
164
+ this.setButtonState();
165
+ }
166
+
167
+ setButtonState() {
152
168
  this.refs.button.setState({
153
169
  hasQuery: !this.refs.query.isEmpty(),
154
170
  hasDatabases: !!this.databaseType,
155
- methods: this.determineBlastMethod()
171
+ methods: this.determineBlastMethods()
156
172
  });
157
173
  }
158
174
 
175
+ handleDatabaseSelectionChanged(selectedDbs) {
176
+ if (!_.isEqual(selectedDbs, this.state.currentlySelectedDbs))
177
+ this.setState({ currentlySelectedDbs: selectedDbs });
178
+ }
179
+
159
180
  handleAlgoChanged(algo) {
160
181
  if (algo in this.state.preDefinedOpts) {
161
- var preDefinedOpts = this.state.preDefinedOpts[algo];
162
- this.refs.opts.setState({
163
- method: algo,
164
- preOpts: preDefinedOpts,
165
- value: (preDefinedOpts['last search'] ||
166
- preDefinedOpts['default']).join(' ')
167
- });
182
+ this.setState({ blastMethod: algo });
168
183
  }
169
184
  else {
170
- this.refs.opts.setState({ preOpts: {}, value: '', method: '' });
185
+ this.setState({ blastMethod: ''});
171
186
  }
172
187
  }
173
188
 
189
+ residuesInSelectedDbs() {
190
+ return this.state.currentlySelectedDbs.reduce((sum, db) => sum + parseInt(db.ncharacters, 10), 0);
191
+ }
192
+
174
193
  render() {
175
194
  return (
176
- <div className="container">
195
+ <div>
177
196
  <div id="overlay" style={{ position: 'absolute', top: 0, left: 0, width: '100vw', height: '100vw', background: 'rgba(0, 0, 0, 0.2)', display: 'none', zIndex: 99 }} />
178
197
 
179
- <div className="notifications" id="notifications">
198
+ <div className="fixed top-0 left-0 w-full max-h-8 px-8" data-notifications id="notifications">
180
199
  <FastqNotification />
181
200
  <NucleotideNotification />
182
201
  <ProteinNotification />
183
202
  <MixedNotification />
184
203
  </div>
185
204
 
186
- <form id="blast" ref={this.formRef} onSubmit={this.handleFormSubmission} className="form-horizontal">
187
- <div className="form-group query-container">
188
- <SearchQueryWidget ref="query" onSequenceTypeChanged={this.handleSequenceTypeChanged} />
205
+ <form id="blast" ref={this.formRef} onSubmit={this.handleFormSubmission}>
206
+ <div className="px-4">
207
+ <SearchQueryWidget ref="query" onSequenceTypeChanged={this.handleSequenceTypeChanged} onSequenceChanged={this.handleSequenceChanged}/>
208
+
209
+ {this.useTreeWidget() ?
210
+ <DatabasesTree ref="databases"
211
+ databases={this.state.databases} tree={this.state.tree}
212
+ preSelectedDbs={this.state.preSelectedDbs}
213
+ onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
214
+ onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
215
+ :
216
+ <Databases ref="databases" databases={this.state.databases}
217
+ preSelectedDbs={this.state.preSelectedDbs}
218
+ onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
219
+ onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
220
+ }
221
+
222
+ <Options blastMethod={this.state.blastMethod} predefinedOptions={this.state.preDefinedOpts[this.state.blastMethod] || {}} blastTasks={(this.state.blastTaskMap || {})[this.state.blastMethod]} />
189
223
  </div>
190
- {this.useTreeWidget() ?
191
- <DatabasesTree ref="databases"
192
- databases={this.state.databases} tree={this.state.tree}
193
- preSelectedDbs={this.state.preSelectedDbs}
194
- onDatabaseTypeChanged={this.handleDatabaseTypeChanged} />
195
- :
196
- <Databases ref="databases" databases={this.state.databases}
197
- preSelectedDbs={this.state.preSelectedDbs}
198
- onDatabaseTypeChanged={this.handleDatabaseTypeChanged} />
199
- }
200
- <div className="form-group">
201
- <Options ref="opts" />
202
- <div className="col-md-2">
203
- <div className="form-group" style={{ 'textAlign': 'center', 'padding': '7px 0' }}>
204
- <label>
205
- <input type="checkbox" id="toggleNewTab" /> Open results in new tab
206
- </label>
207
- </div>
208
- </div>
224
+
225
+ <div className="py-6"></div> {/* add a spacer so that the sticky action bar does not hide any contents */}
226
+
227
+ <div className="pb-4 pt-2 px-4 sticky bottom-0 md:flex flex-row md:space-x-4 items-center justify-end bg-gradient-to-t to-gray-100/90 from-white/90">
228
+ <QueryStats
229
+ residuesInQuerySequence={this.state.residuesInQuerySequence} numberOfDatabasesSelected={this.state.currentlySelectedDbs.length} residuesInSelectedDbs={this.residuesInSelectedDbs()}
230
+ currentBlastMethod={this.state.blastMethod}
231
+ />
232
+
233
+ <label className="block my-4 md:my-2">
234
+ <input type="checkbox" id="toggleNewTab" /> Open results in new tab
235
+ </label>
209
236
  <SearchButton ref="button" onAlgoChanged={this.handleAlgoChanged} />
210
237
  </div>
238
+
211
239
  </form>
212
240
  </div>
213
241
  );
@@ -220,11 +248,11 @@ class ProteinNotification extends Component {
220
248
  render() {
221
249
  return (
222
250
  <div
223
- className="notification row"
251
+ data-role="notification"
224
252
  id="protein-sequence-notification"
225
253
  style={{ display: 'none' }}>
226
254
  <div
227
- className="alert-info col-md-6 col-md-offset-3">
255
+ className="bg-blue-100 border rounded border-blue-800 px-4 py-2 my-2">
228
256
  Detected: amino-acid sequence(s).
229
257
  </div>
230
258
  </div>
@@ -235,11 +263,11 @@ class ProteinNotification extends Component {
235
263
  class NucleotideNotification extends Component {
236
264
  render() {
237
265
  return (<div
238
- className="notification row"
266
+ data-role="notification"
239
267
  id="nucleotide-sequence-notification"
240
268
  style={{ display: 'none' }}>
241
269
  <div
242
- className="alert-info col-md-6 col-md-offset-3">
270
+ className="bg-blue-100 border rounded border-blue-800 px-4 py-2 my-2">
243
271
  Detected: nucleotide sequence(s).
244
272
  </div>
245
273
  </div>
@@ -250,11 +278,11 @@ class NucleotideNotification extends Component {
250
278
  class FastqNotification extends Component {
251
279
  render() {
252
280
  return (<div
253
- className="notification row"
281
+ data-role="notification"
254
282
  id="fastq-sequence-notification"
255
283
  style={{ display: 'none' }}>
256
284
  <div
257
- className="alert-info col-md-6 col-md-offset-3">
285
+ className="bg-blue-100 border rounded border-blue-800 px-4 py-2 my-2">
258
286
  Detected FASTQ and automatically converted to FASTA.
259
287
  </div>
260
288
  </div>
@@ -266,7 +294,7 @@ class MixedNotification extends Component {
266
294
  render() {
267
295
  return (
268
296
  <div
269
- className="notification row"
297
+ data-role="notification"
270
298
  id="mixed-sequence-notification"
271
299
  style={{ display: 'none' }}>
272
300
  <div