sequenceserver 3.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sequenceserver might be problematic. Click here for more details.

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