1
|
|
|
/** global: d3 */ |
2
|
|
|
/** |
3
|
|
|
* Bootstrapping functions, event handling, etc... for application. |
4
|
|
|
*/ |
5
|
|
|
|
6
|
|
|
var jsondash = function() { |
7
|
|
|
var my = { |
8
|
|
|
chart_wall: null |
9
|
|
|
}; |
10
|
|
|
var dashboard_data = null; |
11
|
|
|
var $API_ROUTE_URL = '[name="dataSource"]'; |
12
|
|
|
var $API_PREVIEW = '#api-output'; |
13
|
|
|
var $API_PREVIEW_BTN = '#api-output-preview'; |
14
|
|
|
var $MODULE_FORM = '#module-form'; |
15
|
|
|
var $VIEW_BUILDER = '#view-builder'; |
16
|
|
|
var $ADD_MODULE = '#add-module'; |
17
|
|
|
var $MAIN_CONTAINER = '#container'; |
18
|
|
|
var $EDIT_MODAL = '#chart-options'; |
19
|
|
|
var $DELETE_BTN = '#delete-widget'; |
20
|
|
|
var $DELETE_DASHBOARD = '.delete-dashboard'; |
21
|
|
|
var $SAVE_MODULE = '#save-module'; |
22
|
|
|
var $EDIT_CONTAINER = '#edit-view-container'; |
23
|
|
|
|
24
|
|
|
function addWidget(container, model) { |
25
|
|
|
if(document.querySelector('[data-guid="' + model.guid + '"]')) return d3.select('[data-guid="' + model.guid + '"]'); |
|
|
|
|
26
|
|
|
return d3.select(container).select('div') |
27
|
|
|
.append('div') |
28
|
|
|
.classed({item: true, widget: true}) |
29
|
|
|
.attr('data-guid', model.guid) |
30
|
|
|
.attr('data-refresh', model.refresh) |
31
|
|
|
.attr('data-refresh-interval', model.refreshInterval) |
32
|
|
|
.style('width', model.width + 'px') |
33
|
|
|
.style('height', model.height + 'px') |
34
|
|
|
.html(d3.select('#chart-template').html()) |
35
|
|
|
.select('.widget-title .widget-title-text').text(model.name); |
36
|
|
|
} |
37
|
|
|
|
38
|
|
|
function previewAPIRoute(e) { |
39
|
|
|
e.preventDefault(); |
40
|
|
|
// Shows the response of the API field as a json payload, inline. |
41
|
|
|
$.ajax({ |
42
|
|
|
type: 'get', |
43
|
|
|
url: $($API_ROUTE_URL).val().trim(), |
44
|
|
|
success: function(d) { |
45
|
|
|
$($API_PREVIEW).html(prettyCode(d)); |
46
|
|
|
}, |
47
|
|
|
error: function(d, status, error) { |
48
|
|
|
$($API_PREVIEW).html(error); |
49
|
|
|
} |
50
|
|
|
}); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
function refreshableType(type) { |
54
|
|
|
if(type === 'youtube') return false; |
|
|
|
|
55
|
|
|
return true; |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
function saveModule(e){ |
|
|
|
|
59
|
|
|
var data = jsondash.util.serializeToJSON($($MODULE_FORM).serializeArray()); |
60
|
|
|
var last = $('.modules').find('input').last(); |
|
|
|
|
61
|
|
|
var newfield = $('<input class="form-control" type="text">'); |
62
|
|
|
var id = jsondash.util.guid(); |
63
|
|
|
// Add a unique guid for referencing later. |
64
|
|
|
data['guid'] = id; |
65
|
|
|
// Add family for lookups |
66
|
|
|
data['family'] = $($MODULE_FORM).find('select option:selected').data().family; |
67
|
|
|
if(!data.refresh || !refreshableType(data.type)) data['refresh'] = false; |
|
|
|
|
68
|
|
|
if(!data.override) data['override'] = false; |
|
|
|
|
69
|
|
|
newfield.attr('name', 'module_' + id); |
70
|
|
|
newfield.val(JSON.stringify(data)); |
71
|
|
|
$('.modules').append(newfield); |
72
|
|
|
// Add new visual block to view grid |
73
|
|
|
addWidget($VIEW_BUILDER, data); |
74
|
|
|
// Refit the grid |
75
|
|
|
my.chart_wall.masonry(); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
function isModalButton(e) { |
79
|
|
|
return e.relatedTarget.id === $ADD_MODULE.replace('#', ''); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
function updateEditForm(e) { |
83
|
|
|
var module_form = $($MODULE_FORM); |
84
|
|
|
// If the modal caller was the add modal button, skip populating the field. |
85
|
|
|
if(isModalButton(e)) { |
86
|
|
|
module_form.find('input').each(function(_, input){ |
87
|
|
|
$(input).val(''); |
88
|
|
|
}); |
89
|
|
|
$($API_PREVIEW).empty(); |
90
|
|
|
$($DELETE_BTN).hide(); |
91
|
|
|
return; |
92
|
|
|
} |
93
|
|
|
$($DELETE_BTN).show(); |
94
|
|
|
// Updates the fields in the edit form to the active widgets values. |
95
|
|
|
var data = $(e.relatedTarget).closest('.item.widget').data(); |
96
|
|
|
var guid = data.guid; |
97
|
|
|
var module = getModuleByGUID(guid); |
98
|
|
|
// Update the modal window fields with this one's value. |
99
|
|
|
$.each(module, function(field, val){ |
100
|
|
|
if(field === 'override' || field === 'refresh') { |
101
|
|
|
module_form.find('[name="' + field + '"]').prop('checked', val); |
102
|
|
|
} else { |
103
|
|
|
module_form.find('[name="' + field + '"]').val(val); |
104
|
|
|
} |
105
|
|
|
}); |
106
|
|
|
// Update with current guid for referencing the module. |
107
|
|
|
module_form.attr('data-guid', guid); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
function updateModule(e){ |
|
|
|
|
111
|
|
|
var module_form = $($MODULE_FORM); |
112
|
|
|
// Updates the module input fields with new data by rewriting them all. |
113
|
|
|
var guid = module_form.attr('data-guid'); |
114
|
|
|
var active = getModuleByGUID(guid); |
115
|
|
|
// Update the modules values to the current input values. |
116
|
|
|
module_form.find('input').each(function(_, input){ |
117
|
|
|
var name = $(input).attr('name'); |
118
|
|
|
if(name) { |
119
|
|
|
if(name === 'override' || name === 'refresh') { |
120
|
|
|
// Convert checkbox to json friendly format. |
121
|
|
|
active[name] = $(input).is(':checked'); |
122
|
|
|
} else { |
123
|
|
|
active[name] = $(input).val(); |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
}); |
127
|
|
|
// Update bar chart type |
128
|
|
|
var chart_type = module_form.find('select'); |
129
|
|
|
active[chart_type.attr('name')] = chart_type.val(); |
130
|
|
|
// Clear out module input values |
131
|
|
|
$('.modules').empty(); |
132
|
|
|
$.each(dashboard_data.modules, function(i, module){ |
133
|
|
|
var val = JSON.stringify(module, module); |
134
|
|
|
var input = $('<input type="text" name="module_' + i + '" class="form-control">'); |
135
|
|
|
input.val(val); |
136
|
|
|
$('.modules').append(input); |
137
|
|
|
}); |
138
|
|
|
updateWidget(active); |
139
|
|
|
$($EDIT_CONTAINER).collapse(); |
140
|
|
|
// Refit the grid |
141
|
|
|
my.chart_wall.masonry(); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
function updateWidget(config) { |
145
|
|
|
// Trigger update form into view since data is dirty |
146
|
|
|
// Update visual size to existing widget. |
147
|
|
|
var widget = getModuleWidgetByGUID(config.guid); |
148
|
|
|
loader(widget); |
149
|
|
|
widget.style({ |
150
|
|
|
height: config.height + 'px', |
151
|
|
|
width: config.width + 'px' |
152
|
|
|
}); |
153
|
|
|
widget.select('.widget-title .widget-title-text').text(config.name); |
154
|
|
|
loadWidgetData(widget, config); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
function refreshWidget(e) { |
158
|
|
|
e.preventDefault(); |
159
|
|
|
var container = $(this).closest('.widget'); |
160
|
|
|
var guid = container.attr('data-guid'); |
161
|
|
|
var config = getModuleByGUID(guid); |
162
|
|
|
var widget = addWidget($MAIN_CONTAINER, config); |
163
|
|
|
loadWidgetData(widget, config); |
164
|
|
|
my.chart_wall.masonry(); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
function addChartContainers(container, data) { |
168
|
|
|
for(var name in data.modules){ |
|
|
|
|
169
|
|
|
// Closure to maintain each chart data value in loop |
170
|
|
|
(function(config){ |
171
|
|
|
var config = data.modules[name]; |
|
|
|
|
172
|
|
|
// Add div wrappers for js grid layout library, |
173
|
|
|
// and add title, icons, and buttons |
174
|
|
|
var widget = addWidget(container, config); |
175
|
|
|
// Determine how to load this widget |
176
|
|
|
loadWidgetData(widget, config); |
177
|
|
|
})(data.modules[name]); |
178
|
|
|
} |
179
|
|
|
my.chart_wall.masonry(); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
function getModuleWidgetByGUID(guid) { |
183
|
|
|
return d3.select('.item.widget[data-guid="' + guid + '"]'); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
function getModuleByGUID(guid) { |
187
|
|
|
return dashboard_data.modules.find(function(n){return n['guid'] === guid}); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
function deleteModule(e) { |
191
|
|
|
e.preventDefault(); |
192
|
|
|
if(!confirm('Are you sure?')) return; |
|
|
|
|
193
|
|
|
var guid = $($MODULE_FORM).attr('data-guid'); |
194
|
|
|
var module = getModuleByGUID(guid); |
|
|
|
|
195
|
|
|
// Remove form input and visual widget |
196
|
|
|
$('.modules').find('#' + guid).remove(); |
197
|
|
|
$('.item.widget[data-guid="' + guid + '"]').remove(); |
198
|
|
|
$($EDIT_MODAL).modal('hide'); |
199
|
|
|
// Redraw wall to replace visual 'hole' |
200
|
|
|
my.chart_wall.masonry(); |
201
|
|
|
// Trigger update form into view since data is dirty |
202
|
|
|
$($EDIT_CONTAINER).collapse('show'); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
function addDomEvents() { |
206
|
|
|
// TODO: debounce/throttle |
207
|
|
|
$($API_ROUTE_URL).on('change.charts', previewAPIRoute); |
208
|
|
|
$($API_PREVIEW_BTN).on('click.charts', previewAPIRoute); |
209
|
|
|
// Save module popup form |
210
|
|
|
$($SAVE_MODULE).on('click.charts.module', saveModule); |
211
|
|
|
// Edit existing modules |
212
|
|
|
$($EDIT_MODAL).on('show.bs.modal', updateEditForm); |
213
|
|
|
$('#update-module').on('click.charts.module', updateModule); |
214
|
|
|
// Allow swapping of edit/update events |
215
|
|
|
// for the add module button and form modal |
216
|
|
|
$($ADD_MODULE).on('click.charts', function(){ |
217
|
|
|
$('#update-module') |
218
|
|
|
.attr('id', $SAVE_MODULE.replace('#', '')) |
219
|
|
|
.text('Save module') |
220
|
|
|
.off('click.charts.module') |
221
|
|
|
.on('click.charts', saveModule); |
222
|
|
|
}); |
223
|
|
|
// Allow swapping of edit/update events |
224
|
|
|
// for the edit button and form modal |
225
|
|
|
$('.widget-edit').on('click.charts', function(){ |
226
|
|
|
$($SAVE_MODULE) |
227
|
|
|
.attr('id', 'update-module') |
228
|
|
|
.text('Update module') |
229
|
|
|
.off('click.charts.module') |
230
|
|
|
.on('click.charts', updateModule); |
231
|
|
|
}); |
232
|
|
|
// Add delete button for existing widgets. |
233
|
|
|
$($DELETE_BTN).on('click.charts', deleteModule); |
234
|
|
|
// Add delete confirm for dashboards. |
235
|
|
|
$($DELETE_DASHBOARD).on('submit.charts', function(e){ |
236
|
|
|
if(!confirm('Are you sure?')) e.preventDefault(); |
|
|
|
|
237
|
|
|
}); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
function togglePanel(e) { |
|
|
|
|
241
|
|
|
e.preventDefault(); |
242
|
|
|
var el = $(this).attr('href'); |
243
|
|
|
$(el).toggleClass('open'); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
function initGrid(container) { |
|
|
|
|
247
|
|
|
my.chart_wall = $('#container').masonry({ |
248
|
|
|
columnWidth: 5, |
249
|
|
|
itemSelector: '.item', |
250
|
|
|
transitionDuration: 0, |
251
|
|
|
fitWidth: true |
252
|
|
|
}); |
253
|
|
|
$('.item.widget').removeClass('hidden'); |
254
|
|
|
my.chart_wall.masonry(); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
function loader(container) { |
258
|
|
|
container.select('.loader-overlay').classed({hidden: false}); |
259
|
|
|
container.select('.widget-loader').classed({hidden: false}); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
function unload(container) { |
263
|
|
|
container.select('.loader-overlay').classed({hidden: true}); |
264
|
|
|
container.select('.widget-loader').classed({hidden: true}); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
function handleInputs(widget, config) { |
268
|
|
|
var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs'; |
269
|
|
|
// Load event handlers for these newly created forms. |
270
|
|
|
$(inputs_selector).find('form').on('submit', function(e){ |
271
|
|
|
e.stopImmediatePropagation(); |
272
|
|
|
e.preventDefault(); |
273
|
|
|
// Just create a new url for this, but use existing config. |
274
|
|
|
// The global object config will not be altered. |
275
|
|
|
// The first {} here is important, as it enforces a deep copy, |
276
|
|
|
// not a mutation of the original object. |
277
|
|
|
var url = config.dataSource; |
278
|
|
|
// Ensure we don't lose params already save on this endpoint url. |
279
|
|
|
var existing_params = url.split('?')[1]; |
280
|
|
|
var params = getValidParamString($(this).serializeArray()); |
281
|
|
|
var _config = $.extend({}, config, { |
282
|
|
|
dataSource: url.replace(/\?.+/, '') + '?' + existing_params + '&' + params |
283
|
|
|
}); |
284
|
|
|
// Otherwise reload like normal. |
285
|
|
|
loadWidgetData(widget, _config); |
286
|
|
|
// Hide the form again |
287
|
|
|
$(inputs_selector).removeClass('in'); |
288
|
|
|
}); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
function getValidParamString(arr) { |
292
|
|
|
// Jquery $.serialize and $.serializeArray will |
293
|
|
|
// return empty query parameters, which is undesirable and can |
294
|
|
|
// be error prone for RESTFUL endpoints. |
295
|
|
|
// e.g. `foo=bar&bar=` becomes `foo=bar` |
296
|
|
|
var param_str = ''; |
297
|
|
|
arr = arr.filter(function(param, i){return param.value !== '';}); |
|
|
|
|
298
|
|
|
$.each(arr, function(i, param){ |
299
|
|
|
param_str += (param.name + '=' + param.value); |
300
|
|
|
if(i < arr.length - 1 && arr.length > 1) param_str += '&'; |
|
|
|
|
301
|
|
|
}); |
302
|
|
|
return param_str; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
function loadWidgetData(widget, config) { |
306
|
|
|
loader(widget); |
307
|
|
|
try { |
308
|
|
|
// Handle any custom inputs the user specified for this module. |
309
|
|
|
// They map to standard form inputs and correspond to query |
310
|
|
|
// arguments for this dataSource. |
311
|
|
|
if(config.inputs) {handleInputs(widget, config);} |
312
|
|
|
|
313
|
|
|
if(config.type === 'datatable') { |
314
|
|
|
jsondash.handlers.handleDataTable(widget, config); |
315
|
|
|
} |
316
|
|
|
else if(jsondash.util.isSparkline(config.type)) { |
317
|
|
|
jsondash.handlers.handleSparkline(widget, config); |
318
|
|
|
} |
319
|
|
|
else if(config.type === 'iframe') { |
320
|
|
|
jsondash.handlers.handleIframe(widget, config); |
321
|
|
|
} |
322
|
|
|
else if(config.type === 'timeline') { |
323
|
|
|
jsondash.handlers.handleTimeline(widget, config); |
324
|
|
|
} |
325
|
|
|
else if(config.type === 'venn') { |
326
|
|
|
jsondash.handlers.handleVenn(widget, config); |
327
|
|
|
} |
328
|
|
|
else if(config.type === 'number') { |
329
|
|
|
jsondash.handlers.handleSingleNum(widget, config); |
330
|
|
|
} |
331
|
|
|
else if(config.type === 'youtube') { |
332
|
|
|
jsondash.handlers.handleYoutube(widget, config); |
333
|
|
|
} |
334
|
|
|
else if(config.type === 'custom') { |
335
|
|
|
jsondash.handlers.handleCustom(widget, config); |
336
|
|
|
} |
337
|
|
|
else if(config.type === 'plotly-any') { |
338
|
|
|
jsondash.handlers.handlePlotly(widget, config); |
339
|
|
|
} |
340
|
|
|
else if(jsondash.util.isD3Subtype(config)) { |
341
|
|
|
jsondash.handlers.handleD3(widget, config); |
342
|
|
|
} else { |
343
|
|
|
jsondash.handlers.handleC3(widget, config); |
344
|
|
|
} |
345
|
|
|
} catch(e) { |
346
|
|
|
if(console && console.error) console.error(e); |
|
|
|
|
347
|
|
|
unload(widget); |
348
|
|
|
} |
349
|
|
|
addResizeEvent(widget, config); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
function addResizeEvent(widget, config) { |
353
|
|
|
// Add resize event |
354
|
|
|
$(widget[0]).resizable({ |
355
|
|
|
helper: 'resizable-helper', |
356
|
|
|
stop: function(event, ui) { |
357
|
|
|
var active = getModuleByGUID(config.guid); |
|
|
|
|
358
|
|
|
// Update the configs dimensions. |
359
|
|
|
config = $.extend(config, {width: ui.size.width, height: ui.size.height}); |
360
|
|
|
updateModuleInput(config); |
361
|
|
|
loadWidgetData(widget, config); |
362
|
|
|
my.chart_wall.masonry(); |
363
|
|
|
// Open save panel |
364
|
|
|
$($EDIT_CONTAINER).collapse('show'); |
365
|
|
|
} |
366
|
|
|
}); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
function updateModuleInput(config) { |
370
|
|
|
$('input[id="' + config.guid + '"]').val(JSON.stringify(config)); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
function prettyCode(code) { |
374
|
|
|
if(typeof code === "object") return JSON.stringify(code, null, 4); |
|
|
|
|
375
|
|
|
return JSON.stringify(JSON.parse(code), null, 4); |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
function addRefreshers(modules) { |
379
|
|
|
$.each(modules, function(_, module){ |
380
|
|
|
if(module.refresh && module.refreshInterval) { |
381
|
|
|
var container = d3.select('[data-guid="' + module.guid + '"]'); |
382
|
|
|
setInterval(function(){ |
383
|
|
|
loadWidgetData(container, module); |
384
|
|
|
}, parseInt(module.refreshInterval, 10)); |
385
|
|
|
} |
386
|
|
|
}); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
function loadDashboard(data) { |
390
|
|
|
// Load the grid before rendering the ajax, since the DOM |
391
|
|
|
// is rendered server side. |
392
|
|
|
initGrid($MAIN_CONTAINER); |
393
|
|
|
// Add actual ajax data. |
394
|
|
|
addChartContainers($MAIN_CONTAINER, data); |
395
|
|
|
dashboard_data = data; |
396
|
|
|
|
397
|
|
|
// Add event handlers for widget UI |
398
|
|
|
$('.widget-refresh').on('click.charts', refreshWidget); |
399
|
|
|
|
400
|
|
|
// Setup refresh intervals for all widgets that specify it. |
401
|
|
|
addRefreshers(data.modules); |
402
|
|
|
|
403
|
|
|
// Format json config display |
404
|
|
|
$('#json-output').on('show.bs.modal', function(e){ |
|
|
|
|
405
|
|
|
var code = $(this).find('code').text(); |
406
|
|
|
$(this).find('code').text(prettyCode(code)); |
407
|
|
|
}); |
408
|
|
|
|
409
|
|
|
// Reformat the code inside of the raw json field, to pretty print |
410
|
|
|
// for the user. |
411
|
|
|
$('#raw-config').text(prettyCode($('#raw-config').text())); |
412
|
|
|
|
413
|
|
|
// Setup responsive handlers |
414
|
|
|
var jres = jRespond([ |
415
|
|
|
{ |
416
|
|
|
label: 'handheld', |
417
|
|
|
enter: 0, |
418
|
|
|
exit: 767 |
419
|
|
|
} |
420
|
|
|
]); |
421
|
|
|
jres.addFunc({ |
422
|
|
|
breakpoint: 'handheld', |
423
|
|
|
enter: function() { |
424
|
|
|
$('.widget').css({ |
425
|
|
|
'max-width': '100%', |
426
|
|
|
'width': '100%', |
427
|
|
|
'position': 'static' |
428
|
|
|
}); |
429
|
|
|
} |
430
|
|
|
}); |
431
|
|
|
} |
432
|
|
|
my.config = { |
433
|
|
|
WIDGET_MARGIN_X: 20, |
434
|
|
|
WIDGET_MARGIN_Y: 60 |
435
|
|
|
}; |
436
|
|
|
my.loadDashboard = loadDashboard; |
437
|
|
|
my.handlers = {}; |
438
|
|
|
my.util = {}; |
439
|
|
|
my.loader = loader; |
440
|
|
|
my.unload = unload; |
441
|
|
|
my.addDomEvents = addDomEvents; |
442
|
|
|
return my; |
443
|
|
|
}(); |
444
|
|
|
|
Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.
Consider:
If you or someone else later decides to put another statement in, only the first statement will be executed.
In this case the statement
b = 42
will always be executed, while the logging statement will be executed conditionally.ensures that the proper code will be executed conditionally no matter how many statements are added or removed.