sequenceserver 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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