foist
[kismet-logviewer.git] / logviewer / static / js / jquery.kismet.devicedata.js
1 // Map a json struct into a simple table
2
3 /* Fields is an array, processed in order, of:
4     {
5         "field": "..." // Field spec
6         "title": "..." // title text
7
8         "help": "..." // Help / explanatory text
9
10         Options will contain AT LEAST
11         'key' - current field key
12         'data' - current data
13         'value' - resolved value
14         'basekey' - string key of base in an iteration
15         'base' - resolved object base
16         and may also include
17         'index' - iteration index
18         'container' - parent container for draw functions
19         'sanitize' - sanitize HTML of content, default true
20
21         Optional function for filtering if we display this entity, returns
22         boolean for display.  
23         "filter": function(opts) { return bool }
24
25         Optional shortcut filters for common filtering options
26         "filterOnEmpty": boolean // Filter this row if the value does not exist,
27         or exists, is a string, and is empty ('')
28         "filterOnZero": boolean // Filter this row if the value does not exist,
29         or exists, is a number, and is equal to 0
30
31         Subgroups (nested table of a subset of fields)
32
33         Indicates we have a subgroup.  Title is string, or function
34         returning a string
35         "groupTitle": string | function(opts)
36         "fields": [...] // Additional nested fields w/in the subgroup
37
38         Iterative groups (vectors and dictionaries of multiple values,
39         the fields group is applied to each index
40         "groupIterate": boolean // Do we iterate over an index of the field
41         and apply fields to each?
42         "iterateTitle": string|function(opts) // Fixed string
43         or optional function for each index
44         each index.
45         "fields": [...] // Additional nested fields which will be indexed 
46         and grouped.
47
48         When using iterator groups, field references should be based on the 
49         inner fields, ie a top-level array field of foo.bar.array containing
50         foo.bar.array[x].val1, foo.bar.array[x].val2, the sub group of 
51         fields should reference fields as 'val1' and 'val2' to get automatically
52         indexed by reference
53
54         // A storage object passed to render and draw in opts['storage'], for 
55         // holding js-scope variables
56         "storage": {}
57
58         // Perform live updates on this field by calling draw() when new data is
59         // available
60         "liveupdate": bool
61
62         // Optional string or function for rendering that should return html, taking
63         // the original key, data, and resolved value; this is used to create any 
64         // necessary wrapper objects.
65         "render": string | function(opts) {}  
66
67         // Optional function for drawing data, called repeatedly as data is updated; 
68         // This function can return nothing and manipulate only the content it is
69         // given, or it can return a string or object which replaces the current 
70         // content of the cell
71         "draw": function(opts) {}
72
73         // Optional function for
74
75         "emtpy": string | function(opts) 
76         Text to be substituted when there is no value
77     }
78 */
79
80 (function ($) {
81     function showitemhelp(item, title) {
82         var h = $(window).height() / 3;
83         var w = $(window).width() / 2;
84
85         if (w < 450) 
86             w = $(window).width() - 5;
87
88         if (h < 200)
89             h = $(window).height() - 5;
90
91         $.jsPanel({
92                 id: "item-help",
93                 headerTitle: title,
94                 headerControls: {
95                     controls: 'closeonly',
96                     iconfont: 'jsglyph',
97                 },
98                 paneltype: 'modal',
99                 content: '<div style="padding: 10px;"><h3>' + title + '</h3><p>' + item['help'],
100             })
101             .resize({
102                 width: w,
103                 height: h
104             })
105             .reposition({
106                 my: 'center',
107                 at: 'center',
108                 of: 'window'
109             });
110     }
111
112     function make_help_func(item, title) {
113         return function() { showitemhelp(item, title); };
114     }
115
116     $.fn.devicedata = function(data, options) {
117         var settings = $.extend({
118             "stripe": true,
119             "id": "kismetDeviceData",
120             "fields": [],
121             "baseobject": "",
122             "sanitize": true,
123             "storage": {},
124         }, options);
125
126         var subtable = $('table.kismet_devicedata#' + kismet.sanitizeId(settings['id']), this);
127
128         // Do we need to make a table to hold our stuff?
129         if (subtable.length == 0) {
130             subtable = $('<table />', {
131                     "id": kismet.sanitizeId(settings['id']),
132                     "class": "kismet_devicedata",
133                     "width": "100%",
134                 });
135             this.append(subtable);
136         }
137
138         settings.fields.forEach(function(v, index, array) {
139             var id;
140             var liveupdate = false;
141
142             if ('id' in v)
143                 id = kismet.sanitizeId(settings.baseobject + v['id']);
144             else
145                 id = kismet.sanitizeId(settings.baseobject + v['field']);
146
147             if ('liveupdate' in v)
148                 liveupdate = v['liveupdate'];
149
150             // Do we have a function for rendering this?
151             var d = kismet.ObjectByString(data, settings.baseobject + v['field']);
152
153             var callopts = {
154                 key: v['field'],
155                 basekey: settings.baseobject,
156                 base: kismet.ObjectByString(data, settings.baseobject),
157                 data: data,
158                 value: d,
159                 id: id,
160                 storage: settings['storage']
161             };
162
163             if ('index' in settings) {
164                 callopts['index'] = settings['index'];
165             }
166
167             if ('filter' in v && typeof(v['filter']) === 'function') {
168                 if (!(v['filter'](callopts))) {
169                     return;
170                 }
171             }
172
173             if ('filterOnEmpty' in v && v['filterOnEmpty'] && 
174                     (typeof(d) === 'undefined' ||
175                      (typeof(d) === 'string' && d.length == 0))) {
176                 return;
177             }
178
179             if ('filterOnZero' in v && v['filterOnZero'] &&
180                     (typeof(d) === 'undefined' ||
181                      (typeof(d) === 'number' && d == 0) ||
182                      typeof(d) === 'string' && d === '0')) {
183                 return;
184             }
185
186             // Do we have a sub-group or group list?
187             if ('groupTitle' in v) {
188                 var drow = $('tr.kismet_devicedata_grouptitle#tr_' + id, subtable);
189
190                 if (drow.length == 0) {
191                     drow = $('<tr>', {
192                         class: 'kismet_devicedata_grouptitle',
193                         id: 'tr_' + id
194                     });
195
196                     subtable.append(drow);
197
198                     var cell = $('<td>', {
199                         class: 'kismet_devicedata_span',
200                         colspan: 2
201                     });
202
203                     drow.append(cell);
204
205                     var contentdiv = $('<div>', {
206                         id: 'cd_' + id
207                     });
208
209                     callopts['container'] = contentdiv;
210                     callopts['cell'] = cell;
211                     callopts['containerid'] = 'cd_' + id;
212
213                     var gt = "";
214
215                     if (typeof(v['groupTitle']) === 'string')
216                         gt = v['groupTitle'];
217                     else if (typeof(v['groupTitle']) === 'function')
218                         gt = v['groupTitle'](callopts);
219
220                     cell.append($('<b class="devicedata_subgroup_header">' + gt + '</b>'));
221
222                     if ('help' in v && v['help']) {
223                         fn = make_help_func(v, gt);
224
225                         cell.append($('<i>', {
226                             class: 'k_dd_td_help pseudolink fa fa-question-circle'
227                         })
228                             .on('click', fn)
229                         );
230
231                     }
232
233                     cell.append($('<br>'));
234
235                     cell.append(contentdiv);
236
237                     if ('render' in v && typeof(v.render) === 'function') {
238                         contentdiv.html(v.render(callopts));
239                     }
240                 } else if (!liveupdate) {
241                     var contentdiv = $('div#cd_' + id, drow);
242
243                     if ('groupField' in v) {
244                         if (typeof(v['groupField']) === 'string')
245                             v['baseobject'] = settings.baseobject + v['groupField'] + "/";
246                     }
247                     contentdiv.devicedata(data, v);
248                     return;
249                 }
250
251                 var cell = $('td', drow);
252                 var contentdiv = $('div#cd_' + id, drow);
253
254                 callopts['container'] = cell;
255                 callopts['cell'] = cell;
256                 callopts['containerid'] = 'cd_' + id;
257
258                 // Recursively fill in the div with the sub-settings
259                 if ('groupField' in v) {
260                     if (typeof(v['groupField']) === 'string')
261                         v['baseobject'] = settings.baseobject + v['groupField'] + "/";
262                 }
263
264                 contentdiv.devicedata(data, v);
265
266                 // Apply the draw function after the subgroup is created
267                 if ('draw' in v && typeof(v.draw) === 'function') {
268                     var r = v.draw(callopts);
269
270                     if (typeof(r) !== 'undefined' && typeof(r) !== 'none') 
271                         cell.html(r);
272                 }
273
274                 return;
275             }
276
277             // Iterative group
278             if ('groupIterate' in v && v['groupIterate'] == true) {
279                 for (var idx in d) {
280                     // index the subobject
281                     v['baseobject'] = `${v['field']}[${idx}]/`;
282                     v['index'] = idx;
283
284                     callopts['index'] = idx;
285                     callopts['basekey'] = `${v['field']}[${idx}]/`;
286                     callopts['base'] = kismet.ObjectByString(data, callopts['basekey']);
287
288                     var subid = kismet.sanitizeId(`${id}[${idx}]`);
289                     callopts['id'] = subid;
290
291                     var drow = $('tr.kismet_devicedata_groupdata#tr_' + subid, subtable);
292
293                     if (drow.length == 0) {
294                         drow = $('<tr>', {
295                             class: 'kismet_devicedata_groupdata',
296                             id: 'tr_' + subid
297                         });
298
299                         subtable.append(drow);
300
301                         var cell = $('<td>', {
302                             class: 'kismet_devicedata_span', 
303                             colspan: 2
304                         });
305
306                         drow.append(cell);
307
308                         // Make the content div for it all the time
309                         var contentdiv = $('<div>', {
310                             id: 'cd_' + subid
311                         });
312
313                         callopts['container'] = contentdiv;
314                         callopts['cell'] = cell;
315                         callopts['containerid'] = 'cd_' + subid;
316
317                         // If we have a title, make a span row for it
318                         if ('iterateTitle' in v) {
319                             // console.log('iteratetitle', subid);
320
321                             var title_span = $('<span>');
322                             callopts['title'] = title_span;
323
324                             if (typeof(v['iterateTitle']) === 'string')
325                                 title_span.html(v['iterateTitle']);
326                             else if (typeof(v['iterateTitle']) === 'function')
327                                 title_span.html(it = v['iterateTitle'](callopts));
328
329                             cell.append($('<b>', {
330                                 'class': 'devicedata_subgroup_header'
331                             }).append(title_span))
332
333                             cell.append($('<br />'));
334                         }
335
336                         cell.append(contentdiv);
337
338                         if ('render' in v && typeof(v.render) === 'function') {
339                             contentdiv.html(v.render(callopts));
340                         }
341                     } else if (!liveupdate) {
342                         var contentdiv = $('div#cd_' + subid, drow);
343                         contentdiv.devicedata(data, v);
344
345                         return;
346                     }
347
348                     var cell = $('td', drow);
349                     var contentdiv = $('div#cd_' + subid, drow);
350
351                     callopts['cell'] = cell;
352                     callopts['container'] = contentdiv;
353                     callopts['containerid'] = 'cd_' + subid;
354
355                     contentdiv.devicedata(data, v);
356
357                     // Apply the draw function after the iterative group is processed
358                     if ('draw' in v && typeof(v.draw) === 'function') {
359                         var r = v.draw(callopts);
360
361                         if (typeof(r) !== 'undefined' && typeof(r) !== 'none') 
362                             contentdiv.html(r);
363                     }
364                 }
365
366                 return;
367             }
368
369             // Standard row
370             var drow = $('tr.kismet_devicedata_groupdata#tr_' + id, subtable);
371
372             if (drow.length == 0) {
373                 drow = $('<tr>', {
374                     class: 'kismet_devicedata_groupdata',
375                     id: 'tr_' + id
376                 });
377
378                 var td;
379
380                 if (v["span"]) {
381                     td = $('<td>', {
382                         colspan: 2,
383                         class: 'kismet_devicedata_span kismet_devicedata_td_content'
384                     });
385                     drow.append(td);
386                 } else {
387                     var title = $('<td>', {
388                         class: 'kismet_devicedata_td_title'
389                     });
390                     var content = $('<td>', {
391                         class: 'kismet_devicedata_td_content'
392                     });
393
394                     td = content;
395
396                     drow.append(title);
397                     drow.append(content);
398
399                     title.html(v['title']);
400
401                     if (v['help']) {
402                         fn = make_help_func(v, v['title']);
403
404                         title.append($('<i>', {
405                             class: 'k_dd_td_help pseudolink fa fa-question-circle'
406                         })
407                             .on('click', fn)
408                         );
409
410                     }
411                 }
412
413                 if ('render' in v) {
414                     if (typeof(v['render']) === 'function') {
415                         td.html(v['render'](callopts));
416                     } else if (typeof(v['render']) === 'string') {
417                         td.html(v['render']);
418                     }
419
420                 }
421
422                 subtable.append(drow);
423             } else if (!liveupdate) {
424                 return;
425             }
426
427             var td = $('td.kismet_devicedata_td_content', drow);
428
429             // Apply the draw function after the row is created
430             if ('draw' in v && typeof(v.draw) === 'function') {
431                 callopts['container'] = td;
432                 var r = v.draw(callopts);
433
434                 if (typeof(r) !== 'undefined' && typeof(r) !== 'none') 
435                     td.html(r);
436             } else if ('empty' in v && 
437                 (typeof(d) === 'undefined' ||
438                     (typeof(d) !== 'undefined' && d.length == 0))) {
439                 if (typeof(v['empty']) === 'string')
440                     td.html(v['empty']);
441                 else if (typeof(v['empty']) === 'function')
442                     td.html(v['empty'](callopts));
443             } else if ('zero' in v &&
444                 (typeof(d) === 'undefined' ||
445                     (typeof(d) === 'number' && d == 0))) {
446                 if (typeof(v['zero']) === 'string')
447                     td.html(v['zero']);
448                 else if (typeof(v['zero']) === 'function')
449                     td.html(v['zero'](callopts));
450             } else {
451                 td.html(d);
452             }
453
454         }
455         );
456     };
457 }(jQuery));