sequenceserver 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sequenceserver might be problematic. Click here for more details.
- checksums.yaml +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
|