Project

General

Profile

Hacking Workbench » History » Version 17

Tom Clegg, 11/18/2014 05:48 PM

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
21
* "Getting started with Rails":http://guides.rubyonrails.org/getting_started.html at rubyonrails.org
22
* "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)
23
24 1 Tom Clegg
h2. Unlike a typical Rails project...
25
26 7 Peter Amstutz
* ActiveRecord in Workbench doesn't talk to the database directly, but instead queries the Arvados API as REST client.
27
* The Arvados query API is somewhat limited and doesn't accept SQL statements, so Workbench has to work harder to get what it needs.
28 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.
29
30
h2. Unlike what you might expect...
31
32
* Workbench doesn't use the Ruby SDK. It uses a sort of baked-in Rails SDK.
33
** TODO: move it out of Workbench into a gem.
34
** TODO: use the Ruby SDK under the hood.
35
36
h2. Running in development mode
37
38 2 Misha Zatsman
h3. SSL certificates
39
40 4 Tom Clegg
You can get started quickly with SSL by generating a self-signed certificate:
41 1 Tom Clegg
42
 openssl req -new -x509 -nodes -out ~/self-signed.pem -keyout ~/self-signed.key -days 3650 -subj '/CN=arvados.example.com'
43
44
Alternatively, download a set from the bottom of the [[API server]] page.
45 2 Misha Zatsman
46
h3. Download and configure
47 1 Tom Clegg
48 2 Misha Zatsman
Follow "these instructions":http://doc.arvados.org/install/install-workbench-app.html to download the source and configure your workbench instance.
49 3 Misha Zatsman
50 4 Tom Clegg
h3. Start the server
51 1 Tom Clegg
52 4 Tom Clegg
Save something like the following at @~/bin/workbench@, make it executable[1], make sure @~/bin@ is in your path[2]:
53 1 Tom Clegg
54
 #!/bin/sh
55
set -e
56
cd ~/arvados/apps/workbench
57
export RAILS_ENV=development
58 5 Tom Clegg
bundle install --path=vendor/bundle
59 4 Tom Clegg
exec bundle exec passenger start -p 3031 --ssl --ssl-certificate ~/self-signed.pem --ssl-certificate-key ~/self-signed.key
60 1 Tom Clegg
61
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
62
63
Once you see:
64
65
 =============== Phusion Passenger Standalone web server started ===============
66
67
You can visit your server at:
68
69 4 Tom Clegg
 @https://{ip-or-host}:3031/@
70
71 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
72
73
 @ps x |grep nginx |grep master@
74
75
And then
76
77
 @kill ####@
78
79
Replacing #### with the number in the left column returned by ps
80
81 4 Tom Clegg
fn1. @chmod +x ~/bin/workbench@
82
83
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.
84
85
h2. Running tests
86
87
The test suite brings up an API server in test mode, and runs browser tests with Firefox.
88
89 17 Tom Clegg
Make sure API server has its dependencies in place and its database schema up-to-date.
90 4 Tom Clegg
91 1 Tom Clegg
<pre>
92 17 Tom Clegg
(
93
 set -e
94
 cd ../../services/api
95
 RAILS_ENV=test bundle install --path=vendor/bundle
96
 RAILS_ENV=test bundle exec rake db:migrate
97
)
98 4 Tom Clegg
</pre>
99
100
Install headless testing tools.
101
102
<pre>
103
sudo apt-get install xvfb iceweasel
104
</pre>
105
106
(Install firefox instead of iceweasel if you're not using Debian.)
107
108 10 Tom Clegg
Install phantomjs. (See http://phantomjs.org/download.html for latest version.)
109
110
<pre>
111 16 Tom Clegg
wget -P /tmp https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.8-linux-x86_64.tar.bz2
112
sudo tar -C /usr/local -xjf /tmp/phantomjs-1.9.8-linux-x86_64.tar.bz2
113
sudo ln -s ../phantomjs-1.9.8-linux-x86_64/bin/phantomjs /usr/local/bin/
114 10 Tom Clegg
</pre>
115
116 4 Tom Clegg
Run the test suite.
117
118
<pre>
119
RAILS_ENV=test bundle exec rake test
120 1 Tom Clegg
</pre>
121 9 Tom Clegg
122 13 Tom Clegg
h3. When tests fail...
123
124
When an integration test fails (or skips) a screenshot is automatically saved in @arvados/apps/workbench/tmp/workbench-fail-1.png@, etc.
125
126 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:
127 13 Tom Clegg
* <pre>
128 1 Tom Clegg
$ RAILS_ENV=test bundle exec rake test TESTOPTS=-v
129 13 Tom Clegg
[...]
130
ApplicationControllerTest#test_links_for_object = 0.10 s = .
131
[...]
132
Saved ./tmp/workbench-fail-2.png
133 16 Tom Clegg
CollectionsTest#test_combine_selected_collection_files_into_new_collection = 10.89 s = F
134 13 Tom Clegg
[...]
135
</pre>
136 9 Tom Clegg
137 1 Tom Clegg
h2. Loading state from API into models
138
139
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@:
140
141
<pre><code class="ruby">
142
  api_response = $arvados_api_client.api(...)
143
  private_reload api_response
144
</code></pre>
145
146
h2. Features
147
148
h3. Authentication
149
150
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.
151
152
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.)
153
154
h3. Default filter behavior
155
156
@before_filter :find_object_by_uuid@
157
158
* This is enabled by default, @except :index, :create@.
159
* 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.)
160
* If you define a collection method (where there's no point looking up an object with the :id supplied in the request), skip this.
161
162
<pre><code class="ruby">
163
  skip_before_filter :find_object_by_uuid, only: [:action_that_takes_no_uuid_param]
164
</code></pre>
165
166
h3. Error handling
167
168
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.)
169
170
In a controller you get there like this
171
172
<pre><code class="ruby">
173
  @errors = ['I could not achieve what you wanted.']
174
  render_error status: 500
175
</code></pre>
176
177
You can also do this, anywhere
178
179
<pre><code class="ruby">
180
  raise 'My spoon is too big.'
181
</code></pre>
182
183
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.
184
185
h2. Development patterns
186
187
h3. Add a model
188
189
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.
190
191
_(Need to fill in details here)_
192
# @rails generate model ....@
193
# Delete migration
194 8 Peter Amstutz
# Change base class to ArvadosBase
195
# @rails generate controller ...@ 
196 1 Tom Clegg
197
Model _attributes_, on the other hand, are populated automatically.
198
199
h3. Add a configuration knob
200
201
Same situation as API server. See [[Hacking API Server]].
202
203
h3. Add an API method
204
205
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.
206
207
h3. Writing tests
208
209
(TODO)
210
211
h3. AJAX using Rails UJS (remote:true with JavaScript response)
212
213
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.
214
215
# 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@:
216
<pre><code class="ruby">
217
<%= link_to "Blurfl", blurfl_fizz_buzz_url(id: @object.uuid), {class: 'btn btn-primary', remote: true} %>
218
</code></pre>
219
# Ensure the targeted action responds appropriately to both "js" and "html" requests. At minimum:
220
<pre><code class="ruby">
221
class FizzBuzzesController
222
  #...
223
  def blurfl
224
    @howmany = 1
225
    #...
226
    respond_to do |format|
227
      format.js
228
      format.html
229
    end
230
  end
231
end
232
</code></pre>
233
# The @html@ view is used if this is a normal page load (presumably this means the client has turned off JS).
234
#* @app/views/fizz_buzz/blurfl.html.erb@
235
<pre><code>
236
<p>I am <%= @howmany %></p>
237
</code></pre>
238
# 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@:
239
<pre><code class="javascript">
240
window.alert('I am <%= @howmany %>');
241
</code></pre>
242
# The browser opens an alert box:
243
<pre>
244
I am 1
245
</pre>
246
# 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@:
247
<pre><code class="javascript">
248
var new_content = "<%= escape_javascript(render partial: 'latest_news') %>";
249
if ($('div#latest-news').html() != new_content)
250
   $('div#latest-news').html(new_content);
251
</code></pre>
252
253
*TODO: error handling*
254
255
h3. AJAX invoked from custom JavaScript (JSON response)
256
257
(and error handling)
258
259
h3. Add JavaScript triggers and fancy behavior
260
261
Some guidelines for implementing stuff nicely in JavaScript:
262
* Don't rely on the DOM being loaded before your script is loaded.
263
** 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".
264
** jQuery's delegated event pattern can help keep your code clean. See http://api.jquery.com/on/
265
<pre><code class="javascript">
266
// worse:
267
$('table.fizzbuzzer tr').
268
    on('mouseover', function(e, xhr) {
269
        console.log("This only works if the table exists when this setup script is executed.");
270
    });
271
// better:
272
$(document).
273
    on('mouseover', 'table.fizzbuzzer tr', function(e, xhr) {
274
        console.log("This works even if the table appears (or has the fizzbuzzer class added) later.");
275
    });
276
</code></pre>
277
278
* If your code really only makes sense for a particular view, rather than embedding @<script>@ tags in the middle of the page,
279
** use this:
280
<pre><code class="ruby">
281
<% content_for :js do %>
282
console.log("hurray, this goes in HEAD");
283
<% end %>
284
</code></pre>
285
** or, if your code should run after [most of] the DOM is loaded:
286
<pre><code class="ruby">
287
<% content_for :footer_js do %>
288
console.log("hurray, this runs at the bottom of the BODY element in the default layout.");
289
<% end %>
290
</code></pre>
291
292
* 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.
293
** In @app/views/fizz_buzzes/blurfl.html.erb@
294
<pre>
295
<table class="fizzbuzzer">
296
 <tr>
297
  <td>fizz</td><td>buzz</td>
298
 </tr>
299
</table>
300
</pre>
301
** In @app/assets/javascripts/fizz_buzzes.js@
302
<pre><code class="javascript">
303
<% content_for :js do %>
304
$(document).on('mouseover', 'table.fizzbuzzer tr', function() {
305
    console.log('buzz');
306
});
307
<% end %>
308
</code></pre>
309
** Advantage: You can reuse the special behavior in other tables/pages/classes
310
** Advantage: The JavaScript can get compiled, minified, cached in the browser, etc., instead of being rendered with every page view
311
** Advantage: The JavaScript code is available regardless of how the content got into the DOM (regular page view, partial update with AJAX)
312
313 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)
314
315 11 Tom Clegg
h3. Invoking chooser
316
317
Example from @app/views/projects/_show_contents.html.erb@:
318
319
<pre>
320
    <%= link_to(
321
          choose_collections_path(
322
            title: 'Add data to project:',
323
            multiple: true,
324
            action_name: 'Add',
325
            action_href: actions_path(id: @object.uuid),
326
            action_method: 'post',
327
            action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
328
          { class: "btn btn-primary btn-sm", remote: true, method: 'get', data: {'event-after-select' => 'page-refresh'} }) do %>
329
      <i class="fa fa-fw fa-plus"></i> Add data...
330
    <% end %>
331
</pre>
332
333
Tour:
334 1 Tom Clegg
335
(TODO)
336
337 15 Tom Clegg
h3. Infinite scroll
338
339
When showing a list that might be too long to render up front in its entirety, use the infinite-scroll feature.
340
341
Links/buttons that flip to page 1, 2, 3, etc. (e.g., <code class="ruby">render partial: "paging"</code>) are deprecated.
342
343
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.
344
345
346
h3. Filtering lists
347
348
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!
349
350
The comments at the top of source:apps/workbench/app/assets/javascripts/filterable.js tell you how to do it.
351
352 1 Tom Clegg
h3. Tabs/panes on index & show pages
353
354
(TODO)
355
356
h3. User notifications
357
358
(TODO)
359
360
h3. Customizing breadcrumbs
361
362
(TODO)
363
364
h3. Making a page accessible before login
365
366
(TODO)
367
368
h3. Making a page accessible to non-active users
369
370
(TODO)