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 +4 -4
- data/bin/sequenceserver +7 -2
- data/lib/sequenceserver/blast/formatter.rb +13 -4
- data/lib/sequenceserver/blast/report.rb +33 -3
- data/lib/sequenceserver/config.rb +4 -1
- data/lib/sequenceserver/makeblastdb.rb +80 -72
- data/lib/sequenceserver/pool.rb +1 -1
- data/lib/sequenceserver/report.rb +1 -5
- data/lib/sequenceserver/routes.rb +23 -2
- data/lib/sequenceserver/sys.rb +1 -1
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver.rb +8 -2
- data/public/js/report.js +60 -6
- data/public/js/tests/advanced_parameters.spec.js +36 -0
- data/public/js/tests/mock_data/sequences.js +32 -0
- data/public/js/tests/report.spec.js +62 -6
- data/public/js/tests/search_query.spec.js +36 -19
- data/public/js/visualisation_helpers.js +1 -1
- data/public/sequenceserver-report.min.js +2 -2
- data/public/sequenceserver-search.min.js +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61b15f95cdc065739de70f2ba94d3022e6beec789dd9c64199ee1be3a2459c63
|
4
|
+
data.tar.gz: d6c108bd39cb8c6a832787d2cbaa52b2c5a3653c0b08b289cb149b7221a9870c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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
|
29
|
-
@
|
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?(
|
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:
|
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
|
-
|
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
|
67
|
-
tsv_ir = parse_tsv
|
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.
|
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
|
-
|
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
|
-
|
60
|
-
|
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
|
71
|
-
#
|
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
|
84
|
-
|
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
|
95
|
-
|
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.
|
104
|
-
def
|
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.
|
115
|
-
def
|
116
|
-
@
|
117
|
-
|
118
|
-
|
119
|
-
|
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.
|
125
|
-
def
|
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
|
118
|
+
next if formatted_fastas.any? { |f| f[0] == path }
|
133
119
|
|
134
120
|
@fastas_to_format << [path,
|
135
|
-
|
136
|
-
|
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
|
-
|
137
|
+
|
138
|
+
out
|
150
139
|
rescue CommandFailed => e
|
151
|
-
|
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)
|
193
|
-
"-taxid_map #{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
|
-
|
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
|
-
|
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
|
-
!
|
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
|
-
|
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
|
|
data/lib/sequenceserver/pool.rb
CHANGED
@@ -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
|
-
|
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.
|
169
|
+
send_file out.filepath, filename: out.filename, type: out.mime
|
149
170
|
end
|
150
171
|
|
151
172
|
post '/cloud_share' do
|
data/lib/sequenceserver/sys.rb
CHANGED
@@ -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.
|
70
|
+
FileUtils.cp(temp_files[channel], filename)
|
71
71
|
end
|
72
72
|
|
73
73
|
# Read the remaining temp files into memory. For large outputs,
|
data/lib/sequenceserver.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
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> 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
|
-
|
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
|
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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
});
|
@@ -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 =
|
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 + ' × 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 =
|
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 + ' × 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.
|
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-
|
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
|