cyclid-ui 0.1.0

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.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +178 -0
  3. data/README.md +20 -0
  4. data/app/cyclid_ui.rb +124 -0
  5. data/app/cyclid_ui/config.rb +64 -0
  6. data/app/cyclid_ui/controllers/auth.rb +102 -0
  7. data/app/cyclid_ui/controllers/base.rb +41 -0
  8. data/app/cyclid_ui/controllers/health.rb +96 -0
  9. data/app/cyclid_ui/controllers/organization.rb +64 -0
  10. data/app/cyclid_ui/controllers/user.rb +40 -0
  11. data/app/cyclid_ui/helpers.rb +55 -0
  12. data/app/cyclid_ui/memcache.rb +45 -0
  13. data/app/cyclid_ui/models/user.rb +91 -0
  14. data/app/cyclid_ui/templates/footer.mustache +8 -0
  15. data/app/cyclid_ui/templates/job.mustache +21 -0
  16. data/app/cyclid_ui/templates/job_info.mustache +40 -0
  17. data/app/cyclid_ui/templates/layout.mustache +102 -0
  18. data/app/cyclid_ui/templates/login.mustache +78 -0
  19. data/app/cyclid_ui/templates/organization.mustache +150 -0
  20. data/app/cyclid_ui/templates/user.mustache +97 -0
  21. data/app/cyclid_ui/views/job.rb +25 -0
  22. data/app/cyclid_ui/views/layout.rb +52 -0
  23. data/app/cyclid_ui/views/login.rb +25 -0
  24. data/app/cyclid_ui/views/organization.rb +25 -0
  25. data/app/cyclid_ui/views/user.rb +25 -0
  26. data/bin/cyclid-ui-assets +17 -0
  27. data/lib/cyclid_ui/app.rb +4 -0
  28. data/public/images/LICENSE +3 -0
  29. data/public/images/cyclid-logo-large.png +0 -0
  30. data/public/images/favicon16.png +0 -0
  31. data/public/images/favicon32.png +0 -0
  32. data/public/images/favicon48.png +0 -0
  33. data/public/images/favicon64.png +0 -0
  34. data/public/images/favicon96.png +0 -0
  35. data/public/js/api.js +34 -0
  36. data/public/js/cyclid.js +32 -0
  37. data/public/js/job.js +215 -0
  38. data/public/js/organization.js +345 -0
  39. data/public/js/user.js +145 -0
  40. data/public/vendor/bootstrap/css/bootstrap-theme.css +587 -0
  41. data/public/vendor/bootstrap/css/bootstrap-theme.css.map +1 -0
  42. data/public/vendor/bootstrap/css/bootstrap-theme.min.css +6 -0
  43. data/public/vendor/bootstrap/css/bootstrap-theme.min.css.map +1 -0
  44. data/public/vendor/bootstrap/css/bootstrap.css +6760 -0
  45. data/public/vendor/bootstrap/css/bootstrap.css.map +1 -0
  46. data/public/vendor/bootstrap/css/bootstrap.min.css +6 -0
  47. data/public/vendor/bootstrap/css/bootstrap.min.css.map +1 -0
  48. data/public/vendor/bootstrap/css/custom.css +193 -0
  49. data/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  50. data/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  51. data/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  52. data/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  53. data/public/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  54. data/public/vendor/bootstrap/js/bootstrap.js +2363 -0
  55. data/public/vendor/bootstrap/js/bootstrap.min.js +7 -0
  56. data/public/vendor/bootstrap/js/npm.js +13 -0
  57. data/public/vendor/font-awesome/HELP-US-OUT.txt +7 -0
  58. data/public/vendor/font-awesome/css/font-awesome.css +2199 -0
  59. data/public/vendor/font-awesome/css/font-awesome.min.css +4 -0
  60. data/public/vendor/font-awesome/fonts/FontAwesome.otf +0 -0
  61. data/public/vendor/font-awesome/fonts/fontawesome-webfont.eot +0 -0
  62. data/public/vendor/font-awesome/fonts/fontawesome-webfont.svg +685 -0
  63. data/public/vendor/font-awesome/fonts/fontawesome-webfont.ttf +0 -0
  64. data/public/vendor/font-awesome/fonts/fontawesome-webfont.woff +0 -0
  65. data/public/vendor/font-awesome/fonts/fontawesome-webfont.woff2 +0 -0
  66. data/public/vendor/font-awesome/less/animated.less +34 -0
  67. data/public/vendor/font-awesome/less/bordered-pulled.less +25 -0
  68. data/public/vendor/font-awesome/less/core.less +12 -0
  69. data/public/vendor/font-awesome/less/fixed-width.less +6 -0
  70. data/public/vendor/font-awesome/less/font-awesome.less +18 -0
  71. data/public/vendor/font-awesome/less/icons.less +733 -0
  72. data/public/vendor/font-awesome/less/larger.less +13 -0
  73. data/public/vendor/font-awesome/less/list.less +19 -0
  74. data/public/vendor/font-awesome/less/mixins.less +60 -0
  75. data/public/vendor/font-awesome/less/path.less +15 -0
  76. data/public/vendor/font-awesome/less/rotated-flipped.less +20 -0
  77. data/public/vendor/font-awesome/less/screen-reader.less +5 -0
  78. data/public/vendor/font-awesome/less/stacked.less +20 -0
  79. data/public/vendor/font-awesome/less/variables.less +744 -0
  80. data/public/vendor/font-awesome/scss/_animated.scss +34 -0
  81. data/public/vendor/font-awesome/scss/_bordered-pulled.scss +25 -0
  82. data/public/vendor/font-awesome/scss/_core.scss +12 -0
  83. data/public/vendor/font-awesome/scss/_fixed-width.scss +6 -0
  84. data/public/vendor/font-awesome/scss/_icons.scss +733 -0
  85. data/public/vendor/font-awesome/scss/_larger.scss +13 -0
  86. data/public/vendor/font-awesome/scss/_list.scss +19 -0
  87. data/public/vendor/font-awesome/scss/_mixins.scss +60 -0
  88. data/public/vendor/font-awesome/scss/_path.scss +15 -0
  89. data/public/vendor/font-awesome/scss/_rotated-flipped.scss +20 -0
  90. data/public/vendor/font-awesome/scss/_screen-reader.scss +5 -0
  91. data/public/vendor/font-awesome/scss/_stacked.scss +20 -0
  92. data/public/vendor/font-awesome/scss/_variables.scss +744 -0
  93. data/public/vendor/font-awesome/scss/font-awesome.scss +18 -0
  94. data/public/vendor/jquery-2.2.4.min.js +4 -0
  95. data/public/vendor/jquery.md5.js +269 -0
  96. data/public/vendor/js.cookie.js +151 -0
  97. data/public/vendor/mustache.min.js +1 -0
  98. metadata +322 -0
@@ -0,0 +1,345 @@
1
+ function org_show_job() {
2
+ var job_id = $(this).data('job_id');
3
+ var job_info_inner = $('#job-info-inner').html();
4
+
5
+ $(`#collapse${job_id}`).html(job_info_inner);
6
+
7
+ var url = `${gblOrganizationURL}/jobs/${job_id}`;
8
+ api_get(url, gblUsername, ji_update_all, ji_get_failed);
9
+
10
+ // Watch the job for status updates
11
+ (function(j, u) {
12
+ addNamedInterval(`watcher${j}`,
13
+ function() { ji_watch_job(u); },
14
+ 3000);
15
+ })(job_id, url);
16
+
17
+ // Mark the parent row as active
18
+ $(`#row${job_id}`).addClass('active');
19
+ }
20
+
21
+ function org_hide_job() {
22
+ var job_id = $(this).data('job_id');
23
+
24
+ // Clear any watchers
25
+ removeNamedInterval(`watcher${job_id}`);
26
+
27
+ // Remove the job info panel
28
+ $(`#collapse${job_id} > #job-info-panel`).remove();
29
+
30
+ // Remove the parent row active highlight
31
+ $(`#row${job_id}`).removeClass('active');
32
+ }
33
+
34
+ function org_add_job(job, append) {
35
+ var accordian = $('#job-accordian tbody');
36
+
37
+ var template = $('#job-info').html();
38
+ Mustache.parse(template);
39
+
40
+ var duration = ji_calculate_duration(job.started, job.ended);
41
+
42
+ var data = {id: job.id,
43
+ name: job.job_name,
44
+ started: new Date(job.started).toUTCString(),
45
+ duration: duration,
46
+ status: ji_job_status_to_indicator(job.status)};
47
+
48
+ var rendered = Mustache.render(template, data);
49
+ var row = $(rendered);
50
+ row.hide();
51
+ if( append ) {
52
+ accordian.append(row);
53
+ } else {
54
+ accordian.prepend(row);
55
+ }
56
+ row.fadeIn('slow');
57
+
58
+ // Add the job ID to the collapsable element so it can associate itself
59
+ // to the correct job
60
+ $(`#collapse${job.id}`).data('job_id', job.id);
61
+
62
+ // If the job is active, add it to the active list
63
+ if( window.active_jobs == undefined )
64
+ window.active_jobs = [];
65
+
66
+ if( ji_job_active(job.status) ){
67
+ var active = {job_id: job.id, status: job.status};
68
+ window.active_jobs.push(active);
69
+ }
70
+ }
71
+
72
+ function org_job_list_failed(xhr) {
73
+ var failure_message = `Failed to retrieve job list<br>
74
+ <strong>${xhr.status}:</strong> ${xhr.responseText}`;
75
+
76
+ failure_message = `List failed: ${xhr.status}`;
77
+ $('#organization_failure > #error_message').html(failure_message);
78
+
79
+ $('#organization_failure').removeClass('hidden');
80
+ }
81
+
82
+ function org_update_job_list(jobs, append) {
83
+ console.log(jobs);
84
+
85
+ // Load the list
86
+ var records = jobs.records;
87
+ var length = records.length;
88
+ for( var i = length - 1; i >= 0; i-- ){
89
+ var job = records[i];
90
+ console.log(`job ${i}: ${JSON.stringify(job)}`);
91
+
92
+ org_add_job(job, append);
93
+ }
94
+
95
+ // Add collapse event handlers to each collapsable element to retrieve
96
+ // & remove the job info
97
+ $('.collapse').each(function(index) {
98
+ $(this).on('hidden.bs.collapse', org_hide_job);
99
+ $(this).on('show.bs.collapse', org_show_job);
100
+ });
101
+ // Ensure any active rows are properly hidden when a new one is shown
102
+ $('.collapse').on('show.bs.collapse', function () {
103
+ $('.collapse.in').collapse('hide');
104
+ });
105
+
106
+ // Show or hide the "Load more" button depending on if we're at the end of the list
107
+ if( gblOffset > 0 ) {
108
+ $('#org-load-more').removeClass('hidden');
109
+ } else {
110
+ $('#org-load-more').addClass('hidden');
111
+ }
112
+ }
113
+
114
+ function org_update_counter(total, loaded) {
115
+ var count = `Showing <strong>${total} - ${loaded + 1}</strong> of <strong>${total}</strong>`;
116
+ $('#org-counter').html(count);
117
+ }
118
+
119
+ function org_load_chunk(start) {
120
+ var limit = 100;
121
+ var offset = Math.max(start, limit) - limit;
122
+
123
+ // If we're on the last "chunk", there may be less than 'limit' jobs left to
124
+ // load. In that case we need to adjust the limit; we can cheat and use the
125
+ // currently set global offset which happens to be the total remainder of
126
+ // the jobs.
127
+ if( offset == 0){
128
+ limit = gblOffset;
129
+ }
130
+ console.log(`offset=${offset} limit=${limit} gblOffset=${gblOffset}`);
131
+
132
+ // Remember the current offset
133
+ gblOffset = offset;
134
+
135
+ // Update the counter
136
+ org_update_counter(gblTotal, gblOffset);
137
+
138
+ var url = `${gblOrganizationURL}/jobs?limit=${limit}&offset=${offset}`;
139
+
140
+ // Apply any search terms
141
+ var search = window.search;
142
+ if( ! $.isEmptyObject(search) ){
143
+ for( var s in search ){
144
+ url += `&${s}=${search[s]}`;
145
+ }
146
+ }
147
+ console.log(`chunk url=${url}`);
148
+
149
+ api_get(url, gblUsername, function(jobs) { org_update_job_list(jobs, true); }, org_job_list_failed);
150
+ }
151
+
152
+ function org_apply_updates(stats) {
153
+ // Are there any new jobs?
154
+ var count = stats.total - gblTotal;
155
+ if( count > 0 ){
156
+ console.log(`loading ${count} new jobs...`);
157
+
158
+ var url = `${gblOrganizationURL}/jobs?limit=${count}&offset=${gblTotal}`;
159
+ api_get(url, gblUsername, function(jobs) { org_update_job_list(jobs, false); }, org_job_list_failed);
160
+
161
+ gblTotal = stats.total;
162
+
163
+ // Update the counter
164
+ org_update_counter(gblTotal, gblOffset);
165
+ }
166
+ }
167
+
168
+ function org_apply_indicator_update(job, active, idx) {
169
+ console.log(`callback for job #${active.job_id}: old status is ${active.status}, new status is ${job.status}`);
170
+ if( job.status == 0 ){
171
+ console.log(`got a 0 status: ${JSON.stringify(job)}`);
172
+ }
173
+
174
+ if( job.status != active.status ){
175
+ console.log(`job #${job.job_id} status changed from ${active.status} to ${job.status}`);
176
+
177
+ var indicator = ji_job_status_to_indicator(job.status);
178
+ $(`#row${job.job_id} > #status`).html(indicator);
179
+
180
+ // Did the job finish? If so we can stop watching it
181
+ if( ji_job_finished(job.status) ) {
182
+ console.log(`job #${job.job_id} has finished; removing from active_jobs at position ${idx}`);
183
+ window.active_jobs.splice(idx, 1);
184
+ } else {
185
+ // Remember the current status
186
+ active.status = job.status;
187
+ window.active_jobs[idx] = active;
188
+ }
189
+ }
190
+ }
191
+
192
+ function org_watch_job_list() {
193
+ // Get the current total number of jobs
194
+ var url = `${gblOrganizationURL}/jobs?stats_only=true`;
195
+
196
+ // Apply any search terms
197
+ var search = window.search;
198
+ if( ! $.isEmptyObject(search) ){
199
+ for( var s in search ){
200
+ url += `&${s}=${search[s]}`;
201
+ }
202
+ }
203
+
204
+ api_get(url, gblUsername, org_apply_updates, org_job_list_failed);
205
+
206
+ // Check the status of any current jobs
207
+ var length = window.active_jobs.length;
208
+ for( var idx = 0; idx < length; idx++ ){
209
+ var active = window.active_jobs[idx];
210
+
211
+ console.log(`checking status of job #${active.job_id}: current status is ${active.status}`);
212
+
213
+ (function(a, i){
214
+ url = `${gblOrganizationURL}/jobs/${active.job_id}/status`
215
+ api_get(url,
216
+ gblUsername,
217
+ function(job) { org_apply_indicator_update(job, a, i); },
218
+ org_job_list_failed);
219
+ })(active, idx);
220
+ }
221
+ }
222
+
223
+ function org_initialize_job_list(stats) {
224
+ console.log(stats);
225
+
226
+ gblTotal = stats.total;
227
+ gblOffset = gblTotal;
228
+
229
+ // Load the first set of jobs
230
+ org_load_chunk(gblTotal, 100);
231
+
232
+ // Watch for any new jobs
233
+ addNamedInterval('job_list', org_watch_job_list, 3000);
234
+ }
235
+
236
+ function org_clear_job_list() {
237
+ // Close any open job views
238
+ $('.collapse.in').collapse('hide');
239
+
240
+ // Remove any timers
241
+ clearAllNamedIntervals();
242
+
243
+ // Clear the active jobs list
244
+ window.active_jobs = [];
245
+
246
+ // Hide the "Load more" button if it's visible
247
+ $('#org-load-more').addClass('hidden');
248
+
249
+ // Clear the job list
250
+ $('#job-accordian tbody').empty();
251
+
252
+ // Reset job counts
253
+ gblTotal = 0;
254
+ gblOffset = 0;
255
+
256
+ // Reset the "x of y loaded" counter
257
+ org_update_counter(gblTotal, gblOffset);
258
+ }
259
+
260
+ function org_search_form_get() {
261
+ var name = $('#search_name').val();
262
+ var from = $('#search_from').val();
263
+ var to = $('#search_to').val();
264
+ var status = $('#search_status').val();
265
+
266
+ console.log(`name=${name} from=${from} to=${to} status=${status}`);
267
+
268
+ var search = {};
269
+ if( name != '' )
270
+ search['s_name'] = name;
271
+ if( from != '' )
272
+ search['s_from'] = new Date(from).toISOString();
273
+ if( to != '' )
274
+ search['s_to'] = new Date(to).toISOString();
275
+ if( status != 'Any' )
276
+ search['s_status'] = status;
277
+
278
+ return search;
279
+ }
280
+
281
+ function org_search_submit() {
282
+ var search = org_search_form_get();
283
+ console.log(`search=${search}`);
284
+
285
+ if( ! $.isEmptyObject(search) ){
286
+ // Remember the search terms that are being used
287
+ window.search = search;
288
+
289
+ // Reset the job list
290
+ org_clear_job_list();
291
+
292
+ // Find the number of jobs & load the initial set
293
+ var url = `${gblOrganizationURL}/jobs?stats_only=true`;
294
+
295
+ for( var s in search ){
296
+ url += `&${s}=${search[s]}`;
297
+ }
298
+ console.log(`search url=${url}`);
299
+
300
+ // Load the intial set of jobs
301
+ api_get(url, gblUsername, org_initialize_job_list, org_job_list_failed);
302
+ }
303
+ }
304
+
305
+ function org_search_form_reset() {
306
+ var search = window.search;
307
+ console.log(`search=${search}`);
308
+
309
+ // Don't do anything if the form is already clear
310
+ if( ! $.isEmptyObject(search) ){
311
+ $('#search_name').val('');
312
+ $('#search_from').val('');
313
+ $('#search_to').val('');
314
+ $('#search_status').val('Any');
315
+
316
+ $('#search_btn_clear').prop('disabled', true);
317
+ $('#search_btn_search').prop('disabled', true);
318
+
319
+ // Clear any saved search terms
320
+ window.search = {};
321
+
322
+ // Reset the job list
323
+ org_clear_job_list();
324
+
325
+ // Load the jobs from the start
326
+ var url = `${gblOrganizationURL}/jobs?stats_only=true`;
327
+ api_get(url, gblUsername, org_initialize_job_list, org_job_list_failed);
328
+ }
329
+ }
330
+
331
+ function org_search_form_changed() {
332
+ var search = org_search_form_get();
333
+ console.log(`search=${search}`);
334
+
335
+ // Enable or disable the Search & Clear buttons
336
+ if( $.isEmptyObject(search) ){
337
+ console.log('disabling');
338
+ $('#search_btn_clear').prop('disabled', true);
339
+ $('#search_btn_search').prop('disabled', true);
340
+ } else {
341
+ console.log('enabling');
342
+ $('#search_btn_clear').prop('disabled', false);
343
+ $('#search_btn_search').prop('disabled', false);
344
+ }
345
+ }
@@ -0,0 +1,145 @@
1
+ function user_show_error(msg){
2
+ $('#user_failure > #error_message').html(msg);
3
+ $('#user_failure').removeClass('hidden');
4
+ }
5
+
6
+ function user_get_failed(xhr){
7
+ var failure_message = `Failed to retrieve user details<br>
8
+ <strong>${xhr.status}:</strong> ${xhr.responseText}`;
9
+ failure_message = `Get failed: ${xhr.status}`;
10
+
11
+ user_show_error(failure_message);
12
+ }
13
+
14
+ function user_update_details(user){
15
+ console.log(JSON.stringify(user));
16
+
17
+ $('#user_heading').text(user.username);
18
+
19
+ name = user.name || user.username;
20
+ $('#user_name').text(name);
21
+ $('#user_email').text(user.email);
22
+
23
+ var length = user.organizations.length;
24
+ if( length > 0 ){
25
+ for(var i=0; i < length; i++){
26
+ var org = user.organizations[i];
27
+ var org_link = `<a href="#" id="user_config_${org}" download="${org}"><i class="fa fa-download" aria-hidden="true"></i>&nbsp;${org}</a><br>`;
28
+ $('#user_org_list').append(org_link);
29
+
30
+ var config_org = $(`#user_config_${org}`);
31
+ config_org.data('org', org);
32
+ config_org.click(function(e) {
33
+ var org = $(this).data('org');
34
+
35
+ var config = `server: ${gblAPIURL}\n` +
36
+ `organization: ${org}\n` +
37
+ `username: ${user.username}\n` +
38
+ `secret: `;
39
+
40
+ console.log(`${org} was clicked: ${config}`);
41
+
42
+ this.href = "data:application/x-yaml," + encodeURIComponent(config);
43
+ });
44
+ }
45
+ } else {
46
+ $('#user_org_list').append('<em>None</em>');
47
+ }
48
+
49
+ $('#user_info').removeClass('hidden');
50
+ }
51
+
52
+ function user_password_secret_toggle() {
53
+ var secret = $('#user_password_secret');
54
+
55
+ // Toggle the disabled state on the 'secret' input; generate a new secret
56
+ // if one is required.
57
+ secret.prop('disabled', function(i, v) {
58
+ var key = secret.val();
59
+ if(v && !key){
60
+ // The key does not need to be cryptographically strong; just suitably
61
+ // unique
62
+ var key1 = Math.random().toString(36).substr(2,34);
63
+ var key2 = Math.random().toString(36).substr(2,34);
64
+
65
+ key = $.md5(key1 + key2);
66
+ secret.val(key);
67
+ }
68
+ return !v;
69
+ });
70
+ }
71
+
72
+ function user_password_form_reset() {
73
+ $('#user_password_failure').addClass('hidden');
74
+ $('#user_password_password_1').val('');
75
+ $('#user_password_password_2').val('');
76
+ $('#user_password_reset_signing').removeAttr('checked');
77
+ $('#user_password_secret').val('');
78
+ $('#user_password_secret').prop('disabled', true);
79
+ }
80
+
81
+ function user_password_update_failed(xhr){
82
+ var failure_message = `Failed to change password<br>
83
+ <strong>${xhr.status}:</strong> ${xhr.responseText}`;
84
+ failure_message = `Put failed: ${xhr.status}`;
85
+
86
+ user_show_error(failure_message);
87
+ }
88
+
89
+ function user_password_validate_submit() {
90
+ var password_1 = $('#user_password_password_1').val();
91
+ var password_2 = $('#user_password_password_2').val();
92
+ var change_secret = $('#user_password_reset_signing').is(':checked');
93
+ var secret = $('#user_password_secret').val();
94
+
95
+ console.log(`1=${password_1} 2=${password_2} change_secret=${change_secret} secret=${secret}`);
96
+
97
+ var post_data = {};
98
+
99
+ // Check that the password is valid
100
+ if(password_1 == '' || password_2 == '') {
101
+ // Show an error message
102
+ console.log('password is empty');
103
+ $('#user_password_error_message').text('Password cannot be empty');
104
+ $('#user_password_failure').removeClass('hidden');
105
+
106
+ return false;
107
+ } else if(password_1 != password_2) {
108
+ // Show an error message
109
+ console.log('passwords do not match');
110
+ $('#user_password_error_message').text('Passwords do not match');
111
+ $('#user_password_failure').removeClass('hidden');
112
+
113
+ return false;
114
+ } else {
115
+ post_data['new_password'] = password_1;
116
+ }
117
+
118
+ if(change_secret){
119
+ if( secret == '' ){
120
+ // Show an error message
121
+ console.log('secret is empty');
122
+ $('#user_password_error_message').text('Client token cannot be empty');
123
+ $('#user_password_failure').removeClass('hidden');
124
+
125
+ return false;
126
+ } else {
127
+ post_data['secret'] = secret;
128
+ }
129
+ }
130
+
131
+ console.log(`url=${gblUserURL} data=${JSON.stringify(post_data)}`);
132
+
133
+ // Update the authentication details
134
+ api_put(gblUserURL, post_data, gblUsername, null, user_password_update_failed);
135
+ }
136
+
137
+ $(document).ready(function() {
138
+ $('#user_password_reset_signing').click(user_password_secret_toggle);
139
+
140
+ // Reset the password form every time it's opened
141
+ $('#user_password_modal').on('show.bs.modal', user_password_form_reset);
142
+
143
+ // Validate & submit the password form
144
+ $('#user_password_ok').click(user_password_validate_submit);
145
+ });