t2-web 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES +18 -0
- data/LICENSE +23 -0
- data/README +41 -0
- data/Rakefile +54 -0
- data/bin/t2_webapp.rb +295 -0
- data/public/css/form.css +70 -0
- data/public/css/results_header.css +23 -0
- data/public/css/results_navigation.css +17 -0
- data/public/css/tipsy.css +7 -0
- data/public/images/info.png +0 -0
- data/public/images/lumc_logo2.png +0 -0
- data/public/images/nbic_logo.gif +0 -0
- data/public/images/snake_transparent.gif +0 -0
- data/public/images/tipsy.gif +0 -0
- data/public/scripts/form.js +98 -0
- data/public/scripts/jquery-1.6.1.js +8936 -0
- data/public/scripts/jquery.tipsy.js +104 -0
- data/public/scripts/results.js +25 -0
- data/views/form.haml +173 -0
- data/views/results.haml +28 -0
- metadata +165 -0
data/CHANGES
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
= Changes log for the Taverna via the Web Gem
|
2
|
+
|
3
|
+
== Version 0.0.5
|
4
|
+
* Updated to new myexperiment-rest API
|
5
|
+
|
6
|
+
== Version 0.0.4
|
7
|
+
* Improved the UI (added logos, tooltips, changed colours).
|
8
|
+
* Made some code abstractions
|
9
|
+
|
10
|
+
== Version 0.0.3
|
11
|
+
* Improved the UI (changed divs with frames).
|
12
|
+
|
13
|
+
== Version 0.0.2
|
14
|
+
* Added upload functionality option for each input.
|
15
|
+
|
16
|
+
== Version 0.0.1
|
17
|
+
* Works. Very basic functionality and UI.
|
18
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
== t2-web
|
2
|
+
|
3
|
+
The MIT License
|
4
|
+
|
5
|
+
Copyright (c) 2010 - Netherlands Bioinformatics Centre
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
15
|
+
all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
= t2-web interface generator
|
2
|
+
|
3
|
+
|
4
|
+
Authors:: Konstantinos Karasavvas
|
5
|
+
Gem Version:: 0.0.5
|
6
|
+
Contact:: mailto:kostas.karasavvas@nbic.nl
|
7
|
+
Licence:: MIT (See LICENCE or http://www.opensource.org/licenses/mit-license)
|
8
|
+
Copyright:: (c) 2010 Netherlands Bioinformatics Centre, The Netherlands
|
9
|
+
|
10
|
+
|
11
|
+
== Synopsis
|
12
|
+
|
13
|
+
This web application accepts a myExperiment workflow identifier and constructs
|
14
|
+
a web form to allow end-users to configure and execute that workflow via a web
|
15
|
+
browser with no other dependencies.. The web form interface is as good as the
|
16
|
+
description provided in the workflow itself in myExperiment.
|
17
|
+
|
18
|
+
|
19
|
+
== Installation
|
20
|
+
|
21
|
+
[sudo] gem install t2-web
|
22
|
+
|
23
|
+
|
24
|
+
== Usage
|
25
|
+
|
26
|
+
t2_webapp
|
27
|
+
|
28
|
+
Runs the web application at port 9494 and waits for incoming requests.
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
== References
|
33
|
+
|
34
|
+
Taverna to Web Form Generator:: https://trac.nbic.nl/elabfactory/wiki/t2web
|
35
|
+
Taverna2:: http://www.taverna.org.uk
|
36
|
+
myExperiment:: http://www.myexperiment.org
|
37
|
+
|
38
|
+
|
39
|
+
== Semantic Versioning
|
40
|
+
|
41
|
+
This module uses semantic versioning concepts from http://semver.org/.
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#
|
2
|
+
# To change this template, choose Tools | Templates
|
3
|
+
# and open the template in the editor.
|
4
|
+
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'rake'
|
8
|
+
require 'rake/clean'
|
9
|
+
require 'rake/testtask'
|
10
|
+
require 'rubygems/package_task'
|
11
|
+
require 'rdoc/task'
|
12
|
+
|
13
|
+
spec = Gem::Specification.new do |s|
|
14
|
+
s.name = 't2-web'
|
15
|
+
s.version = '0.0.5'
|
16
|
+
s.extra_rdoc_files = ['README', 'LICENSE', 'CHANGES']
|
17
|
+
s.summary = 'WS (with libs) that generates a Web UI form for a Taverna2 workflow and then enacts it to a T2 server.'
|
18
|
+
s.description = s.summary
|
19
|
+
s.author = 'Kostas Karasavvas'
|
20
|
+
s.email = 'kostas.karasavvas@nbic.nl'
|
21
|
+
s.executables = ['t2_webapp.rb']
|
22
|
+
s.files = %w(LICENSE README CHANGES Rakefile) + Dir.glob("{bin,lib,spec,public,views}/**/*")
|
23
|
+
s.require_path = "lib"
|
24
|
+
s.bindir = "bin"
|
25
|
+
s.add_dependency 'sinatra', '~> 1.2.6'
|
26
|
+
s.add_dependency 'haml', '~> 3.1.2'
|
27
|
+
s.add_dependency 'rest-client', '~> 1.6.3'
|
28
|
+
s.add_dependency 't2-server', '~> 0.6.1'
|
29
|
+
s.add_dependency 'myexperiment-rest', '~> 0.3.0'
|
30
|
+
end
|
31
|
+
|
32
|
+
Gem::PackageTask.new(spec) do |p|
|
33
|
+
p.gem_spec = spec
|
34
|
+
p.need_tar = true
|
35
|
+
p.need_zip = true
|
36
|
+
end
|
37
|
+
|
38
|
+
RDoc::Task.new do |rdoc|
|
39
|
+
files =['README', 'LICENSE', 'CHANGES', 'lib/**/*.rb']
|
40
|
+
rdoc.rdoc_files.add(files)
|
41
|
+
rdoc.main = "README" # page to start on
|
42
|
+
rdoc.title = "web-t2 Docs"
|
43
|
+
rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
|
44
|
+
rdoc.options << '--line-numbers'
|
45
|
+
end
|
46
|
+
|
47
|
+
Rake::TestTask.new do |t|
|
48
|
+
t.test_files = FileList['test/**/*.rb']
|
49
|
+
end
|
50
|
+
|
51
|
+
# make gem and copy to test deployment server
|
52
|
+
task :copy_to_server => :gem do
|
53
|
+
`scp pkg/#{spec.name}-#{spec.version}.gem kostas@mybiobank.org:/home/kostas`
|
54
|
+
end
|
data/bin/t2_webapp.rb
ADDED
@@ -0,0 +1,295 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems' if RUBY_VERSION < '1.9'
|
4
|
+
require 'sinatra'
|
5
|
+
require 'haml'
|
6
|
+
require 'open-uri'
|
7
|
+
require 'rest-client'
|
8
|
+
require 't2-server'
|
9
|
+
require 'myexperiment-rest'
|
10
|
+
require 'cgi'
|
11
|
+
#require "web-t2" TODO: move whole of WebT2App in lib
|
12
|
+
|
13
|
+
|
14
|
+
class WebT2App < Sinatra::Base
|
15
|
+
|
16
|
+
WEB_APP_NAME = "t2web"
|
17
|
+
# TODO: const for TMP_UPLOAD_PATH ? - need to read from config file?
|
18
|
+
|
19
|
+
set :port, 9494
|
20
|
+
set :views, File.dirname(__FILE__) + '/../views'
|
21
|
+
set :public, File.dirname(__FILE__) + '/../public'
|
22
|
+
|
23
|
+
# can be used from routes and views (haml)
|
24
|
+
helpers do
|
25
|
+
|
26
|
+
# Used before we sent the workflow inputs to the taverna server. Just converts
|
27
|
+
# double quotations to single ones. That is because the t2-server library
|
28
|
+
# is confused with double quotes and apparently it is not easy to fix.
|
29
|
+
def t2_library_sanitize(string)
|
30
|
+
ex = string.gsub('"', "'")
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Deletes last new line of file if it exists! It is needed for t2 workflows that
|
35
|
+
# do not sanitize properly, i.e. via a user-provided beanshell script
|
36
|
+
#
|
37
|
+
def chomp_last_newline(file)
|
38
|
+
|
39
|
+
if File.file?(file) and File.size(file) > 1
|
40
|
+
f = open(file, "rb+")
|
41
|
+
f.seek(-1, File::SEEK_END)
|
42
|
+
f.truncate(File.size(file) - 1) if f.read(1) == "\n"
|
43
|
+
f.close
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# Construct the complete or partial URL as MyExperimentRest lib expects it
|
49
|
+
# TODO: Maybe update myExperimentREST lib to accept only wid and wkf_version
|
50
|
+
# Minor for now
|
51
|
+
def get_url_from_wid(wid, wkf_version)
|
52
|
+
if wkf_version == "default"
|
53
|
+
"/workflows/#{wid}"
|
54
|
+
else
|
55
|
+
"/workflows/#{wid}?version=#{wkf_version}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Generates the contents of the header frame
|
61
|
+
def generate_header_frame(my_exp_wkf, my_exp_usr)
|
62
|
+
<<END
|
63
|
+
<html>
|
64
|
+
<head><link href='/css/form.css' rel='stylesheet'/></head>
|
65
|
+
<body>
|
66
|
+
#{generate_header_table(my_exp_wkf, my_exp_usr)}
|
67
|
+
</body>
|
68
|
+
</html>
|
69
|
+
END
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# Generates the contents of the header frame
|
74
|
+
def generate_header_table(my_exp_wkf, my_exp_usr)
|
75
|
+
<<END
|
76
|
+
<div id='header'>
|
77
|
+
<table class='header'>
|
78
|
+
<tr>
|
79
|
+
<td>
|
80
|
+
<img alt='NBIC logo' src='/images/nbic_logo.gif' />
|
81
|
+
</td>
|
82
|
+
<td>
|
83
|
+
<table class='header-title'>
|
84
|
+
<tr>
|
85
|
+
<td class='header-title'>Workflow: #{@my_exp_wkf.title}</td>
|
86
|
+
</tr>
|
87
|
+
<tr>
|
88
|
+
<td class='right'>workflow by #{@my_exp_usr.name}</td>
|
89
|
+
|
90
|
+
</tr>
|
91
|
+
</table>
|
92
|
+
</td>
|
93
|
+
<td>
|
94
|
+
<img alt='LUMC logo' class='right' src='/images/lumc_logo2.png' />
|
95
|
+
</td>
|
96
|
+
</tr>
|
97
|
+
</table>
|
98
|
+
</div>
|
99
|
+
END
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Generates tooltip html text for descriptions
|
104
|
+
def generate_label_tooltip(input)
|
105
|
+
"Description: " << CGI::unescapeHTML(input.descriptions[0])
|
106
|
+
end
|
107
|
+
|
108
|
+
# Generates tooltip html text for textarea example
|
109
|
+
def generate_textarea_tooltip(input)
|
110
|
+
"Example: " << CGI::unescapeHTML(input.examples[0])
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Generates the contents of the data-navigation frame
|
115
|
+
def generate_data_navigation_frame(my_exp_wkf, uuid, wid, wkf_version, t2_server, finished_execution)
|
116
|
+
|
117
|
+
data_navigation_frame = "<html><head><link href='/css/results_navigation.css' rel='stylesheet'/></head><body><table>"
|
118
|
+
if my_exp_wkf.outputs.size >=1
|
119
|
+
my_exp_wkf.outputs.each do |output|
|
120
|
+
data_navigation_frame << "<tr>"
|
121
|
+
if finished_execution
|
122
|
+
data_navigation_frame << "<td><a href='javascript:void(0);' onclick='parent.getSampleOutput(\\\"#{t2_server}\\\", \\\"#{uuid}\\\", " <<
|
123
|
+
"\\\"#{output.name}\\\", \\\"#{wid}\\\", \\\"#{wkf_version}\\\");'>#{output.name}</a></td><td></td>"
|
124
|
+
else
|
125
|
+
data_navigation_frame << "<td>#{output.name}</td><td><img src='/images/snake_transparent.gif' alt='Loading Content...'></td>"
|
126
|
+
end
|
127
|
+
data_navigation_frame << "</tr>"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
data_navigation_frame << "</table></body></html>"
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
# Check type of data and generate appropriate html for that result!
|
135
|
+
def generate_html_result(data)
|
136
|
+
html = ""
|
137
|
+
#p data
|
138
|
+
if data.instance_of? Array
|
139
|
+
html = data.flatten.join('<br>')
|
140
|
+
elsif data.instance_of? String
|
141
|
+
html = data.gsub(/[\n]/, '<br>')
|
142
|
+
else
|
143
|
+
html = data
|
144
|
+
end
|
145
|
+
html
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
# Create the Web UI for that workflow
|
153
|
+
get "/#{WEB_APP_NAME}/workflow/:wid" do
|
154
|
+
|
155
|
+
# Get http request's parameters
|
156
|
+
# TODO: consider adding the following to a session
|
157
|
+
@wid = params[:wid]
|
158
|
+
@wkf_version = params[:wkf_version] || nil
|
159
|
+
@t2_server = params[:server] || "http://test.mybiobank.org/taverna-server"
|
160
|
+
|
161
|
+
# Get myExperiment workflow object
|
162
|
+
# TODO: catch exception -- make custom exceptions?
|
163
|
+
@my_exp_wkf = MyExperimentREST::Workflow.from_id_and_version(@wid, @wkf_version)
|
164
|
+
|
165
|
+
# Get myExperiment user object
|
166
|
+
# TODO: catch exception -- make custom exceptions?
|
167
|
+
@my_exp_usr = MyExperimentREST::User.from_uri(@my_exp_wkf.uploader_uri)
|
168
|
+
|
169
|
+
haml :form
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
# Enact the workflow with the submitted parameters and returns
|
175
|
+
# result display (involves checking run's status and getting results)
|
176
|
+
post "/#{WEB_APP_NAME}/enact" do
|
177
|
+
#p params
|
178
|
+
|
179
|
+
# Get http POST request's parameters
|
180
|
+
# TODO: consider adding the following to a session
|
181
|
+
@wid = params[:wid]
|
182
|
+
@wkf_version = params[:wkf_version]
|
183
|
+
@t2_server = params[:server]
|
184
|
+
|
185
|
+
@my_exp_wkf = MyExperimentREST::Workflow.from_id_and_version(@wid, @wkf_version)
|
186
|
+
|
187
|
+
# Get myExperiment user object (TODO: session!, exceptions)
|
188
|
+
@my_exp_usr = MyExperimentREST::User.from_uri(@my_exp_wkf.uploader_uri)
|
189
|
+
|
190
|
+
|
191
|
+
# use the uri reference to download the workflow locally
|
192
|
+
#wkf_file = URI.parse(@my_exp_wkf.content_uri)
|
193
|
+
wkf = open(@my_exp_wkf.content_uri).read
|
194
|
+
|
195
|
+
# create run
|
196
|
+
begin
|
197
|
+
run = T2Server::Run.create(@t2_server, wkf)
|
198
|
+
rescue T2Server::T2ServerError => e
|
199
|
+
return "404 Not Found: run could not be instantiated!"
|
200
|
+
end
|
201
|
+
|
202
|
+
# Get the run as instance member to make visible in views
|
203
|
+
@run_uuid = run.uuid
|
204
|
+
|
205
|
+
# Set workflow inputs
|
206
|
+
@my_exp_wkf.inputs.each do |input|
|
207
|
+
if params[:"upload-checkbox-#{input.name}"] == "yes"
|
208
|
+
filename = params[:"#{input.name}-file"]
|
209
|
+
tmp_filename = "/tmp/#{WEB_APP_NAME}/#{filename}"
|
210
|
+
|
211
|
+
# make sure that the file is uploaded completely, i.e. all it's
|
212
|
+
# connections are closed
|
213
|
+
#while `lsof -c cp | grep /tmp/#{WEB_APP_NAME}/#{filename}` != ""
|
214
|
+
# sleep 1
|
215
|
+
#end
|
216
|
+
|
217
|
+
chomp_last_newline(tmp_filename)
|
218
|
+
run.upload_input_file(input.name, tmp_filename)
|
219
|
+
else
|
220
|
+
run.set_input(input.name, t2_library_sanitize(params["#{input.name}-input".to_sym]) )
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# start run and wait until it is finished
|
225
|
+
run.start
|
226
|
+
|
227
|
+
haml :results
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
|
232
|
+
# Proxy operation (to bypass cross-domain AJAX) to get T2 run's status
|
233
|
+
get '/runs/:uuid/status' do
|
234
|
+
uuid = params[:uuid]
|
235
|
+
t2_server = params[:server]
|
236
|
+
|
237
|
+
response = RestClient.get t2_server + "/rest/runs/" + uuid + "/status", :content_type => "text/plain"
|
238
|
+
|
239
|
+
response.to_str
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
# Get results for specified run and output - display it in data-display div
|
245
|
+
# Cross-domain AJAX to get result from T2
|
246
|
+
get "/#{WEB_APP_NAME}/run/:uuid/output/:out" do
|
247
|
+
|
248
|
+
# Get http request's parameters
|
249
|
+
# TODO: consider adding the following to a session
|
250
|
+
@wid = params[:wid]
|
251
|
+
@wkf_version = params[:wkf_version]
|
252
|
+
|
253
|
+
# Get myExperiment workflow object
|
254
|
+
# TODO: with sessions that would be remembered
|
255
|
+
@my_exp_wkf = MyExperimentREST::Workflow.from_id_and_version(@wid, @wkf_version)
|
256
|
+
|
257
|
+
@run_uuid = params[:uuid]
|
258
|
+
@t2_server = params[:server]
|
259
|
+
t2_output = params[:out]
|
260
|
+
|
261
|
+
# Get t2 server and then the run using uuid
|
262
|
+
server = T2Server::Server.connect(@t2_server)
|
263
|
+
run = server.run(@run_uuid)
|
264
|
+
|
265
|
+
# Get output from t2 server
|
266
|
+
# TODO: that gets the whole output.. request client lib to support partial
|
267
|
+
# download! -- refs could then be used to download the full results!
|
268
|
+
begin
|
269
|
+
data_lists = run.get_output(t2_output, false)
|
270
|
+
rescue Exception => e
|
271
|
+
data_lists = run.get_output("#{t2_output}.error", false)
|
272
|
+
end
|
273
|
+
|
274
|
+
# that should be a sample of the results -- not the complete results!
|
275
|
+
data = generate_html_result(data_lists)
|
276
|
+
|
277
|
+
data
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
# Used from input's upload-form to upload files without refresh (actually
|
282
|
+
# a hidden iframe is refreshed).
|
283
|
+
post "/#{WEB_APP_NAME}/upload" do
|
284
|
+
filename = params[:file][:filename]
|
285
|
+
tempfile = params[:file][:tempfile]
|
286
|
+
FileUtils.mkdir("/tmp/#{WEB_APP_NAME}") unless File.exist?("/tmp/#{WEB_APP_NAME}")
|
287
|
+
FileUtils.copy(tempfile.path, "/tmp/#{WEB_APP_NAME}/#{filename}")
|
288
|
+
"Successfully copied #{filename}!!"
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
# Start Web App
|
295
|
+
WebT2App.run!
|
data/public/css/form.css
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
body{
|
2
|
+
background-color: #fdfdfd;
|
3
|
+
}
|
4
|
+
|
5
|
+
table.header {
|
6
|
+
width: 100%;
|
7
|
+
}
|
8
|
+
|
9
|
+
table.header-title {
|
10
|
+
border-collapse: collapse;
|
11
|
+
/* With equal left-right margins table will be aligned in the centre */
|
12
|
+
margin-left: auto;
|
13
|
+
margin-right: auto;
|
14
|
+
}
|
15
|
+
|
16
|
+
td.header-title {
|
17
|
+
font-size: 200%;
|
18
|
+
background-color: #f0f0f0;
|
19
|
+
padding: 12px;
|
20
|
+
border-bottom: 4px solid #909090;
|
21
|
+
border-top: 4px solid #909090;
|
22
|
+
}
|
23
|
+
|
24
|
+
#inputs {
|
25
|
+
}
|
26
|
+
|
27
|
+
table.inputs {
|
28
|
+
width: 800px;
|
29
|
+
border-collapse: collapse;
|
30
|
+
/* With equal left-right margins table will be aligned in the centre */
|
31
|
+
margin-left: auto;
|
32
|
+
margin-right: auto;
|
33
|
+
}
|
34
|
+
|
35
|
+
th.inputs {
|
36
|
+
font-size: 120%;
|
37
|
+
background-color: #f0f0f0;
|
38
|
+
padding: 10px;
|
39
|
+
border-bottom: 1px solid #909090;
|
40
|
+
border-top: 4px solid #909090;
|
41
|
+
}
|
42
|
+
|
43
|
+
td.inputs {
|
44
|
+
padding: 5px;
|
45
|
+
vertical-align: top;
|
46
|
+
}
|
47
|
+
|
48
|
+
td.right {
|
49
|
+
text-align: right;
|
50
|
+
}
|
51
|
+
|
52
|
+
img.right {
|
53
|
+
float: right;
|
54
|
+
}
|
55
|
+
|
56
|
+
#description {
|
57
|
+
}
|
58
|
+
|
59
|
+
#inputs {
|
60
|
+
}
|
61
|
+
|
62
|
+
#outputs {
|
63
|
+
}
|
64
|
+
|
65
|
+
#note {
|
66
|
+
}
|
67
|
+
|
68
|
+
label {
|
69
|
+
text-decoration: underline;
|
70
|
+
}
|