Project

General

Profile

Hacking Workbench » History » Version 9

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