Project

General

Profile

Hacking Workbench » History » Version 2

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