Merge branch '16104-select-uuid'
[arvados.git] / apps / workbench / app / controllers / pipeline_instances_controller.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class PipelineInstancesController < ApplicationController
6   skip_before_action :find_object_by_uuid, only: :compare
7   before_action :find_objects_by_uuid, only: :compare
8   skip_around_action :require_thread_api_token, if: proc { |ctrl|
9     !Rails.configuration.Users.AnonymousUserToken.empty? and
10     'show' == ctrl.action_name
11   }
12
13   include PipelineInstancesHelper
14   include PipelineComponentsHelper
15
16   def copy
17     template = PipelineTemplate.find?(@object.pipeline_template_uuid)
18
19     source = @object
20     @object = PipelineInstance.new
21     @object.pipeline_template_uuid = source.pipeline_template_uuid
22
23     if params['components'] == 'use_latest' and template
24       @object.components = template.components.deep_dup
25       @object.components.each do |cname, component|
26         # Go through the script parameters of each component
27         # that are marked as user input and copy them over.
28         # Skip any components that are not present in the
29         # source instance (there's nothing to copy)
30         if source.components.include? cname
31           component[:script_parameters].each do |pname, val|
32             if val.is_a? Hash and val[:dataclass]
33               # this is user-inputtable, so check the value from the source pipeline
34               srcvalue = source.components[cname][:script_parameters][pname]
35               if not srcvalue.nil?
36                 component[:script_parameters][pname] = srcvalue
37               end
38             end
39           end
40         end
41       end
42     else
43       @object.components = source.components.deep_dup
44     end
45
46     if params['script'] == 'use_same'
47       # Go through each component and copy the script_version from each job.
48       @object.components.each do |cname, component|
49         if source.components.include? cname and source.components[cname][:job]
50           component[:script_version] = source.components[cname][:job][:script_version]
51         end
52       end
53     end
54
55     @object.components.each do |cname, component|
56       component.delete :job
57     end
58     @object.state = 'New'
59
60     # set owner_uuid to that of source, provided it is a project and writable by current user
61     current_project = Group.find(source.owner_uuid) rescue nil
62     if (current_project && current_project.writable_by.andand.include?(current_user.uuid))
63       @object.owner_uuid = source.owner_uuid
64     end
65
66     super
67   end
68
69   def update
70     @updates ||= params.to_unsafe_hash[@object.class.to_s.underscore.singularize.to_sym]
71     if (components = @updates[:components])
72       components.each do |cname, component|
73         if component[:script_parameters]
74           component[:script_parameters].each do |param, value_info|
75             if value_info.is_a? Hash
76               value_info_partitioned = value_info[:value].partition('/') if value_info[:value].andand.class.eql?(String)
77               value_info_value = value_info_partitioned ? value_info_partitioned[0] : value_info[:value]
78               value_info_class = resource_class_for_uuid value_info_value
79               if value_info_class == Link
80                 # Use the link target, not the link itself, as script
81                 # parameter; but keep the link info around as well.
82                 link = Link.find value_info[:value]
83                 value_info[:value] = link.head_uuid
84                 value_info[:link_uuid] = link.uuid
85                 value_info[:link_name] = link.name
86               else
87                 # Delete stale link_uuid and link_name data.
88                 value_info[:link_uuid] = nil
89                 value_info[:link_name] = nil
90               end
91               if value_info_class == Collection
92                 # to ensure reproducibility, the script_parameter for a
93                 # collection should be the portable_data_hash
94                 # keep the collection name and uuid for human-readability
95                 obj = Collection.find value_info_value
96                 if value_info_partitioned
97                   value_info[:value] = obj.portable_data_hash + value_info_partitioned[1] + value_info_partitioned[2]
98                   value_info[:selection_name] = obj.name ? obj.name + value_info_partitioned[1] + value_info_partitioned[2] : obj.name
99                 else
100                   value_info[:value] = obj.portable_data_hash
101                   value_info[:selection_name] = obj.name
102                 end
103                 value_info[:selection_uuid] = obj.uuid
104               end
105             end
106           end
107         end
108       end
109     end
110     super
111   end
112
113   def graph(pipelines)
114     return nil, nil if params['tab_pane'] != "Graph"
115
116     provenance = {}
117     pips = {}
118     n = 1
119
120     # When comparing more than one pipeline, "pips" stores bit fields that
121     # indicates which objects are part of which pipelines.
122
123     pipelines.each do |p|
124       collections = []
125       hashes = []
126       jobs = []
127
128       p[:components].each do |k, v|
129         provenance["component_#{p[:uuid]}_#{k}"] = v
130
131         collections << v[:output_uuid] if v[:output_uuid]
132         jobs << v[:job][:uuid] if v[:job]
133       end
134
135       jobs = jobs.compact.uniq
136       if jobs.any?
137         Job.where(uuid: jobs).with_count("none").each do |j|
138           job_uuid = j.uuid
139
140           provenance[job_uuid] = j
141           pips[job_uuid] = 0 unless pips[job_uuid] != nil
142           pips[job_uuid] |= n
143
144           hashes << j[:output] if j[:output]
145           ProvenanceHelper::find_collections(j) do |hash, uuid|
146             collections << uuid if uuid
147             hashes << hash if hash
148           end
149
150           if j[:script_version]
151             script_uuid = j[:script_version]
152             provenance[script_uuid] = {:uuid => script_uuid}
153             pips[script_uuid] = 0 unless pips[script_uuid] != nil
154             pips[script_uuid] |= n
155           end
156         end
157       end
158
159       hashes = hashes.compact.uniq
160       if hashes.any?
161         Collection.where(portable_data_hash: hashes).with_count("none").each do |c|
162           hash_uuid = c.portable_data_hash
163           provenance[hash_uuid] = c
164           pips[hash_uuid] = 0 unless pips[hash_uuid] != nil
165           pips[hash_uuid] |= n
166         end
167       end
168
169       collections = collections.compact.uniq
170       if collections.any?
171         Collection.where(uuid: collections).with_count("none").each do |c|
172           collection_uuid = c.uuid
173           provenance[collection_uuid] = c
174           pips[collection_uuid] = 0 unless pips[collection_uuid] != nil
175           pips[collection_uuid] |= n
176         end
177       end
178
179       n = n << 1
180     end
181
182     return provenance, pips
183   end
184
185   def show
186     # the #show action can also be called by #compare, which does its own work to set up @pipelines
187     unless defined? @pipelines
188       @pipelines = [@object]
189     end
190
191     provenance, pips = graph(@pipelines)
192     if provenance
193       @prov_svg = ProvenanceHelper::create_provenance_graph provenance, "provenance_svg", {
194         :request => request,
195         :all_script_parameters => true,
196         :combine_jobs => :script_and_version,
197         :pips => pips,
198         :only_components => true,
199         :no_docker => true,
200         :no_log => true}
201     end
202
203     super
204   end
205
206   def compare
207     @breadcrumb_page_name = 'compare'
208
209     @rows = []          # each is {name: S, components: [...]}
210
211     if params['tab_pane'] == "Compare" or params['tab_pane'].nil?
212       # Build a table: x=pipeline y=component
213       @objects.each_with_index do |pi, pi_index|
214         pipeline_jobs(pi).each do |component|
215           # Find a cell with the same name as this component but no
216           # entry for this pipeline
217           target_row = nil
218           @rows.each_with_index do |row, row_index|
219             if row[:name] == component[:name] and !row[:components][pi_index]
220               target_row = row
221             end
222           end
223           if !target_row
224             target_row = {name: component[:name], components: []}
225             @rows << target_row
226           end
227           target_row[:components][pi_index] = component
228         end
229       end
230
231       @rows.each do |row|
232         # Build a "normal" pseudo-component for this row by picking the
233         # most common value for each attribute. If all values are
234         # equally common, there is no "normal".
235         normal = {}              # attr => most common value
236         highscore = {}           # attr => how common "normal" is
237         score = {}               # attr => { value => how common }
238         row[:components].each do |pj|
239           next if pj.nil?
240           pj.each do |k,v|
241             vstr = for_comparison v
242             score[k] ||= {}
243             score[k][vstr] = (score[k][vstr] || 0) + 1
244             highscore[k] ||= 0
245             if score[k][vstr] == highscore[k]
246               # tie for first place = no "normal"
247               normal.delete k
248             elsif score[k][vstr] == highscore[k] + 1
249               # more pipelines have v than anything else
250               highscore[k] = score[k][vstr]
251               normal[k] = vstr
252             end
253           end
254         end
255
256         # Add a hash in component[:is_normal]: { attr => is_the_value_normal? }
257         row[:components].each do |pj|
258           next if pj.nil?
259           pj[:is_normal] = {}
260           pj.each do |k,v|
261             pj[:is_normal][k] = (normal.has_key?(k) && normal[k] == for_comparison(v))
262           end
263         end
264       end
265     end
266
267     if params['tab_pane'] == "Graph"
268       @pipelines = @objects
269     end
270
271     @object = @objects.first
272
273     show
274   end
275
276   def show_pane_list
277     panes = %w(Components Log Graph Advanced)
278     if @object and @object.state.in? ['New', 'Ready']
279       panes = %w(Inputs) + panes - %w(Log)
280     end
281     if not @object.components.values.any? { |x| x[:job] rescue false }
282       panes -= ['Graph']
283     end
284     panes
285   end
286
287   def compare_pane_list
288     %w(Compare Graph)
289   end
290
291   helper_method :unreadable_inputs_present?
292   def unreadable_inputs_present?
293     unless @unreadable_inputs_present.nil?
294       return @unreadable_inputs_present
295     end
296
297     input_uuids = []
298     input_pdhs = []
299     @object.components.each do |k, component|
300       next if !component
301       component[:script_parameters].andand.each do |p, tv|
302         if (tv.is_a? Hash) and ((tv[:dataclass] == "Collection") || (tv[:dataclass] == "File"))
303           if tv[:value]
304             value = tv[:value]
305           elsif tv[:default]
306             value = tv[:default]
307           else
308             value = ''
309           end
310           if value.present?
311             split = value.split '/'
312             if CollectionsHelper.match(split[0])
313               input_pdhs << split[0]
314             else
315               input_uuids << split[0]
316             end
317           end
318         end
319       end
320     end
321
322     input_pdhs = input_pdhs.uniq
323     input_uuids = input_uuids.uniq
324
325     preload_collections_for_objects input_uuids if input_uuids.any?
326     preload_for_pdhs input_pdhs if input_pdhs.any?
327
328     @unreadable_inputs_present = false
329     input_uuids.each do |uuid|
330       if !collections_for_object(uuid).any?
331         @unreadable_inputs_present = true
332         break
333       end
334     end
335     if !@unreadable_inputs_present
336       input_pdhs.each do |pdh|
337         if !collection_for_pdh(pdh).any?
338           @unreadable_inputs_present = true
339           break
340         end
341       end
342     end
343
344     @unreadable_inputs_present
345   end
346
347   def cancel
348     @object.cancel
349     if params[:return_to]
350       redirect_to params[:return_to]
351     else
352       redirect_to @object
353     end
354   end
355
356   protected
357   def for_comparison v
358     if v.is_a? Hash or v.is_a? Array
359       v.to_json
360     else
361       v.to_s
362     end
363   end
364
365   def load_filters_and_paging_params
366     params[:limit] = 20
367     super
368   end
369
370   def find_objects_by_uuid
371     @objects = model_class.where(uuid: params[:uuids])
372   end
373 end