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