Project

General

Profile

Hacking Workbench » History » Version 21

Phil Hodgson, 11/28/2014 11:01 AM

1 1 Tom Clegg
h1. Hacking Workbench
2
3
{{toc}}
4
5
h2. Source tree layout
6
7
Everything is in @/apps/workbench@.
8
9
Key pieces to know about before going much further:
10
11
|/|Usual Rails project layout|
12
|/app/controllers/application_controller.rb|Controller superclass with authentication setup, error handling, and generic CRUD actions|
13
|/app/controllers/*.rb|Actions other than generic CRUD (users#activity, jobs#generate_provenance, ...)|
14
|/app/models/arvados_base.rb|Default Arvados model behavior and ActiveRecord-like accessors and introspection features|
15
|/app/models/arvados_resource_list.rb|ActiveRelation-like class (what you get from Model.where() etc.)|
16
17 12 Tom Clegg
h2. Background resources
18
19
Workbench is a Rails 4 application.
20
* "Getting started with Rails":http://guides.rubyonrails.org/getting_started.html at rubyonrails.org
21 1 Tom Clegg
* "AJAX in Rails 3.1":http://blog.madebydna.com/all/code/2011/12/05/ajax-in-rails-3.html blog post (still relevant in Rails 4)
22 20 Tom Clegg
23
Javascript readings
24
* http://javascript.crockford.com/code.html
25
* http://javascript.crockford.com/private.html
26
27
Angular readings
28
* http://www.ng-newsletter.com/posts/beginner2expert-how_to_start.html
29
* https://docs.angularjs.org/guide/introduction
30
* https://github.com/johnpapa/angularjs-styleguide
31 12 Tom Clegg
32 1 Tom Clegg
h2. Unlike a typical Rails project...
33
34 7 Peter Amstutz
* ActiveRecord in Workbench doesn't talk to the database directly, but instead queries the Arvados API as REST client.
35
* The Arvados query API is somewhat limited and doesn't accept SQL statements, so Workbench has to work harder to get what it needs.
36 1 Tom Clegg
* Workbench itself only has the privileges of the Workbench user: when making Arvados API calls, it uses the API token provided by the user.
37
38
h2. Unlike what you might expect...
39
40
* Workbench doesn't use the Ruby SDK. It uses a sort of baked-in Rails SDK.
41
** TODO: move it out of Workbench into a gem.
42
** TODO: use the Ruby SDK under the hood.
43
44
h2. Running in development mode
45
46 2 Misha Zatsman
h3. SSL certificates
47
48 4 Tom Clegg
You can get started quickly with SSL by generating a self-signed certificate:
49 1 Tom Clegg
50
 openssl req -new -x509 -nodes -out ~/self-signed.pem -keyout ~/self-signed.key -days 3650 -subj '/CN=arvados.example.com'
51
52
Alternatively, download a set from the bottom of the [[API server]] page.
53 2 Misha Zatsman
54
h3. Download and configure
55 1 Tom Clegg
56 2 Misha Zatsman
Follow "these instructions":http://doc.arvados.org/install/install-workbench-app.html to download the source and configure your workbench instance.
57 3 Misha Zatsman
58 4 Tom Clegg
h3. Start the server
59 1 Tom Clegg
60 4 Tom Clegg
Save something like the following at @~/bin/workbench@, make it executable[1], make sure @~/bin@ is in your path[2]:
61 1 Tom Clegg
62
 #!/bin/sh
63
set -e
64
cd ~/arvados/apps/workbench
65
export RAILS_ENV=development
66 5 Tom Clegg
bundle install --path=vendor/bundle
67 4 Tom Clegg
exec bundle exec passenger start -p 3031 --ssl --ssl-certificate ~/self-signed.pem --ssl-certificate-key ~/self-signed.key
68 1 Tom Clegg
69
The first time you run the above it will take a while to install all the ruby gems. In particular @Installing nokogiri@ takes a while
70
71
Once you see:
72
73
 =============== Phusion Passenger Standalone web server started ===============
74
75
You can visit your server at:
76
77 4 Tom Clegg
 @https://{ip-or-host}:3031/@
78
79 6 Misha Zatsman
You can kill your server with @ctrl-C@ but if you get disconnected from the terminal, it will continue running. You can kill it by running
80
81
 @ps x |grep nginx |grep master@
82
83
And then
84
85
 @kill ####@
86
87
Replacing #### with the number in the left column returned by ps
88
89 4 Tom Clegg
fn1. @chmod +x ~/bin/workbench@
90
91
fn2. In Debian systems, the default .profile adds ~/bin to your path, but only if it exists when you log in. If you just created ~/bin, doing @exec bash -login@ or @source .profile@ should make ~/bin appear in your path.
92
93
h2. Running tests
94
95
The test suite brings up an API server in test mode, and runs browser tests with Firefox.
96
97 17 Tom Clegg
Make sure API server has its dependencies in place and its database schema up-to-date.
98 4 Tom Clegg
99 1 Tom Clegg
<pre>
100 17 Tom Clegg
(
101
 set -e
102
 cd ../../services/api
103
 RAILS_ENV=test bundle install --path=vendor/bundle
104
 RAILS_ENV=test bundle exec rake db:migrate
105
)
106 4 Tom Clegg
</pre>
107
108
Install headless testing tools.
109
110
<pre>
111
sudo apt-get install xvfb iceweasel
112
</pre>
113
114
(Install firefox instead of iceweasel if you're not using Debian.)
115
116 10 Tom Clegg
Install phantomjs. (See http://phantomjs.org/download.html for latest version.)
117
118
<pre>
119 16 Tom Clegg
wget -P /tmp https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.8-linux-x86_64.tar.bz2
120
sudo tar -C /usr/local -xjf /tmp/phantomjs-1.9.8-linux-x86_64.tar.bz2
121
sudo ln -s ../phantomjs-1.9.8-linux-x86_64/bin/phantomjs /usr/local/bin/
122 10 Tom Clegg
</pre>
123
124 4 Tom Clegg
Run the test suite.
125
126
<pre>
127
RAILS_ENV=test bundle exec rake test
128 1 Tom Clegg
</pre>
129 9 Tom Clegg
130 13 Tom Clegg
h3. When tests fail...
131
132
When an integration test fails (or skips) a screenshot is automatically saved in @arvados/apps/workbench/tmp/workbench-fail-1.png@, etc.
133
134 16 Tom Clegg
By default, @rake test@ just shows F when a test fails (and E when a test crashes) and doesn't tell you which tests had problems until the entire test suite is done. During development it makes more sense to use @TESTOPTS=-v@. This reports after each test the test class and name, outcome, and elapsed time:
135 13 Tom Clegg
* <pre>
136 1 Tom Clegg
$ RAILS_ENV=test bundle exec rake test TESTOPTS=-v
137 13 Tom Clegg
[...]
138
ApplicationControllerTest#test_links_for_object = 0.10 s = .
139
[...]
140
Saved ./tmp/workbench-fail-2.png
141 16 Tom Clegg
CollectionsTest#test_combine_selected_collection_files_into_new_collection = 10.89 s = F
142 13 Tom Clegg
[...]
143
</pre>
144 9 Tom Clegg
145 19 Tom Clegg
h3. Iterating on a single test
146
147
Sometimes you want to poke at the code and re-run a single test to confirm that you made it pass. You don't want to reboot everything just to make Minitest notice that you edited your test.
148
149
Here's how:
150
151
<pre>
152
arvados/apps/workbench$ RAILS_ENV=test bundle exec irb -Ilib:test
153
154
load 'test/integration/pipeline_instances_test.rb'
155
156
>>> Minitest.run(%w(-v -n test_Create_and_run_a_pipeline))
157
158
...
159
PipelineInstancesTest#test_Create_and_run_a_pipeline = 14.72 s = E
160
...
161
162
(Edit test/integration/pipeline_instances_test.rb here)
163
164
>>> begin
165
      Object.send :remove_const, :PipelineInstancesTest
166
      ::Minitest::Runnable.runnables.reject! { true }
167
      load 'test/integration/pipeline_instances_test.rb'
168
      Minitest.run(%w(-v -n test_Create_and_run_a_pipeline))
169
    end
170
171
...
172
PipelineInstancesTest#test_Create_and_run_a_pipeline = 38.54 s = .
173
...
174
</pre>
175
176
177 1 Tom Clegg
h2. Loading state from API into models
178
179
If your model makes an API call that returns the new state of an object, load the new attributes into the local model with @private_reload@:
180
181
<pre><code class="ruby">
182
  api_response = $arvados_api_client.api(...)
183
  private_reload api_response
184
</code></pre>
185
186
h2. Features
187
188
h3. Authentication
189
190
ApplicationController uses an around_filter to make sure the user is logged in, redirect to Arvados to complete the login procedure if not, and store the user's API token in Thread.current[:arvados_api_token] if so.
191
192
The @current_user@ helper returns User.current if the user is logged in, otherwise nil. (Generally, only special pages like "welcome" and "error" get displayed to users who aren't logged in.)
193
194
h3. Default filter behavior
195
196
@before_filter :find_object_by_uuid@
197
198
* This is enabled by default, @except :index, :create@.
199
* It renames the @:id@ param to @:uuid@. (The Rails default routing rules use @:id@ to accept params in path components, but @params[:uuid]@ makes more sense everywhere else in our code.)
200
* If you define a collection method (where there's no point looking up an object with the :id supplied in the request), skip this.
201
202
<pre><code class="ruby">
203
  skip_before_filter :find_object_by_uuid, only: [:action_that_takes_no_uuid_param]
204
</code></pre>
205
206
h3. Error handling
207
208
ApplicationController has a render_error method that shows a standard error page. (It's not very good, but it's better than a default Rails stack trace.)
209
210
In a controller you get there like this
211
212
<pre><code class="ruby">
213
  @errors = ['I could not achieve what you wanted.']
214
  render_error status: 500
215
</code></pre>
216
217
You can also do this, anywhere
218
219
<pre><code class="ruby">
220
  raise 'My spoon is too big.'
221
</code></pre>
222
223
The @render_error@ method sends JSON or HTML to the client according to the Accept header in the request (it sends JSON if JavaScript was requested), so reasonable things happen whether or not the request is AJAX.
224
225
h2. Development patterns
226
227
h3. Add a model
228
229
Currently, when the API provides a new model, we need to generate a corresponding model in Workbench: it's not smart enough to pick up the list of models from the API server's discovery document.
230
231
_(Need to fill in details here)_
232
# @rails generate model ....@
233
# Delete migration
234 8 Peter Amstutz
# Change base class to ArvadosBase
235
# @rails generate controller ...@ 
236 1 Tom Clegg
237
Model _attributes_, on the other hand, are populated automatically.
238
239
h3. Add a configuration knob
240
241
Same situation as API server. See [[Hacking API Server]].
242
243
h3. Add an API method
244
245
Workbench is not yet smart enough to look in the discovery document for supported API methods. You need to add a method to the appropriate model class before you can use it in the Workbench app.
246
247
h3. Writing tests
248
249 18 Tom Clegg
In integration tests, this makes your tests flaky because the result depends on whether the page has finished loading:
250
* <pre><code class="ruby">
251
assert page.has_selector?('a', text: 'foo')  # Danger!
252
</code></pre>
253
* Instead, do this:
254
* <pre><code class="ruby">
255
assert_selector('a', text: 'foo')
256
</code></pre>
257
* This lets Capybara wait for the selector to appear.
258
259 1 Tom Clegg
260
h3. AJAX using Rails UJS (remote:true with JavaScript response)
261
262
This pattern is the best way to make a button/link that invokes an asynchronous action on the Workbench server side, i.e., before/without navigating away from the current page.
263
264
# Add <code class="ruby">remote: true</code> to a link or button. This makes Rails put a <code class="html">data-remote="true"</code> attribute in the HTML element. Say, in @app/views/fizz_buzzes/index.html.erb@:
265
<pre><code class="ruby">
266
<%= link_to "Blurfl", blurfl_fizz_buzz_url(id: @object.uuid), {class: 'btn btn-primary', remote: true} %>
267
</code></pre>
268
# Ensure the targeted action responds appropriately to both "js" and "html" requests. At minimum:
269
<pre><code class="ruby">
270
class FizzBuzzesController
271
  #...
272
  def blurfl
273
    @howmany = 1
274
    #...
275
    respond_to do |format|
276
      format.js
277
      format.html
278
    end
279
  end
280
end
281
</code></pre>
282
# The @html@ view is used if this is a normal page load (presumably this means the client has turned off JS).
283
#* @app/views/fizz_buzz/blurfl.html.erb@
284
<pre><code>
285
<p>I am <%= @howmany %></p>
286
</code></pre>
287
# The @js@ view is used if this is an AJAX request. It renders as JavaScript code which will be executed in the browser. Say, in @app/views/fizz_buzz/blurfl.js.erb@:
288
<pre><code class="javascript">
289
window.alert('I am <%= @howmany %>');
290
</code></pre>
291
# The browser opens an alert box:
292
<pre>
293
I am 1
294
</pre>
295
# A common task is to render a partial and use it to update part of the page. Say the partial is in @app/views/fizz_buzz/_latest_news.html.erb@:
296
<pre><code class="javascript">
297
var new_content = "<%= escape_javascript(render partial: 'latest_news') %>";
298
if ($('div#latest-news').html() != new_content)
299
   $('div#latest-news').html(new_content);
300
</code></pre>
301
302
*TODO: error handling*
303
304
h3. AJAX invoked from custom JavaScript (JSON response)
305
306
(and error handling)
307
308
h3. Add JavaScript triggers and fancy behavior
309
310
Some guidelines for implementing stuff nicely in JavaScript:
311
* Don't rely on the DOM being loaded before your script is loaded.
312
** If you need to inspect/alter the DOM as soon as it's loaded, make a setup function that fires on "document ready" and "ajax:complete".
313
** jQuery's delegated event pattern can help keep your code clean. See http://api.jquery.com/on/
314
<pre><code class="javascript">
315
// worse:
316
$('table.fizzbuzzer tr').
317
    on('mouseover', function(e, xhr) {
318
        console.log("This only works if the table exists when this setup script is executed.");
319
    });
320
// better:
321
$(document).
322
    on('mouseover', 'table.fizzbuzzer tr', function(e, xhr) {
323
        console.log("This works even if the table appears (or has the fizzbuzzer class added) later.");
324
    });
325
</code></pre>
326
327
* If your code really only makes sense for a particular view, rather than embedding @<script>@ tags in the middle of the page,
328
** use this:
329
<pre><code class="ruby">
330
<% content_for :js do %>
331
console.log("hurray, this goes in HEAD");
332
<% end %>
333
</code></pre>
334
** or, if your code should run after [most of] the DOM is loaded:
335
<pre><code class="ruby">
336
<% content_for :footer_js do %>
337
console.log("hurray, this runs at the bottom of the BODY element in the default layout.");
338
<% end %>
339
</code></pre>
340
341
* Don't just write JavaScript on the @fizz_buzzes/blurfl@ page and rely on the fact that the only @table@ element on the page is the one you want to attach your special behavior to. Instead, add a class to the table, and use a jQuery selector to attach special behavior to it.
342
** In @app/views/fizz_buzzes/blurfl.html.erb@
343
<pre>
344
<table class="fizzbuzzer">
345
 <tr>
346
  <td>fizz</td><td>buzz</td>
347
 </tr>
348
</table>
349
</pre>
350
** In @app/assets/javascripts/fizz_buzzes.js@
351
<pre><code class="javascript">
352
<% content_for :js do %>
353
$(document).on('mouseover', 'table.fizzbuzzer tr', function() {
354
    console.log('buzz');
355
});
356
<% end %>
357
</code></pre>
358
** Advantage: You can reuse the special behavior in other tables/pages/classes
359
** Advantage: The JavaScript can get compiled, minified, cached in the browser, etc., instead of being rendered with every page view
360
** Advantage: The JavaScript code is available regardless of how the content got into the DOM (regular page view, partial update with AJAX)
361
362 14 Phil Hodgson
* If the result of clicking on some link invokes Javascript that will ultimately change the content of the current page using @window.location.href=@ then it is advisable to add to the link the @force-cache-reload@ CSS class. By doing so, when a user uses the browser-back button to return to the original page, it will be forced to reload itself from the server, thereby reflecting the updated content. (Ref: https://arvados.org/issues/3634)
363
364 11 Tom Clegg
h3. Invoking chooser
365
366
Example from @app/views/projects/_show_contents.html.erb@:
367
368
<pre>
369
    <%= link_to(
370
          choose_collections_path(
371
            title: 'Add data to project:',
372
            multiple: true,
373
            action_name: 'Add',
374
            action_href: actions_path(id: @object.uuid),
375
            action_method: 'post',
376
            action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
377
          { class: "btn btn-primary btn-sm", remote: true, method: 'get', data: {'event-after-select' => 'page-refresh'} }) do %>
378
      <i class="fa fa-fw fa-plus"></i> Add data...
379
    <% end %>
380
</pre>
381
382
Tour:
383 1 Tom Clegg
384
(TODO)
385
386 15 Tom Clegg
h3. Infinite scroll
387
388
When showing a list that might be too long to render up front in its entirety, use the infinite-scroll feature.
389
390
Links/buttons that flip to page 1, 2, 3, etc. (e.g., <code class="ruby">render partial: "paging"</code>) are deprecated.
391
392
The comments that should be at the top of source:apps/workbench/app/assets/javascripts/infinite_scroll.js, when we write them, will tell you how to do it.
393
394
395
h3. Filtering lists
396
397
When a list is displayed, and the user might want to filter them by selecting a category or typing a search string, use @class="filterable"@. It's easy!
398
399
The comments at the top of source:apps/workbench/app/assets/javascripts/filterable.js tell you how to do it.
400
401 1 Tom Clegg
h3. Tabs/panes on index & show pages
402
403
(TODO)
404
405
h3. User notifications
406
407
(TODO)
408
409
h3. Customizing breadcrumbs
410
411
(TODO)
412
413
h3. Making a page accessible before login
414
415
(TODO)
416
417
h3. Making a page accessible to non-active users
418
419
(TODO)
420 21 Phil Hodgson
421
h3. Developing and Testing the Job Log
422
423
To assist with developing and testing the live job log that updates itself via websockets, there is a rake task that will "replay" a log from a file as if it had been generated by a real job. _Note that this is done within the API Server context_, so you must first switch the current directory appropriately (@cd services/api@). The task takes up to three arguments:
424
425
* log path and filename
426
** The relative path to the log file you want to "replay".
427
* time multipler (optional)
428
** The speed factor at which this log replay should be simulated. The default is 1.0, or normal speed. Higher numbers will proportionately increase the speed of the simulation. For example "4" will make it so that log entries that normally would have appeared over the course of four minutes will appear over the course of one minute. Numbers between 0 and 1 will slow down the simulation.
429
* simulated job uuid (optional)
430
** By providing a job UUID to simulate, the rake task will replace the job UUID in the log file with this job UUID. This means that you can be observing the effects on the Log tab of a particular job but use the log file output from another job.
431
432
Note that as with all rake tasks, if there are confusing characters in the list of arguments, including spaces separating the arguments, you will need to enclose the rake argument in quotation marks.
433
434
Example:
435
436
<pre>
437
  rake "replay_job_log[path/to/your.log, 2.0, qr1hi-8i9sb-nf3qk0xzwwz3lre]"'
438
</pre>
439
440
A typical testing iteration using this task would work as follows:
441
442
# Delete the entries from the LOGS table for the Job UUID you will be observing.
443
# Refresh the browser page showing the Job's Log to clear the graph and graph contents.
444
# Run the rake task
445
# Enjoy and/or write beautiful code improvements
446
# Repeat