sequenceserver 2.1.0 → 2.2.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cf703fae66ad96d285ed0556c387dabb0d69be120db5bdd3c3f9f8a03d3a814
4
- data.tar.gz: 503bb0b69cbf773ab536d99f6782f4fff19f4872c49e6377d49a9f7820861c3b
3
+ metadata.gz: 61b15f95cdc065739de70f2ba94d3022e6beec789dd9c64199ee1be3a2459c63
4
+ data.tar.gz: d6c108bd39cb8c6a832787d2cbaa52b2c5a3653c0b08b289cb149b7221a9870c
5
5
  SHA512:
6
- metadata.gz: 1896b2afddf6033c98747f7eba1dabbe15b270940bd19760ebef4ed75af267760d45d40215663173ccb2f964472b458179a459316b86ef72bc26aa75d62faad0
7
- data.tar.gz: 4ac1d9a5a205d299af4b1467521a9bc539e3833d0a571cfa90b490b19df958fc0f7ee2e1341e3d8d5e19fccbe2ee6874884fc7fbcbfbdc6abbe1e60df3c9a76e
6
+ metadata.gz: d4c2b1785980dd4e46a24a8a5a6d36652fb23f5389f9b7320cd9360f081cbad82b6dc6bfe735b5321738611094052631af0de635b77676587d703d466bd5f604
7
+ data.tar.gz: 235e35e8ba4331894a7ac9f107d950462d3b3d892ddafc6f58d3ba48fbe6a03cfc016ac64798921472e296a0cb5577245aca49cbb517d897094a501ec4483beb
data/bin/sequenceserver CHANGED
@@ -119,6 +119,11 @@ begin
119
119
  'Port to run SequenceServer on',
120
120
  argument: true
121
121
 
122
+ on 'o', 'optimistic=',
123
+ 'Optimistic mode does not perform database compatibility checks, which can be faster. ' \
124
+ 'Only use this if you format FASTAs with "sequenceserver -make-blast-databases"',
125
+ argument: true
126
+
122
127
  on 's', 'set',
123
128
  'Set configuration value in default or given config file'
124
129
 
@@ -296,7 +301,7 @@ begin
296
301
  unless response =~ /^[n]$/i
297
302
  puts
298
303
  puts 'Searching ...'
299
- if SequenceServer.makeblastdb.any_unformatted?
304
+ if SequenceServer.makeblastdb.any_to_format?
300
305
  formatted = SequenceServer.makeblastdb.format
301
306
  exit! if formatted.empty? && !set?
302
307
  redo unless set?
@@ -353,7 +358,7 @@ begin
353
358
  end
354
359
 
355
360
  if make_blast_databases?
356
- if SequenceServer.makeblastdb.scan
361
+ if SequenceServer.makeblastdb.any_to_format_or_reformat?
357
362
  puts
358
363
  puts <<~MSG
359
364
  SequenceServer has scanned your databases directory and will now offer
@@ -25,23 +25,32 @@ module SequenceServer
25
25
 
26
26
  attr_reader :format, :mime, :specifiers
27
27
 
28
- def file
29
- @file ||= File.join(job.dir, filename)
28
+ def filepath
29
+ @filepath ||= File.join(job.dir, filename)
30
+ end
31
+
32
+ def size
33
+ File.size(filepath)
30
34
  end
31
35
 
32
36
  def filename
33
37
  @filename ||= "sequenceserver-#{type}_report.#{mime}"
34
38
  end
35
39
 
40
+ def read_file
41
+ File.read(filepath)
42
+ end
43
+
36
44
  private
37
45
 
38
46
  attr_reader :job, :type
39
47
 
40
48
  def run
41
- return if File.exist?(file)
49
+ return if File.exist?(filepath)
50
+
42
51
  command = "blast_formatter -archive '#{job.stdout}'" \
43
52
  " -outfmt '#{format} #{specifiers}'"
44
- sys(command, path: config[:bin], dir: DOTDIR, stdout: file)
53
+ sys(command, path: config[:bin], dir: DOTDIR, stdout: filepath)
45
54
  rescue CommandFailed => e
46
55
  # Mostly we will never get here: empty archive file,
47
56
  # file permissions, broken BLAST binaries, etc. will
@@ -36,6 +36,8 @@ module SequenceServer
36
36
  attr_reader :querydb, :dbtype, :params
37
37
 
38
38
  def to_json(*_args)
39
+ generate
40
+
39
41
  %i[querydb program program_version params stats
40
42
  queries].inject({}) do |h, k|
41
43
  h[k] = send(k)
@@ -48,10 +50,18 @@ module SequenceServer
48
50
  non_parse_seqids: !!job.databases&.any?(&:non_parse_seqids?)).to_json
49
51
  end
50
52
 
51
- private
53
+ def xml_file_size
54
+ return File.size(job.imported_xml_file) if job.imported_xml_file
55
+
56
+ generate
57
+
58
+ xml_formatter.size
59
+ end
52
60
 
53
61
  # Generate report.
54
62
  def generate
63
+ return self if @_generated
64
+
55
65
  job.raise!
56
66
  xml_ir = nil
57
67
  tsv_ir = nil
@@ -63,14 +73,34 @@ module SequenceServer
63
73
  end
64
74
  end
65
75
  else
66
- xml_ir = parse_xml File.read(Formatter.run(job, 'xml').file)
67
- tsv_ir = parse_tsv File.read(Formatter.run(job, 'custom_tsv').file)
76
+ xml_ir = parse_xml(xml_formatter.read_file)
77
+ tsv_ir = parse_tsv(tsv_formatter.read_file)
68
78
  end
69
79
  extract_program_info xml_ir
70
80
  extract_db_info xml_ir
71
81
  extract_params xml_ir
72
82
  extract_stats xml_ir
73
83
  extract_queries xml_ir, tsv_ir
84
+
85
+ @_generated = true
86
+
87
+ self
88
+ end
89
+
90
+ def done?
91
+ return true if job.imported_xml_file
92
+
93
+ File.exist?(xml_formatter.filepath) && File.exist?(tsv_formatter.filepath)
94
+ end
95
+
96
+ private
97
+
98
+ def xml_formatter
99
+ @xml_formatter ||= Formatter.run(job, 'xml')
100
+ end
101
+
102
+ def tsv_formatter
103
+ @tsv_formatter ||= Formatter.run(job, 'custom_tsv')
74
104
  end
75
105
 
76
106
  # Make program name and program name + version available via `program`
@@ -131,7 +131,10 @@ module SequenceServer
131
131
  num_jobs: 1,
132
132
  job_lifetime: 43_200,
133
133
  # Set cloud_share_url to 'disabled' to disable the cloud sharing feature
134
- cloud_share_url: 'https://share.sequenceserver.com/api/v1/shared-job'
134
+ cloud_share_url: 'https://share.sequenceserver.com/api/v1/shared-job',
135
+ # Warn in the UI before rendering results larger than this value
136
+ large_result_warning_threshold: 250 * 1024 * 1024,
137
+ optimistic: false # Faster, but does not perform DB compatibility checks
135
138
  }
136
139
  end
137
140
  end
@@ -8,7 +8,8 @@ module SequenceServer
8
8
  # Example usage:
9
9
  #
10
10
  # makeblastdb = MAKEBLASTDB.new(database_dir)
11
- # makeblastdb.scan && makeblastdb.run
11
+ # makeblastdb.run # formats and re-formats databases in database_dir
12
+ # makeblastdb.formatted_fastas # lists formatted databases
12
13
  #
13
14
  class MAKEBLASTDB
14
15
  extend Forwardable
@@ -20,56 +21,17 @@ module SequenceServer
20
21
  end
21
22
 
22
23
  attr_reader :database_dir
23
- attr_reader :formatted_fastas
24
- attr_reader :fastas_to_format
25
- attr_reader :fastas_to_reformat
26
24
 
27
- # Scans the database directory to determine which FASTA files require
28
- # formatting or re-formatting.
29
- #
30
- # Returns `true` if there are files to (re-)format, `false` otherwise.
31
- def scan
32
- # We need to know the list of formatted FASTAs as reported by blastdbcmd
33
- # first. This is required to determine both unformatted FASTAs and those
34
- # that require reformatting.
35
- @formatted_fastas = []
36
- determine_formatted_fastas
37
-
38
- # Now determine FASTA files that are unformatted or require reformatting.
39
- @fastas_to_format = []
40
- determine_unformatted_fastas
41
- @fastas_to_reformat = []
42
- determine_fastas_to_reformat
43
-
44
- # Return true if there are files to be (re-)formatted or false otherwise.
45
- !@fastas_to_format.empty? || !@fastas_to_reformat.empty?
46
- end
47
-
48
- # Returns true if at least one database in database directory is formatted.
49
25
  def any_formatted?
50
- !@formatted_fastas.empty?
51
- end
52
-
53
- # Returns true if there is at least one unformatted FASTA in the databases
54
- # directory.
55
- def any_unformatted?
56
- !@fastas_to_format.empty?
26
+ formatted_fastas.any?
57
27
  end
58
28
 
59
- # Returns true if the databases directory contains one or more incompatible
60
- # databases.
61
- #
62
- # Note that it is okay to only use V4 databases or only V5 databases.
63
- # Incompatibility arises when they are mixed.
64
- def any_incompatible?
65
- return false if @formatted_fastas.all? { |ff| ff.v4? || ff.alias? }
66
- return false if @formatted_fastas.all? { |ff| ff.v5? || ff.alias? }
67
- true
29
+ def any_to_format_or_reformat?
30
+ any_to_format? || any_to_reformat?
68
31
  end
69
32
 
70
- # Runs makeblastdb on each file in `@fastas_to_format` and
71
- # `@fastas_to_reformat`. Will do nothing unless `#scan`
72
- # has been run before.
33
+ # Runs makeblastdb on each file in `fastas_to_format` and
34
+ # `fastas_to_reformat`.
73
35
  def run
74
36
  format
75
37
  reformat
@@ -80,8 +42,9 @@ module SequenceServer
80
42
  def format
81
43
  # Make the intent clear as well as ensure the program won't crash if we
82
44
  # accidentally call format before calling scan.
83
- return unless @fastas_to_format
84
- @fastas_to_format.select do |path, title, type|
45
+ return unless any_to_format?
46
+
47
+ fastas_to_format.select do |path, title, type|
85
48
  make_blast_database('format', path, title, type)
86
49
  end
87
50
  end
@@ -91,50 +54,75 @@ module SequenceServer
91
54
  def reformat
92
55
  # Make the intent clear as well as ensure the program won't crash if
93
56
  # we accidentally call reformat before calling scan.
94
- return unless @fastas_to_reformat
95
- @fastas_to_reformat.select do |path, title, type, non_parse_seqids|
57
+ return unless any_to_reformat?
58
+
59
+ fastas_to_reformat.select do |path, title, type, non_parse_seqids|
96
60
  make_blast_database('reformat', path, title, type, non_parse_seqids)
97
61
  end
98
62
  end
99
63
 
100
- private
101
-
102
64
  # Determines which FASTA files in the database directory are already
103
- # formatted. Adds to @formatted_fastas.
104
- def determine_formatted_fastas
65
+ # formatted.
66
+ def formatted_fastas
67
+ return @formatted_fastas if defined?(@formatted_fastas)
68
+
69
+ @formatted_fastas = []
70
+
105
71
  blastdbcmd.each_line do |line|
106
72
  path, *rest = line.chomp.split("\t")
107
73
  next if multipart_database_name?(path)
74
+
108
75
  rest << get_categories(path)
109
76
  @formatted_fastas << Database.new(path, *rest)
110
77
  end
78
+
79
+ @formatted_fastas
80
+ end
81
+
82
+ private
83
+
84
+ def any_to_format?
85
+ fastas_to_format.any?
86
+ end
87
+
88
+ def any_to_reformat?
89
+ fastas_to_reformat.any?
111
90
  end
112
91
 
113
92
  # Determines which FASTA files in the database directory require
114
- # reformatting. Adds to @fastas_to_format.
115
- def determine_fastas_to_reformat
116
- @formatted_fastas.each do |ff|
117
- if ff.v4? || ff.non_parse_seqids?
118
- @fastas_to_reformat << [ff.path, ff.title, ff.type, ff.non_parse_seqids?]
119
- end
93
+ # reformatting.
94
+ def fastas_to_reformat
95
+ return @fastas_to_reformat if defined?(@fastas_to_reformat)
96
+
97
+ @fastas_to_reformat = []
98
+ formatted_fastas.each do |ff|
99
+ @fastas_to_reformat << [ff.path, ff.title, ff.type, ff.non_parse_seqids?] if ff.v4? || ff.non_parse_seqids?
120
100
  end
101
+
102
+ @fastas_to_reformat
121
103
  end
122
104
 
123
105
  # Determines which FASTA files in the database directory are
124
- # unformatted. Adds to @fastas_to_format.
125
- def determine_unformatted_fastas
106
+ # unformatted.
107
+ def fastas_to_format
108
+ return @fastas_to_format if defined?(@fastas_to_format)
109
+
110
+ @fastas_to_format = []
111
+
126
112
  # Add a trailing slash to database_dir - Find.find doesn't work as
127
113
  # expected without the trailing slash if database_dir is a symlink
128
114
  # inside a docker container.
129
115
  Find.find(database_dir + '/') do |path|
130
116
  next if File.directory?(path)
131
117
  next unless probably_fasta?(path)
132
- next if @formatted_fastas.any? { |f| f[0] == path }
118
+ next if formatted_fastas.any? { |f| f[0] == path }
133
119
 
134
120
  @fastas_to_format << [path,
135
- make_db_title(path),
136
- guess_sequence_type_in_fasta(path)]
121
+ make_db_title(path),
122
+ guess_sequence_type_in_fasta(path)]
137
123
  end
124
+
125
+ @fastas_to_format
138
126
  end
139
127
 
140
128
  # Runs `blastdbcmd` to determine formatted FASTA files in the database
@@ -146,14 +134,16 @@ module SequenceServer
146
134
  out, err = sys(cmd, path: config[:bin])
147
135
  errpat = /BLAST Database error/
148
136
  fail BLAST_DATABASE_ERROR.new(cmd, err) if err.match(errpat)
149
- return out
137
+
138
+ out
150
139
  rescue CommandFailed => e
151
- fail BLAST_DATABASE_ERROR.new(cmd, e.stderr)
140
+ raise BLAST_DATABASE_ERROR.new(cmd, e.stderr)
152
141
  end
153
142
 
154
143
  # Create BLAST database, given FASTA file and sequence type in FASTA file.
155
144
  def make_blast_database(action, file, title, type, non_parse_seqids = false)
156
145
  return unless make_blast_database?(action, file, type)
146
+
157
147
  title = confirm_database_title(title)
158
148
  extract_fasta(file) unless File.exist?(file)
159
149
  taxonomy = taxid_map(file, non_parse_seqids) || taxid
@@ -188,9 +178,10 @@ module SequenceServer
188
178
  # using blastdbcmd.
189
179
  def taxid_map(db, non_parse_seqids)
190
180
  return if non_parse_seqids
181
+
191
182
  taxid_map = db.sub(/#{File.extname(db)}$/, '.taxid_map.txt')
192
- extract_taxid_map(db, taxid_map) if !File.exist?(taxid_map)
193
- "-taxid_map #{taxid_map}" if !File.zero?(taxid_map)
183
+ extract_taxid_map(db, taxid_map) unless File.exist?(taxid_map)
184
+ "-taxid_map #{taxid_map}" unless File.zero?(taxid_map)
194
185
  end
195
186
 
196
187
  # Get taxid from the user. Returns user input or 0.
@@ -211,10 +202,24 @@ module SequenceServer
211
202
  cmd = "makeblastdb -parse_seqids -hash_index -in '#{file}'" \
212
203
  " -dbtype #{type.to_s.slice(0, 4)} -title '#{title}'" \
213
204
  " #{taxonomy}"
214
- out, err = sys(cmd, path: config[:bin])
205
+
206
+ output = if File.directory?(file)
207
+ File.join(file, 'makeblastdb')
208
+ else
209
+ "#{file}.makeblastdb"
210
+ end
211
+
212
+ out, err = sys(
213
+ cmd,
214
+ path: config[:bin],
215
+ stderr: [output, 'stderr'].join,
216
+ stdout: [output, 'stdout'].join
217
+ )
218
+
215
219
  puts out.strip
216
220
  puts err.strip
217
- return true
221
+
222
+ true
218
223
  rescue CommandFailed => e
219
224
  puts <<~MSG
220
225
  Could not create BLAST database for: #{file}
@@ -261,7 +266,7 @@ module SequenceServer
261
266
  # /home/ben/pd.ben/sequenceserver/db/nr00 => no
262
267
  # /mnt/blast-db/refseq_genomic.100 => yes
263
268
  def multipart_database_name?(db_name)
264
- !(db_name.match(%r{.+/\S+\.\d{2,3}$}).nil?)
269
+ !db_name.match(%r{.+/\S+\.\d{2,3}$}).nil?
265
270
  end
266
271
 
267
272
  def get_categories(path)
@@ -273,7 +278,10 @@ module SequenceServer
273
278
 
274
279
  # Returns true if first character of the file is '>'.
275
280
  def probably_fasta?(file)
276
- return false unless file.match(/((cds)|(fasta)|(fna)|(pep)|(cdna)|(fa)|(prot)|(fas)|(genome)|(nuc)|(dna)|(nt))$/i)
281
+ unless file.match(/((cdna)|(cds)|(dna)|(fa)|(faa)|(fas)|(fasta)|(fna)|(genome)|(nt)|(nuc)|(pep)|(prot))$/i)
282
+ return false
283
+ end
284
+
277
285
  File.read(file, 1) == '>'
278
286
  end
279
287
 
@@ -42,7 +42,7 @@ class Pool
42
42
 
43
43
  def shutdown
44
44
  @size.times do
45
- schedule { throw :exit }
45
+ queue { throw :exit }
46
46
  end
47
47
  @pool.map(&:join)
48
48
  end
@@ -7,11 +7,8 @@ module SequenceServer
7
7
  # own report subclass.
8
8
  class Report
9
9
  class << self
10
- # Generates report for the given job. Returns generated report object.
11
- #
12
- # TODO: Dynamic dispatch.
13
10
  def generate(job)
14
- BLAST::Report.new(job)
11
+ BLAST::Report.new(job).generate
15
12
  end
16
13
  end
17
14
 
@@ -23,7 +20,6 @@ module SequenceServer
23
20
  def initialize(job)
24
21
  @job = job
25
22
  yield if block_given?
26
- generate
27
23
  end
28
24
 
29
25
  attr_reader :job
@@ -106,7 +106,28 @@ module SequenceServer
106
106
  get '/:jid.json' do |jid|
107
107
  job = Job.fetch(jid)
108
108
  halt 202 unless job.done?
109
- Report.generate(job).to_json
109
+
110
+ report = Report.generate(job)
111
+ halt 202 unless report.done?
112
+
113
+ display_large_result_warning =
114
+ SequenceServer.config[:large_result_warning_threshold].to_i.positive? &&
115
+ params[:bypass_file_size_warning] != 'true' &&
116
+ report.xml_file_size > SequenceServer.config[:large_result_warning_threshold]
117
+
118
+ if display_large_result_warning
119
+ halt 200,
120
+ {
121
+ user_warning: 'LARGE_RESULT',
122
+ download_links: [
123
+ { name: 'Standard Tabular Report', url: "download/#{jid}.std_tsv" },
124
+ { name: 'Full Tabular Report', url: "/download/#{jid}.full_tsv" },
125
+ { name: 'Results in XML', url: "/download/#{jid}.xml" }
126
+ ]
127
+ }.to_json
128
+ end
129
+
130
+ report.to_json
110
131
  end
111
132
 
112
133
  # Returns base HTML. Rest happens client-side: polling for and rendering
@@ -145,7 +166,7 @@ module SequenceServer
145
166
  get '/download/:jid.:type' do |jid, type|
146
167
  job = Job.fetch(jid)
147
168
  out = BLAST::Formatter.new(job, type)
148
- send_file out.file, filename: out.filename, type: out.mime
169
+ send_file out.filepath, filename: out.filename, type: out.mime
149
170
  end
150
171
 
151
172
  post '/cloud_share' do
@@ -67,7 +67,7 @@ module SequenceServer
67
67
 
68
68
  # Now move the temporary file to the given path.
69
69
  # TODO: don't we need to explicitly close the temp file here?
70
- FileUtils.mv(temp_files.delete(channel), filename)
70
+ FileUtils.cp(temp_files[channel], filename)
71
71
  end
72
72
 
73
73
  # Read the remaining temp files into memory. For large outputs,
@@ -1,4 +1,4 @@
1
1
  # Define version number.
2
2
  module SequenceServer
3
- VERSION = '2.1.0'.freeze
3
+ VERSION = '2.2.0'.freeze
4
4
  end
@@ -62,6 +62,9 @@ module SequenceServer
62
62
 
63
63
  # SequenceServer initialisation routine.
64
64
  def init(config = {})
65
+ # Reset makeblastdb cache, because configuration may have changed.
66
+ @makeblastdb = nil
67
+
65
68
  # Use default config file if caller didn't specify one.
66
69
  config[:config_file] ||= DEFAULT_CONFIG_FILE
67
70
 
@@ -201,10 +204,13 @@ module SequenceServer
201
204
 
202
205
  logger.debug("Will look for BLAST+ databases in: #{config[:database_dir]}")
203
206
 
204
- makeblastdb.scan
205
- fail NO_BLAST_DATABASE_FOUND, config[:database_dir] if !makeblastdb.any_formatted?
207
+ fail NO_BLAST_DATABASE_FOUND, config[:database_dir] unless makeblastdb.any_formatted?
206
208
 
207
209
  Database.collection = makeblastdb.formatted_fastas
210
+ check_database_compatibility unless config[:optimistic].to_s == 'true'
211
+ end
212
+
213
+ def check_database_compatibility
208
214
  Database.each do |database|
209
215
  logger.debug "Found #{database.type} database '#{database.title}' at '#{database.path}'"
210
216
  if database.non_parse_seqids?
data/public/js/report.js CHANGED
@@ -28,6 +28,8 @@ class Report extends Component {
28
28
  this.nextHSP = 0;
29
29
  this.maxHSPs = 3; // max HSPs to render in a cycle
30
30
  this.state = {
31
+ user_warning: null,
32
+ download_links: [],
31
33
  search_id: '',
32
34
  seqserv_version: '',
33
35
  program: '',
@@ -52,9 +54,8 @@ class Report extends Component {
52
54
  fetchResults() {
53
55
  var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
54
56
  var component = this;
55
-
56
57
  function poll() {
57
- $.getJSON(location.pathname + '.json').complete(function (jqXHR) {
58
+ $.getJSON(location.pathname + '.json' + location.search).complete(function (jqXHR) {
58
59
  switch (jqXHR.status) {
59
60
  case 202:
60
61
  var interval;
@@ -86,7 +87,11 @@ class Report extends Component {
86
87
  setStateFromJSON(responseJSON) {
87
88
  this.lastTimeStamp = Date.now();
88
89
  // the callback prepares the download link for all alignments
89
- this.setState(responseJSON, this.prepareAlignmentOfAllHits);
90
+ if (responseJSON.user_warning == 'LARGE_RESULT') {
91
+ this.setState({user_warning: responseJSON.user_warning, download_links: responseJSON.download_links});
92
+ } else {
93
+ this.setState(responseJSON, this.prepareAlignmentOfAllHits);
94
+ }
90
95
  }
91
96
  /**
92
97
  * Called as soon as the page has loaded and the user sees the loading spinner.
@@ -108,8 +113,8 @@ class Report extends Component {
108
113
  * start iteratively adding 1 HSP to the page every 25 milli-seconds.
109
114
  */
110
115
  componentDidUpdate() {
111
- // Log to console how long the last update take?
112
- console.log((Date.now() - this.lastTimeStamp) / 1000);
116
+ // Log to console how long the last update take?
117
+ // console.log((Date.now() - this.lastTimeStamp) / 1000);
113
118
 
114
119
  // Lock sidebar in its position on the first update.
115
120
  if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {
@@ -286,6 +291,40 @@ class Report extends Component {
286
291
  );
287
292
  }
288
293
 
294
+
295
+ warningJSX() {
296
+ return(
297
+ <div className="container">
298
+ <div className="row">
299
+ <div className="col-md-6 col-md-offset-3 text-center">
300
+ <h1>
301
+ <i className="fa fa-exclamation-triangle"></i>&nbsp; Warning
302
+ </h1>
303
+ <p>
304
+ <br />
305
+ The BLAST result might be too large to load in the browser. If you have a powerful machine you can try loading the results anyway. Otherwise, you can download the results and view them locally.
306
+ </p>
307
+ <br />
308
+ <p>
309
+ {this.state.download_links.map((link, index) => {
310
+ return (
311
+ <a href={link.url} className="btn btn-secondary" key={'download_link_' + index} >
312
+ {link.name}
313
+ </a>
314
+ );
315
+ })}
316
+ </p>
317
+ <br />
318
+ <p>
319
+ <a href={location.pathname + '?bypass_file_size_warning=true'} className="btn btn-primary">
320
+ View results in browser anyway
321
+ </a>
322
+ </p>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ );
327
+ }
289
328
  /**
290
329
  * Renders report overview.
291
330
  */
@@ -350,6 +389,15 @@ class Report extends Component {
350
389
  return this.state.queries.length >= 1;
351
390
  }
352
391
 
392
+ /**
393
+ * Indicates the response contains a warning message for the user
394
+ * in which case we should not render the results and render the
395
+ * warning instead.
396
+ **/
397
+ isUserWarningPresent() {
398
+ return this.state.user_warning;
399
+ }
400
+
353
401
  /**
354
402
  * Returns true if we have at least one hit.
355
403
  */
@@ -539,7 +587,13 @@ class Report extends Component {
539
587
  }
540
588
 
541
589
  render() {
542
- return this.isResultAvailable() ? this.resultsJSX() : this.loadingJSX();
590
+ if (this.isUserWarningPresent()) {
591
+ return this.warningJSX();
592
+ } else if (this.isResultAvailable()) {
593
+ return this.resultsJSX();
594
+ } else {
595
+ return this.loadingJSX();
596
+ }
543
597
  }
544
598
  }
545
599
 
@@ -0,0 +1,36 @@
1
+ /* eslint-disable no-unused-vars */
2
+ /* eslint-disable no-undef */
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import { Form } from '../form';
5
+ import { AMINO_ACID_SEQUENCE } from './mock_data/sequences';
6
+ import data from './mock_data/databases.json';
7
+ import userEvent from '@testing-library/user-event';
8
+ import '@testing-library/jest-dom/extend-expect';
9
+ import '@testing-library/react/dont-cleanup-after-each';
10
+
11
+ export const setMockJSONResult = (result) => {
12
+ global.$.getJSON = (_, cb) => cb(result);
13
+ };
14
+ describe('ADVANCED PARAMETERS', () => {
15
+ const getInputElement = () => screen.getByRole('textbox', { name: '' });
16
+ test('should not render the link to advanced parameters modal if blast algorithm is unknown', () => {
17
+ setMockJSONResult(data);
18
+ const {container } =render(<Form onSequenceTypeChanged={() => { }
19
+ } />);
20
+ const modalButton = container.querySelector('[data-target="#help"]');
21
+ expect(modalButton).toBeNull();
22
+ });
23
+ test('should render the link to advanced parameters modal if blast algorithm is known', () => {
24
+ setMockJSONResult(data);
25
+ const {container } =render(<Form onSequenceTypeChanged={() => { }
26
+ } />);
27
+
28
+ const inputEl = getInputElement();
29
+ // populate search and select dbs to determine blast algorithm
30
+ fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
31
+ const proteinSelectAllBtn = screen.getByRole('heading', { name: /protein databases/i }).parentElement.querySelector('button');
32
+ fireEvent.click(proteinSelectAllBtn);
33
+ const modalButton = container.querySelector('[data-target="#help"]');
34
+ expect(modalButton).not.toBeNull();
35
+ });
36
+ });
@@ -0,0 +1,32 @@
1
+ export const AMINO_ACID_SEQUENCE = `MNTLWLSLWDYPGKLPLNFMVFDTKDDLQAAYWRDPYSIPLAVIFEDPQPISQRLIYEIR
2
+ TNPSYTLPPPPTKLYSAPISCRKNKTGHWMDDILSIKTGESCPVNNYLHSGFLALQMITD
3
+ ITKIKLENSDVTIPDIKLIMFPKEPYTADWMLAFRVVIPLYMVLALSQFITYLLILIVGE
4
+ KENKIKEGMKMMGLNDSVF
5
+ >SI2.2.0_13722 locus=Si_gnF.scaffold06207[1925625..1928536].pep_1 quality=100.00
6
+ MSANRLNVLVTLMLAVALLVTESGNAQVDGYLQFNPKRSAVSSPQKYCGKKLSNALQIIC
7
+ DGVYNSMFKKSGQDFPPQNKRHIAHRINGNEEESFTTLKSNFLNWCVEVYHRHYRFVFVS
8
+ EMEMADYPLAYDISPYLPPFLSRARARGMLDGRFAGRRYRRESRGIHEECCINGCTINEL
9
+ TSYCGP
10
+ `;
11
+ export const NUCLEOTIDE_SEQUENCE = `ATGAATACCCTCTGGCTCTCTTTATGGGATTATCCCGGTAAGCTTCCCTTAAACTTCATG
12
+ GTGTTTGACACGAAGGATGATCTGCAAGCAGCGTATTGGAGAGATCCTTACAGCATACCT
13
+ CTGGCAGTTATCTTCGAGGACCCCCAACCGATATCACAGCGACTTATATATGAAATTAGG
14
+ ACGAATCCTTCATACACTTTGCCGCCACCGCCAACCAAATTGTATTCTGCTCCGATCAGT
15
+ TGTCGAAAGAATAAAACTGGTCACTGGATGGACGACATTTTATCGATAAAAACCGGTGAA
16
+ TCTTGTCCCGTTAACAATTACTTGCATTCTGGCTTCTTGGCTCTGCAAATGATAACGGAT
17
+ ATCACAAAGATAAAATTGGAAAATTCTGACGTGACAATACCGGATATTAAACTCATAATG
18
+ TTTCCTAAAGAGCCGTATACCGCTGACTGGATGCTGGCCTTCAGAGTTGTTATTCCGCTT
19
+ TACATGGTCTTGGCTCTCTCGCAATTTATCACTTATCTTCTGATCCTAATAGTTGGCGAG
20
+ AAGGAAAATAAGATTAAAGAGGGAATGAAGATGATGGGCTTAAATGATTCTGTGTTT
21
+ >SI2.2.0_13722 Si_gnF.scaffold06207[1925625..1928536].pep_1
22
+ ATGTCCGCGAATCGATTGAACGTGCTGGTGACCCTGATGCTCGCCGTCGCGCTTCTTGTG
23
+ ACGGAATCAGGAAATGCACAGGTGGATGGCTATCTCCAATTCAACCCAAAGCGATCCGCC
24
+ GTGAGCTCGCCGCAGAAGTATTGCGGCAAAAAGCTTTCTAATGCTCTACAGATAATCTGT
25
+ GATGGCGTGTACAATTCCATGTTTAAGAAGAGTGGTCAAGATTTTCCCCCGCAAAATAAG
26
+ AGACACATAGCACACAGAATAAATGGGAATGAGGAAGAGAGCTTTACTACGTTAAAGTCG
27
+ AATTTTTTAAACTGGTGTGTTGAAGTTTATCATCGTCACTACAGATTCGTTTTTGTTTCA
28
+ GAGATGGAAATGGCCGATTACCCGCTCGCCTATGATATTTCCCCGTATCTTCCGCCGTTC
29
+ CTGTCGCGAGCGAGGGCACGGGGAATGTTAGACGGTCGCTTCGCCGGCAGACGCTACCGA
30
+ AGGGAGTCGCGGGGCATTCACGAGGAGTGTTGCATCAACGGATGTACGATAAACGAATTG
31
+ ACCAGCTACTGCGGCCCC
32
+ `;
@@ -25,7 +25,14 @@ const TestSidebar = ({ long }) => {
25
25
  />;
26
26
  };
27
27
 
28
+ const clickCheckboxes = (checkboxes, count) => {
29
+ Array.from(checkboxes).slice(0, count).forEach((checkbox) => {
30
+ fireEvent.click(checkbox);
31
+ });
32
+ };
28
33
  describe('REPORT PAGE', () => {
34
+ global.URL.createObjectURL = jest.fn();//.mockReturnValue('xyz.test');
35
+ global.setTimeout = (cb) => cb();
29
36
  it('should render the report component with initial loading state', () => {
30
37
  render(<Report />);
31
38
  expect(screen.getByRole('heading', { name: 'BLAST-ing' })).toBeInTheDocument();
@@ -39,7 +46,7 @@ describe('REPORT PAGE', () => {
39
46
  });
40
47
  it('it should render the report page correctly if there\'s a response provided', () => {
41
48
  setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
42
- const { container } = render(<Report />);
49
+ const { container } = render(<Report getCharacterWidth={jest.fn()} />);
43
50
  expect(container.querySelector('#results')).toBeInTheDocument();
44
51
 
45
52
  });
@@ -63,18 +70,18 @@ describe('REPORT PAGE', () => {
63
70
  });
64
71
 
65
72
  describe('LONG QUERIES (>12)', () => {
66
-
73
+ let container;
74
+ beforeEach(() => {
75
+ container = render(<TestSidebar long />).container;
76
+ });
67
77
  it('should not show navigation links for long queries', () => {
68
- const { container } = render(<TestSidebar long />);
69
78
  expect(container.querySelectorAll('a[href^="#Query_"]').length).toBe(0);
70
79
  });
71
80
  it('should show only next button if on first query ', () => {
72
- render(<TestSidebar long />);
73
81
  expect(nextQueryButton()).toBeInTheDocument();
74
82
  expect(previousQueryButton()).not.toBeInTheDocument();
75
83
  });
76
84
  it('should show both previous and next buttons if not on first query', () => {
77
- render(<TestSidebar long />);
78
85
  const nextBtn = nextQueryButton();
79
86
  expect(nextBtn).toBeInTheDocument();
80
87
  fireEvent.click(nextBtn);
@@ -84,7 +91,6 @@ describe('REPORT PAGE', () => {
84
91
  });
85
92
  it('should show only previous button if on last query', () => {
86
93
  const { queries } = longResponseJSON;
87
- render(<TestSidebar long />);
88
94
  expect(nextQueryButton()).toBeInTheDocument();
89
95
  expect(previousQueryButton()).not.toBeInTheDocument();
90
96
 
@@ -95,5 +101,55 @@ describe('REPORT PAGE', () => {
95
101
  expect(previousQueryButton()).toBeInTheDocument();
96
102
  });
97
103
  });
104
+
105
+ describe('DOWNLOAD LINKS', () => {
106
+ let container;
107
+ beforeEach(() => {
108
+ setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
109
+ container = render(<Report getCharacterWidth={jest.fn()} />).container;
110
+ });
111
+ describe('ALIGNMENT DOWNLOAD', () => {
112
+ it('should generate a blob url and filename for downloading alignment of all hits on render', () => {
113
+ const alignment_download_link = container.querySelector('.download-alignment-of-all');
114
+ const expected_num_hits = container.querySelectorAll('.hit-links input[type="checkbox"]').length;
115
+ const file_name = `alignment-${expected_num_hits}_hits.txt`;
116
+ expect(alignment_download_link.download).toEqual(file_name);
117
+ expect(alignment_download_link.hred).not.toEqual('#');
118
+ });
119
+ it('link for downloading alignment of specific number of selected hits should be disabled on initial load', () => {
120
+ const alignment_download_link = container.querySelector('.download-alignment-of-selected');
121
+ expect(alignment_download_link.classList.contains('disabled')).toBeTruthy();
122
+
123
+ });
124
+ it('should generate a blob url and filename for downloading alignment of specific number of selected hits', () => {
125
+ const alignment_download_link = container.querySelector('.download-alignment-of-selected');
126
+ // QUERY ALL HIT LINKS CHECKBOXES
127
+ const checkboxes = container.querySelectorAll('.hit-links input[type="checkbox"]');
128
+ // SELECT 4 CHECKBOXES
129
+ clickCheckboxes(checkboxes, 4);
130
+ const file_name = 'alignment-4_hits.txt';
131
+ expect(alignment_download_link.textContent).toEqual('Alignment of 4 selected hit(s)');
132
+ expect(alignment_download_link.download).toEqual(file_name);
133
+ });
134
+ });
135
+
136
+ describe('FASTA DOWNLOAD', () => {
137
+ let fasta_download_link;
138
+ beforeEach(() => {
139
+ fasta_download_link = container.querySelector('.download-fasta-of-selected');
140
+ });
141
+ it('link for downloading fasta of selected number of hits should be disabled on initial load', () => {
142
+ expect(fasta_download_link.classList.contains('disabled')).toBeTruthy();
143
+ });
144
+
145
+ it('link for downloading fasta of specific number of selected hits should be active after selection', () => {
146
+ const checkboxes = container.querySelectorAll('.hit-links input[type="checkbox"]');
147
+ // SELECT 5 CHECKBOXES
148
+ clickCheckboxes(checkboxes, 5);
149
+ expect(fasta_download_link.textContent).toEqual('FASTA of 5 selected hit(s)');
150
+ });
151
+ });
152
+ });
98
153
  });
154
+
99
155
  });
@@ -3,39 +3,56 @@
3
3
  import { render, screen, fireEvent } from '@testing-library/react';
4
4
  import { SearchQueryWidget } from '../query';
5
5
  import { Form } from '../form';
6
- import userEvent from '@testing-library/user-event';
6
+ import { AMINO_ACID_SEQUENCE, NUCLEOTIDE_SEQUENCE } from './mock_data/sequences';
7
7
  import '@testing-library/jest-dom/extend-expect';
8
8
  import '@testing-library/react/dont-cleanup-after-each';
9
9
 
10
- export const AMINO_ACID_SEQUENCE = `MNTLWLSLWDYPGKLPLNFMVFDTKDDLQAAYWRDPYSIPLAVIFEDPQPISQRLIYEIR
11
- TNPSYTLPPPPTKLYSAPISCRKNKTGHWMDDILSIKTGESCPVNNYLHSGFLALQMITD
12
- ITKIKLENSDVTIPDIKLIMFPKEPYTADWMLAFRVVIPLYMVLALSQFITYLLILIVGE
13
- KENKIKEGMKMMGLNDSVF
14
- >SI2.2.0_13722 locus=Si_gnF.scaffold06207[1925625..1928536].pep_1 quality=100.00
15
- MSANRLNVLVTLMLAVALLVTESGNAQVDGYLQFNPKRSAVSSPQKYCGKKLSNALQIIC
16
- DGVYNSMFKKSGQDFPPQNKRHIAHRINGNEEESFTTLKSNFLNWCVEVYHRHYRFVFVS
17
- EMEMADYPLAYDISPYLPPFLSRARARGMLDGRFAGRRYRRESRGIHEECCINGCTINEL
18
- TSYCGP
19
- `;
10
+ let container;
11
+ let inputEl;
12
+
20
13
  describe('SEARCH COMPONENT', () => {
21
- const getInputElement = () => screen.getByRole('textbox', { name: '' });
14
+ beforeEach(() => {
15
+ container = render(<Form onSequenceTypeChanged={() => { }
16
+ } />).container;
17
+ inputEl = screen.getByRole('textbox', { name: '' });
18
+ });
19
+
22
20
  test('should render the search component textarea', () => {
23
- render(<SearchQueryWidget onSequenceTypeChanged={() => { }
24
- } />);
25
- const el = getInputElement();
26
- expect(el).toHaveClass('form-control');
21
+ expect(inputEl).toHaveClass('form-control');
27
22
  });
28
23
 
29
24
  test('clear button should only become visible if textarea is not empty', () => {
30
- render(<SearchQueryWidget onSequenceTypeChanged={() => { }
31
- } />);
32
25
  const getButtonWrapper = () => screen.getByRole('button', { name: /clear query sequence\(s\)\./i }).parentElement;
33
26
  expect(getButtonWrapper()).toHaveClass('hidden');
34
- const inputEl = getInputElement();
35
27
  fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
36
28
  expect(getButtonWrapper()).not.toHaveClass('hidden');
37
29
  fireEvent.change(inputEl, { target: { value: '' } });
38
30
  expect(getButtonWrapper()).toHaveClass('hidden');
31
+ });
39
32
 
33
+ test('should correctly detect the amino-acid sequence type and show notification', () => {
34
+ // populate search
35
+ fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
36
+ const activeNotification = container.querySelector('.notification.active');
37
+ expect(activeNotification.id).toBe('protein-sequence-notification');
38
+ const alertWrapper = activeNotification.children[0];
39
+ expect(alertWrapper).toHaveTextContent('Detected: amino-acid sequence(s).');
40
+ });
41
+
42
+ test('should correctly detect the nucleotide sequence type and show notification', () => {
43
+ // populate search
44
+ fireEvent.change(inputEl, { target: { value: NUCLEOTIDE_SEQUENCE } });
45
+ const activeNotification = container.querySelector('.notification.active');
46
+ const alertWrapper = activeNotification.children[0];
47
+ expect(activeNotification.id).toBe('nucleotide-sequence-notification');
48
+ expect(alertWrapper).toHaveTextContent('Detected: nucleotide sequence(s).');
49
+ });
50
+
51
+ test('should correctly detect the mixed sequences and show error notification', () => {
52
+ fireEvent.change(inputEl, { target: { value: `${NUCLEOTIDE_SEQUENCE}${AMINO_ACID_SEQUENCE}` } });
53
+ const activeNotification = container.querySelector('.notification.active');
54
+ expect(activeNotification.id).toBe('mixed-sequence-notification');
55
+ const alertWrapper = activeNotification.children[0];
56
+ expect(alertWrapper).toHaveTextContent('Error: mixed nucleotide and amino-acid sequences detected.');
40
57
  });
41
58
  });
@@ -1,5 +1,5 @@
1
1
  import _ from 'underscore';
2
-
2
+ import d3 from 'd3';
3
3
  export function get_colors_for_evalue(evalue, hits) {
4
4
  var colors = d3.scale
5
5
  .log()
@@ -181,7 +181,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
181
181
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
182
182
 
183
183
  "use strict";
184
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _jquery_world__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./jquery_world */ \"./public/js/jquery_world.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n/* harmony import */ var _sidebar__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sidebar */ \"./public/js/sidebar.js\");\n/* harmony import */ var _circos__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./circos */ \"./public/js/circos.js\");\n/* harmony import */ var _query__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./query */ \"./public/js/query.js\");\n/* harmony import */ var _hit__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./hit */ \"./public/js/hit.js\");\n/* harmony import */ var _hsp__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./hsp */ \"./public/js/hsp.js\");\n/* harmony import */ var _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./alignment_exporter */ \"./public/js/alignment_exporter.js\");\n/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! react/jsx-runtime */ \"./node_modules/react/jsx-runtime.js\");\n/* provided dependency */ var $ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }, _typeof(obj); }\n\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, \"prototype\", { writable: false }); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, \"prototype\", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } else if (call !== void 0) { throw new TypeError(\"Derived constructors may only return object or undefined\"); } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n // for custom $.tooltip function\n\n\n\n\n\n\n\n\n\n/**\n * Renders entire report.\n *\n * Composed of Query and Sidebar components.\n */\n\n\n\n\nvar Report = /*#__PURE__*/function (_Component) {\n _inherits(Report, _Component);\n\n var _super = _createSuper(Report);\n\n function Report(props) {\n var _this;\n\n _classCallCheck(this, Report);\n\n _this = _super.call(this, props); // Properties below are internal state used to render results in small\n // slices (see updateState).\n\n _this.numUpdates = 0;\n _this.nextQuery = 0;\n _this.nextHit = 0;\n _this.nextHSP = 0;\n _this.maxHSPs = 3; // max HSPs to render in a cycle\n\n _this.state = {\n search_id: '',\n seqserv_version: '',\n program: '',\n program_version: '',\n submitted_at: '',\n queries: [],\n results: [],\n querydb: [],\n params: [],\n stats: [],\n alignment_blob_url: '',\n allQueriesLoaded: false,\n cloud_sharing_enabled: false\n };\n _this.prepareAlignmentOfSelectedHits = _this.prepareAlignmentOfSelectedHits.bind(_assertThisInitialized(_this));\n _this.prepareAlignmentOfAllHits = _this.prepareAlignmentOfAllHits.bind(_assertThisInitialized(_this));\n _this.setStateFromJSON = _this.setStateFromJSON.bind(_assertThisInitialized(_this));\n return _this;\n }\n /**\n * Fetch results.\n */\n\n\n _createClass(Report, [{\n key: \"fetchResults\",\n value: function fetchResults() {\n var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];\n var component = this;\n\n function poll() {\n $.getJSON(location.pathname + '.json').complete(function (jqXHR) {\n switch (jqXHR.status) {\n case 202:\n var interval;\n\n if (intervals.length === 1) {\n interval = intervals[0];\n } else {\n interval = intervals.shift();\n }\n\n setTimeout(poll, interval);\n break;\n\n case 200:\n component.setStateFromJSON(jqXHR.responseJSON);\n break;\n\n case 404:\n case 400:\n case 500:\n component.props.showErrorModal(jqXHR.responseJSON);\n break;\n }\n });\n }\n\n poll();\n }\n /**\n * Calls setState after any required modification to responseJSON.\n */\n\n }, {\n key: \"setStateFromJSON\",\n value: function setStateFromJSON(responseJSON) {\n this.lastTimeStamp = Date.now(); // the callback prepares the download link for all alignments\n\n this.setState(responseJSON, this.prepareAlignmentOfAllHits);\n }\n /**\n * Called as soon as the page has loaded and the user sees the loading spinner.\n * We use this opportunity to setup services that make use of delegated events\n * bound to the window, document, or body.\n */\n\n }, {\n key: \"componentDidMount\",\n value: function componentDidMount() {\n this.fetchResults(); // This sets up an event handler which enables users to select text from\n // hit header without collapsing the hit.\n\n this.preventCollapseOnSelection();\n this.toggleTable();\n }\n /**\n * Called for the first time after as BLAST results have been retrieved from\n * the server and added to this.state by fetchResults. Only summary overview\n * and circos would have been rendered at this point. At this stage we kick\n * start iteratively adding 1 HSP to the page every 25 milli-seconds.\n */\n\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate() {\n var _this2 = this;\n\n // Log to console how long the last update take?\n console.log((Date.now() - this.lastTimeStamp) / 1000); // Lock sidebar in its position on the first update.\n\n if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {\n this.affixSidebar();\n } // Queue next update if we have not rendered all results yet.\n\n\n if (this.nextQuery < this.state.queries.length) {\n // setTimeout is used to clear call stack and space out\n // the updates giving the browser a chance to respond\n // to user interactions.\n setTimeout(function () {\n return _this2.updateState();\n }, 25);\n } else {\n this.componentFinishedUpdating();\n }\n }\n /**\n * Push next slice of results to React for rendering.\n */\n\n }, {\n key: \"updateState\",\n value: function updateState() {\n var results = [];\n var numHSPsProcessed = 0;\n\n while (this.nextQuery < this.state.queries.length) {\n var query = this.state.queries[this.nextQuery]; // We may see a query multiple times during rendering because only\n // 3 hsps or are rendered in each cycle, but we want to create the\n // corresponding Query component only the first time we see it.\n\n if (this.nextHit == 0 && this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_query__WEBPACK_IMPORTED_MODULE_5__.ReportQuery, {\n query: query,\n program: this.state.program,\n querydb: this.state.querydb,\n showQueryCrumbs: this.state.queries.length > 1,\n non_parse_seqids: this.state.non_parse_seqids,\n imported_xml: this.state.imported_xml,\n veryBig: this.state.veryBig\n }, 'Query_' + query.number));\n }\n\n while (this.nextHit < query.hits.length) {\n var hit = query.hits[this.nextHit]; // We may see a hit multiple times during rendering because only\n // 10 hsps are rendered in each cycle, but we want to create the\n // corresponding Hit component only the first time we see it.\n\n if (this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hit__WEBPACK_IMPORTED_MODULE_6__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n algorithm: this.state.program,\n querydb: this.state.querydb,\n selectHit: this.selectHit,\n imported_xml: this.state.imported_xml,\n non_parse_seqids: this.state.non_parse_seqids,\n showQueryCrumbs: this.state.queries.length > 1,\n showHitCrumbs: query.hits.length > 1,\n veryBig: this.state.veryBig,\n onChange: this.prepareAlignmentOfSelectedHits\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number));\n }\n\n while (this.nextHSP < hit.hsps.length) {\n // Get nextHSP and increment the counter.\n var hsp = hit.hsps[this.nextHSP++];\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hsp__WEBPACK_IMPORTED_MODULE_7__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n hsp: hsp,\n algorithm: this.state.program,\n showHSPNumbers: hit.hsps.length > 1\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number + '_HSP_' + hsp.number));\n numHSPsProcessed++;\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hsps of a hit,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHSP == hit.hsps.length) {\n this.nextHit = this.nextHit + 1;\n this.nextHSP = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hits of a query,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHit == query.hits.length) {\n this.nextQuery = this.nextQuery + 1;\n this.nextHit = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Push the components to react for rendering.\n\n\n this.numUpdates++;\n this.lastTimeStamp = Date.now();\n this.setState({\n results: this.state.results.concat(results),\n veryBig: this.numUpdates >= 250\n });\n }\n /**\n * Called after all results have been rendered.\n */\n\n }, {\n key: \"componentFinishedUpdating\",\n value: function componentFinishedUpdating() {\n if (this.state.allQueriesLoaded) return;\n this.shouldShowIndex() && this.setupScrollSpy();\n this.setState({\n allQueriesLoaded: true\n });\n }\n /**\n * Returns loading message\n */\n\n }, {\n key: \"loadingJSX\",\n value: function loadingJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"row\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-6 col-md-offset-3 text-center\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"h1\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"i\", {\n className: \"fa fa-cog fa-spin\"\n }), \"\\xA0 BLAST-ing\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"This can take some time depending on the size of your query and database(s). The page will update automatically when BLAST is done.\", /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"You can bookmark the page and come back to it later or share the link with someone.\"]\n })]\n })\n });\n }\n /**\n * Return results JSX.\n */\n\n }, {\n key: \"resultsJSX\",\n value: function resultsJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"row\",\n id: \"results\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"col-md-3 hidden-sm hidden-xs\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_sidebar__WEBPACK_IMPORTED_MODULE_3__[\"default\"], {\n data: this.state,\n atLeastOneHit: this.atLeastOneHit(),\n shouldShowIndex: this.shouldShowIndex(),\n allQueriesLoaded: this.state.allQueriesLoaded,\n cloudSharingEnabled: this.state.cloud_sharing_enabled\n })\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-9\",\n children: [this.overviewJSX(), this.circosJSX(), this.state.results]\n })]\n });\n }\n /**\n * Renders report overview.\n */\n\n }, {\n key: \"overviewJSX\",\n value: function overviewJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"overview\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"strong\", {\n children: [\"SequenceServer \", this.state.seqserv_version]\n }), \" using\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: this.state.program_version\n }), this.state.submitted_at && \", query submitted on \".concat(this.state.submitted_at)]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \" Databases: \"\n }), this.state.querydb.map(function (db) {\n return db.title;\n }).join(', '), ' ', \"(\", this.state.stats.nsequences, \" sequences,\\xA0\", this.state.stats.ncharacters, \" characters)\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \"Parameters: \"\n }), ' ', underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].map(this.state.params, function (val, key) {\n return key + ' ' + val;\n }).join(', ')]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [\"Please cite:\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"a\", {\n href: \"https://doi.org/10.1093/molbev/msz185\",\n children: \"https://doi.org/10.1093/molbev/msz185\"\n })]\n })]\n });\n }\n /**\n * Return JSX for circos if we have at least one hit.\n */\n\n }, {\n key: \"circosJSX\",\n value: function circosJSX() {\n return this.atLeastTwoHits() ? /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_circos__WEBPACK_IMPORTED_MODULE_4__[\"default\"], {\n queries: this.state.queries,\n program: this.state.program,\n collapsed: \"true\"\n }) : /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"span\", {});\n } // Controller //\n\n /**\n * Returns true if results have been fetched.\n *\n * A holding message is shown till results are fetched.\n */\n\n }, {\n key: \"isResultAvailable\",\n value: function isResultAvailable() {\n return this.state.queries.length >= 1;\n }\n /**\n * Returns true if we have at least one hit.\n */\n\n }, {\n key: \"atLeastOneHit\",\n value: function atLeastOneHit() {\n return this.state.queries.some(function (query) {\n return query.hits.length > 0;\n });\n }\n /**\n * Does the report have at least two hits? This is used to determine\n * whether Circos should be enabled or not.\n */\n\n }, {\n key: \"atLeastTwoHits\",\n value: function atLeastTwoHits() {\n var hit_num = 0;\n return this.state.queries.some(function (query) {\n hit_num += query.hits.length;\n return hit_num > 1;\n });\n }\n /**\n * Returns true if index should be shown in the sidebar. Index is shown\n * only for 2 and 8 queries.\n */\n\n }, {\n key: \"shouldShowIndex\",\n value: function shouldShowIndex() {\n var num_queries = this.state.queries.length;\n return num_queries >= 2 && num_queries <= 12;\n }\n /**\n * Prevents folding of hits during text-selection.\n */\n\n }, {\n key: \"preventCollapseOnSelection\",\n value: function preventCollapseOnSelection() {\n $('body').on('mousedown', '.hit > .section-header > h4', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n if (event.type === 'mouseup') {\n // user wants to toggle\n var hitID = $this.parents('.hit').attr('id');\n $(\"div[data-parent-hit=\".concat(hitID, \"]\")).toggle();\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n } else {\n // user wants to select\n $this.attr('data-toggle', '');\n }\n\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /* Handling the fa icon when Hit Table is collapsed */\n\n }, {\n key: \"toggleTable\",\n value: function toggleTable() {\n $('body').on('mousedown', '.resultn > .section-content > .table-hit-overview > .caption', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /**\n * Affixes the sidebar.\n */\n\n }, {\n key: \"affixSidebar\",\n value: function affixSidebar() {\n var $sidebar = $('.sidebar');\n var sidebarOffset = $sidebar.offset();\n\n if (sidebarOffset) {\n $sidebar.affix({\n offset: {\n top: sidebarOffset.top\n }\n });\n }\n }\n /**\n * For the query in viewport, highlights corresponding entry in the index.\n */\n\n }, {\n key: \"setupScrollSpy\",\n value: function setupScrollSpy() {\n $('body').scrollspy({\n target: '.sidebar'\n });\n }\n /**\n * Event-handler when hit is selected\n * Adds glow to hit component.\n * Updates number of Fasta that can be downloaded\n */\n\n }, {\n key: \"selectHit\",\n value: function selectHit(id) {\n var checkbox = $('#' + id);\n var num_checked = $('.hit-links :checkbox:checked').length;\n\n if (!checkbox || !checkbox.val()) {\n return;\n }\n\n var $hit = $(checkbox.data('target')); // Highlight selected hit and enable 'Download FASTA/Alignment of\n // selected' links.\n\n if (checkbox.is(':checked')) {\n $hit.addClass('glow');\n $hit.next('.hsp').addClass('glow');\n $('.download-fasta-of-selected').enable();\n $('.download-alignment-of-selected').enable();\n } else {\n $hit.removeClass('glow');\n $hit.next('.hsp').removeClass('glow');\n }\n\n var $a = $('.download-fasta-of-selected');\n var $b = $('.download-alignment-of-selected');\n\n if (num_checked >= 1) {\n $a.find('.text-bold').html(num_checked);\n $b.find('.text-bold').html(num_checked);\n }\n\n if (num_checked == 0) {\n $a.addClass('disabled').find('.text-bold').html('');\n $b.addClass('disabled').find('.text-bold').html('');\n }\n }\n }, {\n key: \"populate_hsp_array\",\n value: function populate_hsp_array(hit, query_id) {\n return hit.hsps.map(function (hsp) {\n return Object.assign(hsp, {\n hit_id: hit.id,\n query_id: query_id\n });\n });\n }\n }, {\n key: \"prepareAlignmentOfSelectedHits\",\n value: function prepareAlignmentOfSelectedHits() {\n var sequence_ids = $('.hit-links :checkbox:checked').map(function () {\n return this.value;\n }).get();\n\n if (!sequence_ids.length) {\n // remove attributes from link if sequence_ids array is empty\n $('.download-alignment-of-selected').attr('href', '#').removeAttr('download');\n return;\n }\n\n if (this.state.alignment_blob_url) {\n // always revoke existing url if any because this method will always create a new url\n window.URL.revokeObjectURL(this.state.alignment_blob_url);\n }\n\n var hsps_arr = [];\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var self = this;\n\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(this.state.queries, underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].bind(function (query) {\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(query.hits, function (hit) {\n if (underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].indexOf(sequence_ids, hit.id) != -1) {\n hsps_arr = hsps_arr.concat(self.populate_hsp_array(hit, query.id));\n }\n });\n }, this));\n\n var filename = 'alignment-' + sequence_ids.length + '_hits.txt';\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename); // set required download attributes for link\n\n $('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename); // track new url for future removal\n\n this.setState({\n alignment_blob_url: blob_url\n });\n }\n }, {\n key: \"prepareAlignmentOfAllHits\",\n value: function prepareAlignmentOfAllHits() {\n var _this3 = this;\n\n // Get number of hits and array of all hsps.\n var num_hits = 0;\n var hsps_arr = [];\n\n if (!this.state.queries.length) {\n return;\n }\n\n this.state.queries.forEach(function (query) {\n return query.hits.forEach(function (hit) {\n num_hits++;\n hsps_arr = hsps_arr.concat(_this3.populate_hsp_array(hit, query.id));\n });\n });\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var file_name = \"alignment-\".concat(num_hits, \"_hits.txt\");\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, file_name);\n $('.download-alignment-of-all').attr('href', blob_url).attr('download', file_name);\n return false;\n }\n }, {\n key: \"render\",\n value: function render() {\n return this.isResultAvailable() ? this.resultsJSX() : this.loadingJSX();\n }\n }]);\n\n return Report;\n}(react__WEBPACK_IMPORTED_MODULE_1__.Component);\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Report);\n\n//# sourceURL=webpack://SequenceServer/./public/js/report.js?");
184
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _jquery_world__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./jquery_world */ \"./public/js/jquery_world.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n/* harmony import */ var _sidebar__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sidebar */ \"./public/js/sidebar.js\");\n/* harmony import */ var _circos__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./circos */ \"./public/js/circos.js\");\n/* harmony import */ var _query__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./query */ \"./public/js/query.js\");\n/* harmony import */ var _hit__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./hit */ \"./public/js/hit.js\");\n/* harmony import */ var _hsp__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./hsp */ \"./public/js/hsp.js\");\n/* harmony import */ var _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./alignment_exporter */ \"./public/js/alignment_exporter.js\");\n/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! react/jsx-runtime */ \"./node_modules/react/jsx-runtime.js\");\n/* provided dependency */ var $ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }, _typeof(obj); }\n\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, \"prototype\", { writable: false }); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, \"prototype\", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } else if (call !== void 0) { throw new TypeError(\"Derived constructors may only return object or undefined\"); } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n // for custom $.tooltip function\n\n\n\n\n\n\n\n\n\n/**\n * Renders entire report.\n *\n * Composed of Query and Sidebar components.\n */\n\n\n\n\nvar Report = /*#__PURE__*/function (_Component) {\n _inherits(Report, _Component);\n\n var _super = _createSuper(Report);\n\n function Report(props) {\n var _this;\n\n _classCallCheck(this, Report);\n\n _this = _super.call(this, props); // Properties below are internal state used to render results in small\n // slices (see updateState).\n\n _this.numUpdates = 0;\n _this.nextQuery = 0;\n _this.nextHit = 0;\n _this.nextHSP = 0;\n _this.maxHSPs = 3; // max HSPs to render in a cycle\n\n _this.state = {\n user_warning: null,\n download_links: [],\n search_id: '',\n seqserv_version: '',\n program: '',\n program_version: '',\n submitted_at: '',\n queries: [],\n results: [],\n querydb: [],\n params: [],\n stats: [],\n alignment_blob_url: '',\n allQueriesLoaded: false,\n cloud_sharing_enabled: false\n };\n _this.prepareAlignmentOfSelectedHits = _this.prepareAlignmentOfSelectedHits.bind(_assertThisInitialized(_this));\n _this.prepareAlignmentOfAllHits = _this.prepareAlignmentOfAllHits.bind(_assertThisInitialized(_this));\n _this.setStateFromJSON = _this.setStateFromJSON.bind(_assertThisInitialized(_this));\n return _this;\n }\n /**\n * Fetch results.\n */\n\n\n _createClass(Report, [{\n key: \"fetchResults\",\n value: function fetchResults() {\n var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];\n var component = this;\n\n function poll() {\n $.getJSON(location.pathname + '.json' + location.search).complete(function (jqXHR) {\n switch (jqXHR.status) {\n case 202:\n var interval;\n\n if (intervals.length === 1) {\n interval = intervals[0];\n } else {\n interval = intervals.shift();\n }\n\n setTimeout(poll, interval);\n break;\n\n case 200:\n component.setStateFromJSON(jqXHR.responseJSON);\n break;\n\n case 404:\n case 400:\n case 500:\n component.props.showErrorModal(jqXHR.responseJSON);\n break;\n }\n });\n }\n\n poll();\n }\n /**\n * Calls setState after any required modification to responseJSON.\n */\n\n }, {\n key: \"setStateFromJSON\",\n value: function setStateFromJSON(responseJSON) {\n this.lastTimeStamp = Date.now(); // the callback prepares the download link for all alignments\n\n if (responseJSON.user_warning == 'LARGE_RESULT') {\n this.setState({\n user_warning: responseJSON.user_warning,\n download_links: responseJSON.download_links\n });\n } else {\n this.setState(responseJSON, this.prepareAlignmentOfAllHits);\n }\n }\n /**\n * Called as soon as the page has loaded and the user sees the loading spinner.\n * We use this opportunity to setup services that make use of delegated events\n * bound to the window, document, or body.\n */\n\n }, {\n key: \"componentDidMount\",\n value: function componentDidMount() {\n this.fetchResults(); // This sets up an event handler which enables users to select text from\n // hit header without collapsing the hit.\n\n this.preventCollapseOnSelection();\n this.toggleTable();\n }\n /**\n * Called for the first time after as BLAST results have been retrieved from\n * the server and added to this.state by fetchResults. Only summary overview\n * and circos would have been rendered at this point. At this stage we kick\n * start iteratively adding 1 HSP to the page every 25 milli-seconds.\n */\n\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate() {\n var _this2 = this;\n\n // Log to console how long the last update take?\n // console.log((Date.now() - this.lastTimeStamp) / 1000);\n // Lock sidebar in its position on the first update.\n if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {\n this.affixSidebar();\n } // Queue next update if we have not rendered all results yet.\n\n\n if (this.nextQuery < this.state.queries.length) {\n // setTimeout is used to clear call stack and space out\n // the updates giving the browser a chance to respond\n // to user interactions.\n setTimeout(function () {\n return _this2.updateState();\n }, 25);\n } else {\n this.componentFinishedUpdating();\n }\n }\n /**\n * Push next slice of results to React for rendering.\n */\n\n }, {\n key: \"updateState\",\n value: function updateState() {\n var results = [];\n var numHSPsProcessed = 0;\n\n while (this.nextQuery < this.state.queries.length) {\n var query = this.state.queries[this.nextQuery]; // We may see a query multiple times during rendering because only\n // 3 hsps or are rendered in each cycle, but we want to create the\n // corresponding Query component only the first time we see it.\n\n if (this.nextHit == 0 && this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_query__WEBPACK_IMPORTED_MODULE_5__.ReportQuery, {\n query: query,\n program: this.state.program,\n querydb: this.state.querydb,\n showQueryCrumbs: this.state.queries.length > 1,\n non_parse_seqids: this.state.non_parse_seqids,\n imported_xml: this.state.imported_xml,\n veryBig: this.state.veryBig\n }, 'Query_' + query.number));\n }\n\n while (this.nextHit < query.hits.length) {\n var hit = query.hits[this.nextHit]; // We may see a hit multiple times during rendering because only\n // 10 hsps are rendered in each cycle, but we want to create the\n // corresponding Hit component only the first time we see it.\n\n if (this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hit__WEBPACK_IMPORTED_MODULE_6__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n algorithm: this.state.program,\n querydb: this.state.querydb,\n selectHit: this.selectHit,\n imported_xml: this.state.imported_xml,\n non_parse_seqids: this.state.non_parse_seqids,\n showQueryCrumbs: this.state.queries.length > 1,\n showHitCrumbs: query.hits.length > 1,\n veryBig: this.state.veryBig,\n onChange: this.prepareAlignmentOfSelectedHits\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number));\n }\n\n while (this.nextHSP < hit.hsps.length) {\n // Get nextHSP and increment the counter.\n var hsp = hit.hsps[this.nextHSP++];\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hsp__WEBPACK_IMPORTED_MODULE_7__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n hsp: hsp,\n algorithm: this.state.program,\n showHSPNumbers: hit.hsps.length > 1\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number + '_HSP_' + hsp.number));\n numHSPsProcessed++;\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hsps of a hit,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHSP == hit.hsps.length) {\n this.nextHit = this.nextHit + 1;\n this.nextHSP = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hits of a query,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHit == query.hits.length) {\n this.nextQuery = this.nextQuery + 1;\n this.nextHit = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Push the components to react for rendering.\n\n\n this.numUpdates++;\n this.lastTimeStamp = Date.now();\n this.setState({\n results: this.state.results.concat(results),\n veryBig: this.numUpdates >= 250\n });\n }\n /**\n * Called after all results have been rendered.\n */\n\n }, {\n key: \"componentFinishedUpdating\",\n value: function componentFinishedUpdating() {\n if (this.state.allQueriesLoaded) return;\n this.shouldShowIndex() && this.setupScrollSpy();\n this.setState({\n allQueriesLoaded: true\n });\n }\n /**\n * Returns loading message\n */\n\n }, {\n key: \"loadingJSX\",\n value: function loadingJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"row\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-6 col-md-offset-3 text-center\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"h1\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"i\", {\n className: \"fa fa-cog fa-spin\"\n }), \"\\xA0 BLAST-ing\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"This can take some time depending on the size of your query and database(s). The page will update automatically when BLAST is done.\", /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"You can bookmark the page and come back to it later or share the link with someone.\"]\n })]\n })\n });\n }\n /**\n * Return results JSX.\n */\n\n }, {\n key: \"resultsJSX\",\n value: function resultsJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"row\",\n id: \"results\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"col-md-3 hidden-sm hidden-xs\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_sidebar__WEBPACK_IMPORTED_MODULE_3__[\"default\"], {\n data: this.state,\n atLeastOneHit: this.atLeastOneHit(),\n shouldShowIndex: this.shouldShowIndex(),\n allQueriesLoaded: this.state.allQueriesLoaded,\n cloudSharingEnabled: this.state.cloud_sharing_enabled\n })\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-9\",\n children: [this.overviewJSX(), this.circosJSX(), this.state.results]\n })]\n });\n }\n }, {\n key: \"warningJSX\",\n value: function warningJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"container\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"row\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-6 col-md-offset-3 text-center\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"h1\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"i\", {\n className: \"fa fa-exclamation-triangle\"\n }), \"\\xA0 Warning\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"The BLAST result might be too large to load in the browser. If you have a powerful machine you can try loading the results anyway. Otherwise, you can download the results and view them locally.\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"p\", {\n children: this.state.download_links.map(function (link, index) {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"a\", {\n href: link.url,\n className: \"btn btn-secondary\",\n children: link.name\n }, 'download_link_' + index);\n })\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"p\", {\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"a\", {\n href: location.pathname + '?bypass_file_size_warning=true',\n className: \"btn btn-primary\",\n children: \"View results in browser anyway\"\n })\n })]\n })\n })\n });\n }\n /**\n * Renders report overview.\n */\n\n }, {\n key: \"overviewJSX\",\n value: function overviewJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"overview\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"strong\", {\n children: [\"SequenceServer \", this.state.seqserv_version]\n }), \" using\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: this.state.program_version\n }), this.state.submitted_at && \", query submitted on \".concat(this.state.submitted_at)]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \" Databases: \"\n }), this.state.querydb.map(function (db) {\n return db.title;\n }).join(', '), ' ', \"(\", this.state.stats.nsequences, \" sequences,\\xA0\", this.state.stats.ncharacters, \" characters)\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \"Parameters: \"\n }), ' ', underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].map(this.state.params, function (val, key) {\n return key + ' ' + val;\n }).join(', ')]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [\"Please cite:\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"a\", {\n href: \"https://doi.org/10.1093/molbev/msz185\",\n children: \"https://doi.org/10.1093/molbev/msz185\"\n })]\n })]\n });\n }\n /**\n * Return JSX for circos if we have at least one hit.\n */\n\n }, {\n key: \"circosJSX\",\n value: function circosJSX() {\n return this.atLeastTwoHits() ? /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_circos__WEBPACK_IMPORTED_MODULE_4__[\"default\"], {\n queries: this.state.queries,\n program: this.state.program,\n collapsed: \"true\"\n }) : /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"span\", {});\n } // Controller //\n\n /**\n * Returns true if results have been fetched.\n *\n * A holding message is shown till results are fetched.\n */\n\n }, {\n key: \"isResultAvailable\",\n value: function isResultAvailable() {\n return this.state.queries.length >= 1;\n }\n /**\n * Indicates the response contains a warning message for the user\n * in which case we should not render the results and render the\n * warning instead.\n **/\n\n }, {\n key: \"isUserWarningPresent\",\n value: function isUserWarningPresent() {\n return this.state.user_warning;\n }\n /**\n * Returns true if we have at least one hit.\n */\n\n }, {\n key: \"atLeastOneHit\",\n value: function atLeastOneHit() {\n return this.state.queries.some(function (query) {\n return query.hits.length > 0;\n });\n }\n /**\n * Does the report have at least two hits? This is used to determine\n * whether Circos should be enabled or not.\n */\n\n }, {\n key: \"atLeastTwoHits\",\n value: function atLeastTwoHits() {\n var hit_num = 0;\n return this.state.queries.some(function (query) {\n hit_num += query.hits.length;\n return hit_num > 1;\n });\n }\n /**\n * Returns true if index should be shown in the sidebar. Index is shown\n * only for 2 and 8 queries.\n */\n\n }, {\n key: \"shouldShowIndex\",\n value: function shouldShowIndex() {\n var num_queries = this.state.queries.length;\n return num_queries >= 2 && num_queries <= 12;\n }\n /**\n * Prevents folding of hits during text-selection.\n */\n\n }, {\n key: \"preventCollapseOnSelection\",\n value: function preventCollapseOnSelection() {\n $('body').on('mousedown', '.hit > .section-header > h4', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n if (event.type === 'mouseup') {\n // user wants to toggle\n var hitID = $this.parents('.hit').attr('id');\n $(\"div[data-parent-hit=\".concat(hitID, \"]\")).toggle();\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n } else {\n // user wants to select\n $this.attr('data-toggle', '');\n }\n\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /* Handling the fa icon when Hit Table is collapsed */\n\n }, {\n key: \"toggleTable\",\n value: function toggleTable() {\n $('body').on('mousedown', '.resultn > .section-content > .table-hit-overview > .caption', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /**\n * Affixes the sidebar.\n */\n\n }, {\n key: \"affixSidebar\",\n value: function affixSidebar() {\n var $sidebar = $('.sidebar');\n var sidebarOffset = $sidebar.offset();\n\n if (sidebarOffset) {\n $sidebar.affix({\n offset: {\n top: sidebarOffset.top\n }\n });\n }\n }\n /**\n * For the query in viewport, highlights corresponding entry in the index.\n */\n\n }, {\n key: \"setupScrollSpy\",\n value: function setupScrollSpy() {\n $('body').scrollspy({\n target: '.sidebar'\n });\n }\n /**\n * Event-handler when hit is selected\n * Adds glow to hit component.\n * Updates number of Fasta that can be downloaded\n */\n\n }, {\n key: \"selectHit\",\n value: function selectHit(id) {\n var checkbox = $('#' + id);\n var num_checked = $('.hit-links :checkbox:checked').length;\n\n if (!checkbox || !checkbox.val()) {\n return;\n }\n\n var $hit = $(checkbox.data('target')); // Highlight selected hit and enable 'Download FASTA/Alignment of\n // selected' links.\n\n if (checkbox.is(':checked')) {\n $hit.addClass('glow');\n $hit.next('.hsp').addClass('glow');\n $('.download-fasta-of-selected').enable();\n $('.download-alignment-of-selected').enable();\n } else {\n $hit.removeClass('glow');\n $hit.next('.hsp').removeClass('glow');\n }\n\n var $a = $('.download-fasta-of-selected');\n var $b = $('.download-alignment-of-selected');\n\n if (num_checked >= 1) {\n $a.find('.text-bold').html(num_checked);\n $b.find('.text-bold').html(num_checked);\n }\n\n if (num_checked == 0) {\n $a.addClass('disabled').find('.text-bold').html('');\n $b.addClass('disabled').find('.text-bold').html('');\n }\n }\n }, {\n key: \"populate_hsp_array\",\n value: function populate_hsp_array(hit, query_id) {\n return hit.hsps.map(function (hsp) {\n return Object.assign(hsp, {\n hit_id: hit.id,\n query_id: query_id\n });\n });\n }\n }, {\n key: \"prepareAlignmentOfSelectedHits\",\n value: function prepareAlignmentOfSelectedHits() {\n var sequence_ids = $('.hit-links :checkbox:checked').map(function () {\n return this.value;\n }).get();\n\n if (!sequence_ids.length) {\n // remove attributes from link if sequence_ids array is empty\n $('.download-alignment-of-selected').attr('href', '#').removeAttr('download');\n return;\n }\n\n if (this.state.alignment_blob_url) {\n // always revoke existing url if any because this method will always create a new url\n window.URL.revokeObjectURL(this.state.alignment_blob_url);\n }\n\n var hsps_arr = [];\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var self = this;\n\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(this.state.queries, underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].bind(function (query) {\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(query.hits, function (hit) {\n if (underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].indexOf(sequence_ids, hit.id) != -1) {\n hsps_arr = hsps_arr.concat(self.populate_hsp_array(hit, query.id));\n }\n });\n }, this));\n\n var filename = 'alignment-' + sequence_ids.length + '_hits.txt';\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename); // set required download attributes for link\n\n $('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename); // track new url for future removal\n\n this.setState({\n alignment_blob_url: blob_url\n });\n }\n }, {\n key: \"prepareAlignmentOfAllHits\",\n value: function prepareAlignmentOfAllHits() {\n var _this3 = this;\n\n // Get number of hits and array of all hsps.\n var num_hits = 0;\n var hsps_arr = [];\n\n if (!this.state.queries.length) {\n return;\n }\n\n this.state.queries.forEach(function (query) {\n return query.hits.forEach(function (hit) {\n num_hits++;\n hsps_arr = hsps_arr.concat(_this3.populate_hsp_array(hit, query.id));\n });\n });\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var file_name = \"alignment-\".concat(num_hits, \"_hits.txt\");\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, file_name);\n $('.download-alignment-of-all').attr('href', blob_url).attr('download', file_name);\n return false;\n }\n }, {\n key: \"render\",\n value: function render() {\n if (this.isUserWarningPresent()) {\n return this.warningJSX();\n } else if (this.isResultAvailable()) {\n return this.resultsJSX();\n } else {\n return this.loadingJSX();\n }\n }\n }]);\n\n return Report;\n}(react__WEBPACK_IMPORTED_MODULE_1__.Component);\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Report);\n\n//# sourceURL=webpack://SequenceServer/./public/js/report.js?");
185
185
 
186
186
  /***/ }),
187
187
 
@@ -269,7 +269,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
269
269
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
270
270
 
271
271
  "use strict";
272
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"get_colors_for_evalue\": () => (/* binding */ get_colors_for_evalue),\n/* harmony export */ \"get_seq_type\": () => (/* binding */ get_seq_type),\n/* harmony export */ \"prettify_evalue\": () => (/* binding */ prettify_evalue),\n/* harmony export */ \"tick_formatter\": () => (/* binding */ tick_formatter),\n/* harmony export */ \"toLetters\": () => (/* binding */ toLetters)\n/* harmony export */ });\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n\nfunction get_colors_for_evalue(evalue, hits) {\n var colors = d3.scale.log().domain([d3.min([1e-5, d3.min(hits.map(function (d) {\n if (parseFloat(d.evalue) === 0.0) return undefined;\n return d.evalue;\n }))]), d3.max(hits.map(function (d) {\n return d.evalue;\n }))]).range([0, 0.8]);\n var rgb = colors(evalue);\n return d3.hsl(20, 0.82, rgb);\n}\nfunction toLetters(num) {\n var mod = num % 26,\n pow = num / 26 | 0,\n out = mod ? String.fromCharCode(96 + mod) : (--pow, 'z');\n return pow ? toLetters(pow) + out : out;\n}\n/**\n * Defines how ticks will be formatted.\n *\n * Examples: 200 aa, 2.4 kbp, 7.6 Mbp.\n *\n * Borrowed from Kablammo. Modified by Priyam based on https://github.com/mbostock/d3/issues/1722.\n */\n\nfunction tick_formatter(scale, seq_type) {\n var ticks = scale.ticks();\n var prefix = d3.formatPrefix(ticks[ticks.length - 1]);\n var suffixes = {\n amino_acid: 'aa',\n nucleic_acid: 'bp'\n };\n var digits = 0;\n var format;\n\n var _ticks;\n\n do {\n format = d3.format('.' + digits + 'f');\n _ticks = scale.ticks().map(function (d) {\n return format(prefix.scale(d));\n });\n digits++;\n } while (_ticks.length !== underscore__WEBPACK_IMPORTED_MODULE_0__[\"default\"].uniq(_ticks).length);\n\n return function (d) {\n if (!prefix.symbol || d === scale.domain()[0]) {\n return d + ' ' + suffixes[seq_type];\n } else {\n return format(prefix.scale(d)) + ' ' + prefix.symbol + suffixes[seq_type];\n }\n };\n}\nfunction get_seq_type(algorithm) {\n var SEQ_TYPES = {\n blastn: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n blastp: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'amino_acid'\n },\n blastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'amino_acid'\n },\n tblastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n tblastn: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'nucleic_acid'\n }\n };\n return SEQ_TYPES[algorithm];\n}\nfunction prettify_evalue(evalue) {\n var matches = evalue.toString().split('e');\n var base = matches[0];\n var power = matches[1];\n\n if (power) {\n var s = parseFloat(base).toFixed(2);\n var element = '<span>' + s + ' &times; 10<sup>' + power + '</sup></span>';\n return element;\n } else {\n if (!(base % 1 == 0)) return parseFloat(base).toFixed(2);else return base;\n }\n}\n\n//# sourceURL=webpack://SequenceServer/./public/js/visualisation_helpers.js?");
272
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"get_colors_for_evalue\": () => (/* binding */ get_colors_for_evalue),\n/* harmony export */ \"get_seq_type\": () => (/* binding */ get_seq_type),\n/* harmony export */ \"prettify_evalue\": () => (/* binding */ prettify_evalue),\n/* harmony export */ \"tick_formatter\": () => (/* binding */ tick_formatter),\n/* harmony export */ \"toLetters\": () => (/* binding */ toLetters)\n/* harmony export */ });\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n/* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! d3 */ \"./node_modules/d3/d3.js\");\n/* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(d3__WEBPACK_IMPORTED_MODULE_1__);\n\n\nfunction get_colors_for_evalue(evalue, hits) {\n var colors = d3__WEBPACK_IMPORTED_MODULE_1___default().scale.log().domain([d3__WEBPACK_IMPORTED_MODULE_1___default().min([1e-5, d3__WEBPACK_IMPORTED_MODULE_1___default().min(hits.map(function (d) {\n if (parseFloat(d.evalue) === 0.0) return undefined;\n return d.evalue;\n }))]), d3__WEBPACK_IMPORTED_MODULE_1___default().max(hits.map(function (d) {\n return d.evalue;\n }))]).range([0, 0.8]);\n var rgb = colors(evalue);\n return d3__WEBPACK_IMPORTED_MODULE_1___default().hsl(20, 0.82, rgb);\n}\nfunction toLetters(num) {\n var mod = num % 26,\n pow = num / 26 | 0,\n out = mod ? String.fromCharCode(96 + mod) : (--pow, 'z');\n return pow ? toLetters(pow) + out : out;\n}\n/**\n * Defines how ticks will be formatted.\n *\n * Examples: 200 aa, 2.4 kbp, 7.6 Mbp.\n *\n * Borrowed from Kablammo. Modified by Priyam based on https://github.com/mbostock/d3/issues/1722.\n */\n\nfunction tick_formatter(scale, seq_type) {\n var ticks = scale.ticks();\n var prefix = d3__WEBPACK_IMPORTED_MODULE_1___default().formatPrefix(ticks[ticks.length - 1]);\n var suffixes = {\n amino_acid: 'aa',\n nucleic_acid: 'bp'\n };\n var digits = 0;\n var format;\n\n var _ticks;\n\n do {\n format = d3__WEBPACK_IMPORTED_MODULE_1___default().format('.' + digits + 'f');\n _ticks = scale.ticks().map(function (d) {\n return format(prefix.scale(d));\n });\n digits++;\n } while (_ticks.length !== underscore__WEBPACK_IMPORTED_MODULE_0__[\"default\"].uniq(_ticks).length);\n\n return function (d) {\n if (!prefix.symbol || d === scale.domain()[0]) {\n return d + ' ' + suffixes[seq_type];\n } else {\n return format(prefix.scale(d)) + ' ' + prefix.symbol + suffixes[seq_type];\n }\n };\n}\nfunction get_seq_type(algorithm) {\n var SEQ_TYPES = {\n blastn: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n blastp: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'amino_acid'\n },\n blastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'amino_acid'\n },\n tblastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n tblastn: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'nucleic_acid'\n }\n };\n return SEQ_TYPES[algorithm];\n}\nfunction prettify_evalue(evalue) {\n var matches = evalue.toString().split('e');\n var base = matches[0];\n var power = matches[1];\n\n if (power) {\n var s = parseFloat(base).toFixed(2);\n var element = '<span>' + s + ' &times; 10<sup>' + power + '</sup></span>';\n return element;\n } else {\n if (!(base % 1 == 0)) return parseFloat(base).toFixed(2);else return base;\n }\n}\n\n//# sourceURL=webpack://SequenceServer/./public/js/visualisation_helpers.js?");
273
273
 
274
274
  /***/ }),
275
275
 
@@ -181,7 +181,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
181
181
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
182
182
 
183
183
  "use strict";
184
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"get_colors_for_evalue\": () => (/* binding */ get_colors_for_evalue),\n/* harmony export */ \"get_seq_type\": () => (/* binding */ get_seq_type),\n/* harmony export */ \"prettify_evalue\": () => (/* binding */ prettify_evalue),\n/* harmony export */ \"tick_formatter\": () => (/* binding */ tick_formatter),\n/* harmony export */ \"toLetters\": () => (/* binding */ toLetters)\n/* harmony export */ });\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n\nfunction get_colors_for_evalue(evalue, hits) {\n var colors = d3.scale.log().domain([d3.min([1e-5, d3.min(hits.map(function (d) {\n if (parseFloat(d.evalue) === 0.0) return undefined;\n return d.evalue;\n }))]), d3.max(hits.map(function (d) {\n return d.evalue;\n }))]).range([0, 0.8]);\n var rgb = colors(evalue);\n return d3.hsl(20, 0.82, rgb);\n}\nfunction toLetters(num) {\n var mod = num % 26,\n pow = num / 26 | 0,\n out = mod ? String.fromCharCode(96 + mod) : (--pow, 'z');\n return pow ? toLetters(pow) + out : out;\n}\n/**\n * Defines how ticks will be formatted.\n *\n * Examples: 200 aa, 2.4 kbp, 7.6 Mbp.\n *\n * Borrowed from Kablammo. Modified by Priyam based on https://github.com/mbostock/d3/issues/1722.\n */\n\nfunction tick_formatter(scale, seq_type) {\n var ticks = scale.ticks();\n var prefix = d3.formatPrefix(ticks[ticks.length - 1]);\n var suffixes = {\n amino_acid: 'aa',\n nucleic_acid: 'bp'\n };\n var digits = 0;\n var format;\n\n var _ticks;\n\n do {\n format = d3.format('.' + digits + 'f');\n _ticks = scale.ticks().map(function (d) {\n return format(prefix.scale(d));\n });\n digits++;\n } while (_ticks.length !== underscore__WEBPACK_IMPORTED_MODULE_0__[\"default\"].uniq(_ticks).length);\n\n return function (d) {\n if (!prefix.symbol || d === scale.domain()[0]) {\n return d + ' ' + suffixes[seq_type];\n } else {\n return format(prefix.scale(d)) + ' ' + prefix.symbol + suffixes[seq_type];\n }\n };\n}\nfunction get_seq_type(algorithm) {\n var SEQ_TYPES = {\n blastn: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n blastp: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'amino_acid'\n },\n blastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'amino_acid'\n },\n tblastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n tblastn: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'nucleic_acid'\n }\n };\n return SEQ_TYPES[algorithm];\n}\nfunction prettify_evalue(evalue) {\n var matches = evalue.toString().split('e');\n var base = matches[0];\n var power = matches[1];\n\n if (power) {\n var s = parseFloat(base).toFixed(2);\n var element = '<span>' + s + ' &times; 10<sup>' + power + '</sup></span>';\n return element;\n } else {\n if (!(base % 1 == 0)) return parseFloat(base).toFixed(2);else return base;\n }\n}\n\n//# sourceURL=webpack://SequenceServer/./public/js/visualisation_helpers.js?");
184
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"get_colors_for_evalue\": () => (/* binding */ get_colors_for_evalue),\n/* harmony export */ \"get_seq_type\": () => (/* binding */ get_seq_type),\n/* harmony export */ \"prettify_evalue\": () => (/* binding */ prettify_evalue),\n/* harmony export */ \"tick_formatter\": () => (/* binding */ tick_formatter),\n/* harmony export */ \"toLetters\": () => (/* binding */ toLetters)\n/* harmony export */ });\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n/* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! d3 */ \"./node_modules/d3/d3.js\");\n/* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(d3__WEBPACK_IMPORTED_MODULE_1__);\n\n\nfunction get_colors_for_evalue(evalue, hits) {\n var colors = d3__WEBPACK_IMPORTED_MODULE_1___default().scale.log().domain([d3__WEBPACK_IMPORTED_MODULE_1___default().min([1e-5, d3__WEBPACK_IMPORTED_MODULE_1___default().min(hits.map(function (d) {\n if (parseFloat(d.evalue) === 0.0) return undefined;\n return d.evalue;\n }))]), d3__WEBPACK_IMPORTED_MODULE_1___default().max(hits.map(function (d) {\n return d.evalue;\n }))]).range([0, 0.8]);\n var rgb = colors(evalue);\n return d3__WEBPACK_IMPORTED_MODULE_1___default().hsl(20, 0.82, rgb);\n}\nfunction toLetters(num) {\n var mod = num % 26,\n pow = num / 26 | 0,\n out = mod ? String.fromCharCode(96 + mod) : (--pow, 'z');\n return pow ? toLetters(pow) + out : out;\n}\n/**\n * Defines how ticks will be formatted.\n *\n * Examples: 200 aa, 2.4 kbp, 7.6 Mbp.\n *\n * Borrowed from Kablammo. Modified by Priyam based on https://github.com/mbostock/d3/issues/1722.\n */\n\nfunction tick_formatter(scale, seq_type) {\n var ticks = scale.ticks();\n var prefix = d3__WEBPACK_IMPORTED_MODULE_1___default().formatPrefix(ticks[ticks.length - 1]);\n var suffixes = {\n amino_acid: 'aa',\n nucleic_acid: 'bp'\n };\n var digits = 0;\n var format;\n\n var _ticks;\n\n do {\n format = d3__WEBPACK_IMPORTED_MODULE_1___default().format('.' + digits + 'f');\n _ticks = scale.ticks().map(function (d) {\n return format(prefix.scale(d));\n });\n digits++;\n } while (_ticks.length !== underscore__WEBPACK_IMPORTED_MODULE_0__[\"default\"].uniq(_ticks).length);\n\n return function (d) {\n if (!prefix.symbol || d === scale.domain()[0]) {\n return d + ' ' + suffixes[seq_type];\n } else {\n return format(prefix.scale(d)) + ' ' + prefix.symbol + suffixes[seq_type];\n }\n };\n}\nfunction get_seq_type(algorithm) {\n var SEQ_TYPES = {\n blastn: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n blastp: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'amino_acid'\n },\n blastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'amino_acid'\n },\n tblastx: {\n query_seq_type: 'nucleic_acid',\n subject_seq_type: 'nucleic_acid'\n },\n tblastn: {\n query_seq_type: 'amino_acid',\n subject_seq_type: 'nucleic_acid'\n }\n };\n return SEQ_TYPES[algorithm];\n}\nfunction prettify_evalue(evalue) {\n var matches = evalue.toString().split('e');\n var base = matches[0];\n var power = matches[1];\n\n if (power) {\n var s = parseFloat(base).toFixed(2);\n var element = '<span>' + s + ' &times; 10<sup>' + power + '</sup></span>';\n return element;\n } else {\n if (!(base % 1 == 0)) return parseFloat(base).toFixed(2);else return base;\n }\n}\n\n//# sourceURL=webpack://SequenceServer/./public/js/visualisation_helpers.js?");
185
185
 
186
186
  /***/ }),
187
187
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequenceserver
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Queen Mary University of London
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-07-28 00:00:00.000000000 Z
12
+ date: 2023-10-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json_pure
@@ -398,9 +398,11 @@ files:
398
398
  - public/js/share_url.js
399
399
  - public/js/sidebar.js
400
400
  - public/js/svgExporter.js
401
+ - public/js/tests/advanced_parameters.spec.js
401
402
  - public/js/tests/database.spec.js
402
403
  - public/js/tests/mock_data/databases.json
403
404
  - public/js/tests/mock_data/long_response.json
405
+ - public/js/tests/mock_data/sequences.js
404
406
  - public/js/tests/mock_data/short_response.json
405
407
  - public/js/tests/report.spec.js
406
408
  - public/js/tests/search_button.spec.js