Skip to content

Commit 859e320

Browse files
committed
Refresh options UI
- reduce reliance on jQuery - enable customizing options in SequenceServer Cloud as a plugin - make predefined options (when available) more prominent - use radiobuttons for predefined options instead of a hover-based dropdown - make advanced options help modal prompt more prominent - Make the CTA sticky at the bottom so that it's always visible. Useful for instances with a lot of databases and predefined options, also smaller screens
1 parent f74c247 commit 859e320

16 files changed

Lines changed: 1888 additions & 1335 deletions

babel.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ module.exports = {
77
'@babel/plugin-proposal-class-properties',
88
'@babel/plugin-syntax-dynamic-import',
99
],
10-
};
10+
};

jest.config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ module.exports = {
4444
'circos$': '<rootDir>/public/js/tests/mocks/circos.js',
4545
'grapher': '<rootDir>/public/js/grapher.js',
4646
'histogram': '<rootDir>/public/js/null_plugins/grapher/histogram.js',
47-
'query_stats': '<rootDir>/public/js/null_plugins/query_stats.js'
47+
'query_stats': '<rootDir>/public/js/null_plugins/query_stats.js',
48+
'^options$': '<rootDir>/public/js/options.js'
4849
},
4950
watchPlugins: [
5051
'jest-watch-typeahead/filename',
5152
'jest-watch-typeahead/testname'
5253
],
53-
resetMocks: true
54+
resetMocks: true,
5455
};

jest_scripts/babelTransform.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const hasJsxRuntime = (() => {
1212
return false;
1313
}
1414
})();
15+
16+
1517
module.exports = babelJest.createTransformer({
1618
presets: [
1719
[

package-lock.json

Lines changed: 1627 additions & 1193 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@
4141
"@babel/node": "^7.16.8",
4242
"@babel/plugin-proposal-class-properties": "^7.16.7",
4343
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
44-
"@babel/preset-env": "^7.16.11",
45-
"@babel/preset-react": "^7.16.7",
44+
"@babel/preset-env": "^7.24.6",
45+
"@babel/preset-react": "^7.24.6",
4646
"@testing-library/jest-dom": "^5.16.4",
4747
"@testing-library/react": "^13.1.1",
4848
"@testing-library/user-event": "^14.1.1",
49-
"babel-jest": "^28.0.1",
49+
"babel-jest": "^28.1.3",
5050
"babel-loader": "^8.2.5",
5151
"clean-css": "^3.4.1",
5252
"eslint": "^6.5.1",

public/css/app.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/js/form.js

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SearchQueryWidget } from './query';
44
import DatabasesTree from './databases_tree';
55
import { Databases } from './databases';
66
import _ from 'underscore';
7-
import { Options } from './options';
7+
import { Options } from 'options';
88
import QueryStats from 'query_stats';
99

1010
/**
@@ -57,7 +57,8 @@ export class Form extends Component {
5757
tree: data['tree'],
5858
databases: data['database'],
5959
preSelectedDbs: data['preSelectedDbs'],
60-
preDefinedOpts: data['options']
60+
preDefinedOpts: data['options'],
61+
blastTaskMap: data['blastTaskMap']
6162
});
6263

6364
/* Pre-populate the form with server sent query sequences
@@ -178,18 +179,10 @@ export class Form extends Component {
178179

179180
handleAlgoChanged(algo) {
180181
if (algo in this.state.preDefinedOpts) {
181-
var preDefinedOpts = this.state.preDefinedOpts[algo];
182-
this.refs.opts.setState({
183-
method: algo,
184-
preOpts: preDefinedOpts,
185-
value: (preDefinedOpts['last search'] ||
186-
preDefinedOpts['default']).join(' ')
187-
});
188182
this.setState({ blastMethod: algo });
189183
}
190184
else {
191-
this.refs.opts.setState({ preOpts: {}, value: '', method: '' });
192-
this.setState({ blastMethod: '' });
185+
this.setState({ blastMethod: ''});
193186
}
194187
}
195188

@@ -210,32 +203,39 @@ export class Form extends Component {
210203
</div>
211204

212205
<form id="blast" ref={this.formRef} onSubmit={this.handleFormSubmission}>
213-
<SearchQueryWidget ref="query" onSequenceTypeChanged={this.handleSequenceTypeChanged} onSequenceChanged={this.handleSequenceChanged} />
214-
215-
{this.useTreeWidget() ?
216-
<DatabasesTree ref="databases"
217-
databases={this.state.databases} tree={this.state.tree}
218-
preSelectedDbs={this.state.preSelectedDbs}
219-
onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
220-
onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
221-
:
222-
<Databases ref="databases" databases={this.state.databases}
223-
preSelectedDbs={this.state.preSelectedDbs}
224-
onDatabaseTypeChanged={this.handleDatabaseTypeChanged}
225-
onDatabaseSelectionChanged={this.handleDatabaseSelectionChanged} />
226-
}
227-
228-
<div className="md:flex flex-row md:space-x-4 items-center my-6">
229-
<Options ref="opts" />
230-
<label className="block my-4 md:my-0">
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]} />
223+
</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">
231234
<input type="checkbox" id="toggleNewTab" /> Open results in new tab
232235
</label>
233236
<SearchButton ref="button" onAlgoChanged={this.handleAlgoChanged} />
234237
</div>
235-
<QueryStats
236-
residuesInQuerySequence={this.state.residuesInQuerySequence} numberOfDatabasesSelected={this.state.currentlySelectedDbs.length} residuesInSelectedDbs={this.residuesInSelectedDbs()}
237-
currentBlastMethod={this.state.blastMethod}
238-
/>
238+
239239
</form>
240240
</div>
241241
);

public/js/null_plugins/options.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Options } from '../options';
2+
3+
export { Options };

public/js/options.js

Lines changed: 158 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,168 @@ import React, { Component } from 'react';
33
export class Options extends Component {
44
constructor(props) {
55
super(props);
6-
this.state = { preOpts: {}, value: '', method: '' };
7-
this.updateBox = this.updateBox.bind(this);
8-
this.optionsJSX = this.optionsJSX.bind(this);
9-
this.showAdvancedOptions = this.showAdvancedOptions.bind(this);
6+
7+
this.state = {
8+
textValue: '',
9+
objectValue: this.defaultObjectValue(),
10+
paramsMode: 'advanced'
11+
};
12+
13+
this.onTextValueChanged = this.onTextValueChanged.bind(this);
14+
this.optionsPresetsJSX = this.optionsPresetsJSX.bind(this);
15+
this.advancedParamsJSX = this.advancedParamsJSX.bind(this);
16+
this.showAdvancedOptionsHelp = this.showAdvancedOptionsHelp.bind(this);
17+
}
18+
19+
20+
defaultObjectValue() {
21+
return {
22+
max_target_seqs: '',
23+
evalue: '',
24+
task: ''
25+
};
26+
};
27+
28+
componentDidUpdate(prevProps) {
29+
if (prevProps.predefinedOptions !== this.props.predefinedOptions) {
30+
let defaultOptions = this.props.predefinedOptions.default || {attributes: []};
31+
let initialTextValue = (this.props.predefinedOptions['last search'] ||
32+
defaultOptions.attributes || []).join(' ').trim();
33+
let parsedOptions = this.parsedOptions(initialTextValue);
34+
this.setState({ textValue: initialTextValue, objectValue: parsedOptions});
35+
}
1036
}
1137

12-
updateBox(value) {
13-
this.setState({ value: value });
38+
onTextValueChanged(textValue) {
39+
let parsedOptions = this.parsedOptions(textValue.toString());
40+
41+
this.setState({
42+
textValue: textValue.toString(),
43+
objectValue: parsedOptions
44+
});
1445
}
1546

47+
parsedOptions(textValue) {
48+
const words = textValue.split(" ");
49+
let parsedOptions = this.defaultObjectValue();
50+
// Iterate through the words in steps of 2, treating each pair as an option and its potential value
51+
for (let i = 0; i < words.length; i += 2) {
52+
// Ensure there is a pair available
53+
if (words[i]) {
54+
if (words[i].startsWith("-")) {
55+
const optionName = words[i].substring(1).trim();
1656

17-
optionsJSX() {
18-
return <div id="advanced-params-dropdown" className="relative -ml-4 group">
19-
<button className="h-full border border-gray-300 rounded-r p-1 px-2 bg-white hover:bg-gray-200" type="button">
20-
<i className="fa fa-caret-down h-4 w-4"></i>
21-
<span className="sr-only">Advanced parameters</span>
22-
</button>
23-
<div className='z-20 group-hover:block hidden absolute top-full right-0 min-w-fit lg:min-w-[40rem]'>
24-
<ul className="font-mono my-1 border rounded divide-y">
25-
{
26-
Object.entries(this.state.preOpts).map(
27-
([key, value], index) => {
28-
value = value.join(' ');
29-
var bgClass = 'bg-white';
30-
if (value.trim() === this.state.value.trim())
31-
bgClass = 'bg-yellow-100';
32-
return <li key={index} className={`px-2 py-1 hover:bg-gray-200 cursor-pointer ${bgClass}`}
33-
onClick={() => this.updateBox(value)}>
34-
<strong>{key}:</strong>&nbsp;{value}
35-
</li>;
36-
}
37-
)
57+
if (words[i + 1]) {
58+
// Use the second word as the value for this option
59+
parsedOptions[optionName] = words[i + 1];
60+
} else {
61+
// No value found for this option, set it to null or a default value
62+
parsedOptions[optionName] = null;
3863
}
39-
</ul>
64+
}
65+
}
66+
}
67+
68+
return parsedOptions;
69+
}
70+
71+
optionsPresetsJSX() {
72+
return (
73+
<div id="options-presets" className="w-full">
74+
{ Object.keys(this.props.predefinedOptions).length > 1 && <>
75+
<h3 className="w-full font-medium border-b border-seqorange mb-2">Settings</h3>
76+
77+
<p className="text-sm">Choose a predefined setting or customize BLAST parameters.</p>
78+
{this.presetListJSX()}
79+
</>}
4080
</div>
41-
</div>;
81+
);
4282
}
43-
showAdvancedOptions(e) {
83+
84+
presetListJSX() {
85+
return (
86+
<ul className="text-sm my-1">
87+
{
88+
Object.entries(this.props.predefinedOptions).map(
89+
([key, config], index) => {
90+
let textValue = config.attributes.join(' ').trim();
91+
let description = config.description || textValue;
92+
93+
return (
94+
<label key={index} className={`block w-full px-2 py-1 hover:bg-gray-200 cursor-pointer`}>
95+
<input
96+
type="radio"
97+
name="predefinedOption"
98+
value={textValue}
99+
checked={textValue === this.state.textValue}
100+
onChange={() => this.onTextValueChanged(textValue)}
101+
/>
102+
<strong className="ml-2">{key}:</strong>&nbsp;{description}
103+
</label>
104+
);
105+
}
106+
)
107+
}
108+
</ul>
109+
)
110+
}
111+
112+
advancedParamsJSX() {
113+
if (this.state.paramsMode !== 'advanced') {
114+
return null;
115+
}
116+
117+
let classNames = 'flex-grow block px-4 py-1 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 text-base';
118+
119+
if (this.state.textValue) {
120+
classNames += ' bg-yellow-100';
121+
}
122+
123+
return(
124+
<div className="w-full">
125+
<div className="flex items-end">
126+
<label className="flex items-center" htmlFor="advanced">
127+
Advanced parameters
128+
</label>
129+
130+
{this.props.blastMethod &&
131+
<button
132+
className="text-seqblue ml-2"
133+
type="button"
134+
onClick={this.showAdvancedOptionsHelp}
135+
data-toggle="modal" data-target="#help">
136+
See available options
137+
<i className="fa fa-question-circle ml-1 w-3 h-4 fill-current"></i>
138+
</button>
139+
}
140+
141+
{!this.props.blastMethod &&
142+
<span className="text-gray-600 ml-2 text-sm hidden sm:block">
143+
Select databases and fill in the query to see options.
144+
</span>
145+
}
146+
</div>
147+
148+
<div className='flex-grow flex w-full'>
149+
<input type="text" className={classNames}
150+
onChange={e => this.onTextValueChanged(e.target.value)}
151+
id="advanced"
152+
name="advanced"
153+
value={this.state.textValue}
154+
placeholder="eg: -evalue 1.0e-5 -num_alignments 100"
155+
title="View, and enter advanced parameters."
156+
/>
157+
</div>
158+
<div className="text-sm text-gray-600 mt-2">
159+
Options as they would appear in a command line when calling BLAST eg: <i>-evalue 1.0e-5 -num_alignments 100</i>
160+
</div>
161+
</div>
162+
)
163+
}
164+
165+
showAdvancedOptionsHelp(e) {
44166
const ids = ['blastn', 'tblastn', 'blastp', 'blastx', 'tblastx'];
45-
const method = this.state.method.toLowerCase();
167+
const method = this.props.blastMethod.toLowerCase();
46168
// hide options for other algorithms and only show for selected algorithm
47169
for (const id of ids) {
48170
if (id === method) {
@@ -53,33 +175,13 @@ export class Options extends Component {
53175
}
54176
document.querySelector('[data-help-modal]').classList.remove('hidden')
55177
}
178+
56179
render() {
57-
var classNames = 'flex-grow block px-4 py-1 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 text-base';
58-
if (this.state.value.trim()) {
59-
classNames += ' bg-yellow-100';
60-
}
61180
return (
62-
<div className="flex-grow flex flex-col sm:flex-row items-start sm:items-center">
63-
<label className="flex items-center mx-2" htmlFor="advanced">
64-
Advanced parameters:
65-
{/* only show link to advanced parameters if blast method is known */}
66-
{this.state.method && <sup className="mx-1 text-seqblue">
67-
<a href=''
68-
onClick={this.showAdvancedOptions}
69-
data-toggle="modal" data-target="#help">
70-
<i className="fa fa-question-circle w-3 h-4 fill-current"></i>
71-
</a>
72-
</sup>}
73-
</label>
74-
<div className='flex-grow flex w-full sm:w-auto'>
75-
<input type="text" className={classNames}
76-
onChange={e => this.updateBox(e.target.value)}
77-
id="advanced" name="advanced" value={this.state.value}
78-
placeholder="eg: -evalue 1.0e-5 -num_alignments 100"
79-
title="View, and enter advanced parameters."
80-
/>
81-
{Object.keys(this.state.preOpts).length > 1 && this.optionsJSX()}
82-
</div>
181+
<div className="flex-grow flex flex-col items-start sm:items-center space-y-4">
182+
{this.optionsPresetsJSX()}
183+
184+
{this.advancedParamsJSX()}
83185
</div>
84186
);
85187
}

0 commit comments

Comments
 (0)