2c4265e24717a2746c5486102c6a7b2be5fda3f4
[kismet-logviewer.git] / logviewer / static / js / kismet.ui.js
1 (
2   typeof define === "function" ? function (m) { define("kismet-ui-js", m); } :
3   typeof exports === "object" ? function (m) { module.exports = m(); } :
4   function(m){ this.kismet_ui = m(); }
5 )(function () {
6
7 "use strict";
8
9 var local_uri_prefix = "";
10 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
11     local_uri_prefix = KISMET_URI_PREFIX;
12
13 var exports = {};
14
15 exports.window_visible = true;
16
17 // Load spectrum css and js
18 $('<link>')
19     .appendTo('head')
20     .attr({
21         type: 'text/css',
22         rel: 'stylesheet',
23         href: local_uri_prefix + 'css/spectrum.css'
24     });
25 $('<script>')
26     .appendTo('head')
27     .attr({
28         type: 'text/javascript',
29         src: local_uri_prefix + 'js/spectrum.js'
30     });
31
32
33 exports.last_timestamp = 0;
34
35 // Set panels to close on escape system-wide
36 jsPanel.closeOnEscape = true;
37
38 var device_dt = null;
39
40 var DeviceViews = [
41     {
42         name: "All devices",
43         view: "all",
44         priority: -100000,
45         group: "none"
46     },
47 ];
48
49 /* Add a view option that the user can pick for the main device table;
50  * view is expected to be a component of the /devices/views/ api
51  */
52 exports.AddDeviceView = function(name, view, priority, group = 'none') {
53     DeviceViews.push({name: name, view: view, priority: priority, group: group});
54 }
55
56 exports.BuildDeviceViewSelector = function(element) {
57     var grouped_views = [];
58
59     // Pre-sort the array so that as we build our nested stuff we do it in order
60     DeviceViews.sort(function(a, b) {
61         if (a.priority < b.priority)
62             return -1;
63         if (b.priority > a.priority)
64             return 1;
65
66         return 0;
67     });
68
69     // This isn't efficient but happens rarely, so who cares
70     for (var i in DeviceViews) {
71         if (DeviceViews[i]['group'] == 'none') {
72             // If there's no group, immediately add it to the grouped view
73             grouped_views.push(DeviceViews[i]);
74         } else {
75             // Otherwise look for the group already in the view
76             var existing_g = -1;
77             for (var g in grouped_views) {
78                 if (Array.isArray(grouped_views[g])) {
79                     if (grouped_views[g][0]['group'] == DeviceViews[i]['group']) {
80                         existing_g = g;
81                         break;
82                     }
83                 }
84             }
85
86             // Make a new sub-array if we don't exist, otherwise append to the existing array
87             if (existing_g == -1) {
88                 grouped_views.push([DeviceViews[i]]);
89             } else {
90                 grouped_views[existing_g].push(DeviceViews[i]);
91             }
92         }
93     }
94
95     var selector = 
96         $('<select>', {
97             name: 'devices_views_select',
98             id: 'devices_views_select'
99         });
100
101     for (var i in grouped_views) {
102         if (!Array.isArray(grouped_views[i])) {
103             selector.append(
104                 $('<option>', {
105                     value: grouped_views[i]['view']
106                 }).html(grouped_views[i]['name'])
107             );
108         } else {
109             var optgroup =
110                 $('<optgroup>', {
111                     label: grouped_views[i][0]['group']
112                 });
113
114             for (var og in grouped_views[i]) {
115                 optgroup.append(
116                     $('<option>', {
117                         value: grouped_views[i][og]['view']
118                     }).html(grouped_views[i][og]['name'])
119                 );
120             }
121
122             selector.append(optgroup);
123         }
124     }
125
126     var selected_option = kismet.getStorage('kismet.ui.deviceview.selected', 'all');
127     $('option[value="' + selected_option + '"]', selector).prop("selected", "selected");
128
129     selector.on("selectmenuselect", function(evt, elem) {
130         kismet.putStorage('kismet.ui.deviceview.selected', elem.item.value);
131
132         if (device_dt != null) {
133             device_dt.ajax.url(local_uri_prefix + "devices/views/" + elem.item.value + "/devices.json");
134         }
135     });
136
137     element.empty().append(selector);
138
139     selector.selectmenu()
140         .selectmenu("menuWidget")
141         .addClass("selectoroverflow");
142 }
143
144 // Local maps of views for phys and datasources we've already added
145 var existing_views = {};
146 var view_list_updater_tid = 0;
147
148 function deviceview_selector_dynamic_update() {
149     clearTimeout(view_list_updater_tid);
150     view_list_updater_tid = setTimeout(deviceview_selector_dynamic_update, 5000);
151
152     if (!exports.window_visible)
153         return;
154
155     var ds_priority = -5000;
156     var phy_priority = -1000;
157
158     $.get(local_uri_prefix + "devices/views/all_views.json")
159         .done(function(data) {
160             var ds_promises = [];
161
162             var f_datasource_closure = function(uuid) {
163                 var ds_promise = $.Deferred();
164
165                 $.get(local_uri_prefix + "datasource/by-uuid/" + uuid + "/source.json")
166                 .done(function(dsdata) {
167                     var dsdata = kismet.sanitizeObject(dsdata);
168                     var synth_view = 'seenby-' + dsdata['kismet.datasource.uuid'];
169
170                     existing_views[synth_view] = 1;
171
172                     exports.AddDeviceView(dsdata['kismet.datasource.name'], synth_view, ds_priority, 'Datasources');
173                     ds_priority = ds_priority - 1;
174                 })
175                 .always(function() {
176                     ds_promise.resolve();
177                 });
178
179                 return ds_promise.promise();
180             };
181
182             data = kismet.sanitizeObject(data);
183
184             for (var v in data) {
185                 if (data[v]['kismet.devices.view.id'] in existing_views)
186                     continue;
187
188                 if (data[v]['kismet.devices.view.id'].substr(0, 7) === 'seenby-') {
189                     var uuid = data[v]['kismet.devices.view.id'].substr(7);
190                     ds_promises.push(f_datasource_closure(uuid));
191                     // ds_promises.push($.get(local_uri_prefix + "datasource/by-uuid/" + uuid + "/source.json"));
192                 }
193
194                 if (data[v]['kismet.devices.view.id'].substr(0, 4) === 'phy-') {
195                     existing_views[data[v]['kismet.devices.view.id']] = 1;
196                     exports.AddDeviceView(data[v]['kismet.devices.view.description'], data[v]['kismet.devices.view.id'], phy_priority, 'Phy types');
197                     phy_priority = phy_priority - 1;
198                 }
199             }
200
201             // Complete all the DS queries
202             $.when(ds_promises).then(function(pi) {
203                 ;
204             })
205             .done(function() {
206                 // Skip generating this round if the menu is open
207                 if ($("div.viewselector > .ui-selectmenu-button").hasClass("ui-selectmenu-button-open")) {
208                     ;
209                 } else {
210                     exports.BuildDeviceViewSelector($('div.viewselector'));
211                 }
212             });
213         });
214 }
215 deviceview_selector_dynamic_update();
216
217 // List of datatable columns we have available
218 var DeviceColumns = new Array();
219
220 // Device row highlights, consisting of fields, function, name, and color
221 var DeviceRowHighlights = new Array();
222
223 /* Add a jquery datatable column that the user can pick from, with various
224  * options:
225  *
226  * sTitle: datatable column title
227  * name: datatable 'name' field (optional)
228  * field: Kismet field path, array pair of field path and name, array of fields,
229  *  or a function returning one of the above.
230  * fields: Multiple fields.  When multiple fields are defined, ONE field MUST be defined in the
231  *  'field' parameter.  Additional multiple fields may be defined in this parameter.
232  * renderfunc: string name of datatable render function, taking DT arguments
233  *  (data, type, row, meta), (optional)
234  * drawfunc: string name of a draw function, taking arguments:
235  *  dyncolumn - The dynamic column (this)
236  *  datatable - A DataTable() object of the table we're operating on
237  *  row - The row we're operating on, which should be visible
238  *  This will be called during the drawCallback
239  *  stage of the table, on visible rows. (optional)
240  */
241 exports.AddDeviceColumn = function(id, options) {
242     var coldef = {
243         kismetId: id,
244         sTitle: options.sTitle,
245         field: null,
246         fields: null,
247     };
248
249     if ('field' in options) {
250         coldef.field = options.field;
251     }
252
253     if ('fields' in options) {
254         coldef.fields = options.fields;
255     }
256
257     if ('description' in options) {
258         coldef.description = options.description;
259     }
260
261     if ('name' in options) {
262         coldef.name = options.name;
263     }
264
265     if ('orderable' in options) {
266         coldef.bSortable = options.orderable;
267     }
268
269     if ('visible' in options) {
270         coldef.bVisible = options.visible;
271     } else {
272         coldef.bVisible = true;
273     }
274
275     if ('selectable' in options) {
276         coldef.user_selectable = options.selectable;
277     } else {
278         coldef.user_selectable = true;
279     }
280
281     if ('searchable' in options) {
282         coldef.bSearchable = options.searchable;
283     }
284
285     if ('width' in options)
286         coldef.width = options.width;
287
288     if ('sClass' in options)
289         coldef.sClass = options.sClass;
290
291     var f;
292     if (typeof(coldef.field) === 'string') {
293         var fs = coldef.field.split("/");
294         f = fs[fs.length - 1];
295     } else if (Array.isArray(coldef.field)) {
296         f = coldef.field[1];
297     }
298
299     // Bypass datatable/jquery pathing
300     coldef.mData = function(row, type, set) {
301         return kismet.ObjectByString(row, f);
302     }
303
304     // Datatable render function
305     if ('renderfunc' in options) {
306         coldef.mRender = options.renderfunc;
307     }
308
309     // Set an arbitrary draw hook we call ourselves during the draw loop later
310     if ('drawfunc' in options) {
311         coldef.kismetdrawfunc = options.drawfunc;
312     }
313
314     DeviceColumns.push(coldef);
315 }
316
317 /* Add a row highlighter for coloring rows; expects an options dictionary containing:
318  * name: Simple name
319  * description: Longer description
320  * priority: Priority for assigning color
321  * defaultcolor: rgb default color
322  * defaultenable: optional bool, should be turned on by default
323  * fields: *array* of field definitions, each of which may be a single or two-element
324  *  field definition/path.  A *single* field must still be represented as an array,
325  *  ie, ['some.field.def'].  Multiple fields and complex fields could be represented
326  *  as ['some.field.def', 'some.second.field', ['some.complex/field.path', 'field.foo']]
327  * selector: function(data) returning true for color or false for ignore
328  */
329 exports.AddDeviceRowHighlight = function(options) {
330
331     // Load enable preference
332     var storedenable =
333         kismet.getStorage('kismet.rowhighlight.enable' + options.name, 'NONE');
334
335     if (storedenable === 'NONE') {
336         if ('defaultenable' in options) {
337             options['enable'] = options['defaultenable'];
338         } else {
339             options['enable'] = true;
340         }
341     } else {
342         options['enable'] = storedenable;
343     }
344
345     // Load color preference
346     var storedcolor =
347         kismet.getStorage('kismet.rowhighlight.color' + options.name, 'NONE');
348
349     if (storedcolor !== 'NONE') {
350         options['color'] = storedcolor;
351     } else {
352         options['color'] = options['defaultcolor'];
353     }
354
355     DeviceRowHighlights.push(options);
356
357     DeviceRowHighlights.sort(function(a, b) {
358         if (a.priority < b.priority)
359             return -1;
360         if (b.priority > a.priority)
361             return 1;
362
363         return 0;
364     });
365 }
366
367 /* Return columns from the selected list of column IDs */
368 exports.GetDeviceColumns = function(showall = false) {
369     var ret = new Array();
370
371     var order = kismet.getStorage('kismet.datatable.columns', []);
372
373     // If we don't have an order saved
374     if (order.length == 0) {
375         // Sort invisible columns to the end
376         for (var i in DeviceColumns) {
377             if (!DeviceColumns[i].bVisible)
378                 continue;
379             ret.push(DeviceColumns[i]);
380         }
381         for (var i in DeviceColumns) {
382             if (DeviceColumns[i].bVisible)
383                 continue;
384             ret.push(DeviceColumns[i]);
385         }
386         return ret;
387     }
388
389     // Otherwise look for all the columns we have enabled
390     for (var oi in order) {
391         var o = order[oi];
392
393         if (!o.enable)
394             continue;
395
396         // Find the column that matches the ID in the master list of columns
397         var dc = DeviceColumns.find(function(e, i, a) {
398             if (e.kismetId === o.id)
399                 return true;
400             return false;
401         });
402
403         if (dc != undefined && dc.user_selectable) {
404             dc.bVisible = true;
405             ret.push(dc);
406         }
407     }
408
409     // If we didn't find anything, default to the normal behavior - something is wrong
410     if (ret.length == 0) {
411         // Sort invisible columsn to the end
412         for (var i in DeviceColumns) {
413             if (!DeviceColumns[i].bVisible)
414                 continue;
415             ret.push(DeviceColumns[i]);
416         }
417         for (var i in DeviceColumns) {
418             if (DeviceColumns[i].bVisible)
419                 continue;
420             ret.push(DeviceColumns[i]);
421         }
422         return ret;
423     }
424
425     // If we're showing everything, find any other columns we don't have selected,
426     // now that we've added the visible ones in the right order.
427     if (showall) {
428         for (var dci in DeviceColumns) {
429             var dc = DeviceColumns[dci];
430
431             /*
432             if (!dc.user_selectable)
433                 continue;
434                 */
435
436             var rc = ret.find(function(e, i, a) {
437                 if (e.kismetId === dc.kismetId)
438                     return true;
439                 return false;
440             });
441
442             if (rc == undefined) {
443                 dc.bVisible = false;
444                 ret.push(dc);
445             }
446         }
447
448         // Return the list w/out adding the non-user-selectable stuff
449         return ret;
450     }
451
452     // Then append all the columns the user can't select because we need them for
453     // fetching data or providing hidden sorting
454     for (var dci in DeviceColumns) {
455         if (!DeviceColumns[dci].user_selectable) {
456             ret.push(DeviceColumns[dci]);
457         }
458     }
459
460     return ret;
461 }
462
463 // Generate a map of column number to field array so we can tell Kismet what fields
464 // are in what column for sorting
465 exports.GetDeviceColumnMap = function(columns) {
466     var ret = {};
467
468     for (var ci in columns) {
469         var fields = new Array();
470
471         if ('field' in columns[ci]) 
472             fields.push(columns[ci]['field']);
473
474         if ('fields' in columns[ci])
475             fields.push.apply(fields, columns[ci]['fields']);
476
477         ret[ci] = fields;
478     }
479
480     return ret;
481 }
482
483
484 /* Return field arrays for the device list; aggregates fields from device columns,
485  * widget columns, and color highlight columns.
486  */
487 exports.GetDeviceFields = function(selected) {
488     var rawret = new Array();
489     var cols = exports.GetDeviceColumns();
490
491     for (var i in cols) {
492         if ('field' in cols[i] && cols[i]['field'] != null) 
493             rawret.push(cols[i]['field']);
494
495         if ('fields' in cols[i] && cols[i]['fields'] != null) 
496             rawret.push.apply(rawret, cols[i]['fields']);
497     }
498
499     for (var i in DeviceRowHighlights) {
500         rawret.push.apply(rawret, DeviceRowHighlights[i]['fields']);
501     }
502
503     // De-dupe the list of fields/field aliases
504     var ret = rawret.filter(function(item, pos, self) {
505         return self.indexOf(item) == pos;
506     });
507
508     return ret;
509 }
510
511 exports.AddDetail = function(container, id, title, pos, options) {
512     var settings = $.extend({
513         "filter": null,
514         "render": null,
515         "draw": null
516     }, options);
517
518     var det = {
519         id: id,
520         title: title,
521         position: pos,
522         options: settings
523     };
524
525     container.push(det);
526
527     container.sort(function(a, b) {
528         return a.position - b.position;
529     });
530 }
531
532 exports.DetailWindow = function(key, title, options, window_cb, close_cb) {
533     // Generate a unique ID for this dialog
534     var dialogid = "detaildialog" + key;
535     var dialogmatch = '#' + dialogid;
536
537     if (jsPanel.activePanels.list.indexOf(dialogid) != -1) {
538         jsPanel.activePanels.getPanel(dialogid).front();
539         return;
540     }
541
542     var h = $(window).height() - 5;
543
544     // If we're on a wide-screen browser, try to split it into 3 details windows
545     var w = ($(window).width() / 3) - 10;
546
547     // If we can't, split it into 2.  This seems to look better when people
548     // don't run full-size browser windows.
549     if (w < 450) {
550         w = ($(window).width() / 2) - 5;
551     }
552
553     // Finally make it full-width if we're still narrow
554     if (w < 450) {
555         w = $(window).width() - 5;
556     }
557
558     var panel = $.jsPanel({
559         id: dialogid,
560         headerTitle: title,
561
562         headerControls: {
563             iconfont: 'jsglyph',
564             controls: 'closeonly',
565         },
566
567         position: {
568             "my": "left-top",
569             "at": "left-top",
570             "of": "window",
571             "offsetX": 2,
572             "offsetY": 2,
573             "autoposition": "RIGHT"
574         },
575
576         resizable: {
577             minWidth: 450,
578             minHeight: 400,
579             stop: function(event, ui) {
580                 $('div#accordion', ui.element).accordion("refresh");
581             }
582         },
583
584         onmaximized: function() {
585             $('div#accordion', this.content).accordion("refresh");
586         },
587
588         onnormalized: function() {
589             $('div#accordion', this.content).accordion("refresh");
590         },
591
592         onclosed: function() {
593             close_cb(this, options);
594         },
595
596         callback: function() {
597             window_cb(this, options);
598         },
599     }).resize({
600         width: w,
601         height: h,
602         callback: function(panel) {
603             $('div#accordion', this.content).accordion("refresh");
604         },
605     });
606
607     // Did we creep off the screen in our autopositioning?  Put this panel in
608     // the left if so (or if it's a single-panel situation like mobile, just
609     // put it front and center)
610     if (panel.offset().left + panel.width() > $(window).width()) {
611         panel.reposition({
612             "my": "left-top",
613             "at": "left-top",
614             "of": "window",
615             "offsetX": 2,
616             "offsetY": 2,
617         });
618     }
619
620 }
621
622 exports.DeviceDetails = new Array();
623
624 /* Register a device detail accordion panel, taking an id for the panel
625  * content, a title presented to the user, a position in the list, and
626  * options.  Because details are directly rendered all the time and
627  * can't be moved around / saved as configs like columns can, callbacks
628  * are just direct functions here.
629  *
630  * filter and render take one argument, the data to be shown
631  * filter: function(data) {
632  *  return false;
633  * }
634  *
635  * render: function(data) {
636  *  return "Some content";
637  * }
638  *
639  * draw takes the device data and a container element as an argument:
640  * draw: function(data, element) {
641  *  e.append("hi");
642  * }
643  * */
644 exports.AddDeviceDetail = function(id, title, pos, options) {
645     exports.AddDetail(exports.DeviceDetails, id, title, pos, options);
646 }
647
648 exports.GetDeviceDetails = function() {
649     return exports.DeviceDetails;
650 }
651
652 exports.DeviceDetailWindow = function(key) {
653     exports.DetailWindow(key, "Device Details", 
654         {
655             storage: {}
656         },
657
658         function(panel, options) {
659             var content = panel.content;
660
661             panel.active = true;
662
663             window['storage_devlist_' + key] = {};
664
665             window['storage_devlist_' + key]['foobar'] = 'bar';
666
667             panel.updater = function() {
668                 if (exports.window_visible) {
669                     $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
670                         .done(function(fulldata) {
671                             fulldata = kismet.sanitizeObject(fulldata);
672
673                             panel.headerTitle("Device: " + kismet.censorMAC(fulldata['kismet.device.base.commonname']));
674
675                             var accordion = $('div#accordion', content);
676
677                             if (accordion.length == 0) {
678                                 accordion = $('<div />', {
679                                     id: 'accordion'
680                                 });
681
682                                 content.append(accordion);
683                             }
684
685                             var detailslist = kismet_ui.GetDeviceDetails();
686
687                             for (var dii in detailslist) {
688                                 var di = detailslist[dii];
689
690                                 // Do we skip?
691                                 if ('filter' in di.options &&
692                                     typeof(di.options.filter) === 'function') {
693                                     if (di.options.filter(fulldata) == false) {
694                                         continue;
695                                     }
696                                 }
697
698                                 var vheader = $('h3#header_' + di.id, accordion);
699
700                                 if (vheader.length == 0) {
701                                     vheader = $('<h3>', {
702                                         id: "header_" + di.id,
703                                     })
704                                         .html(di.title);
705
706                                     accordion.append(vheader);
707                                 }
708
709                                 var vcontent = $('div#' + di.id, accordion);
710
711                                 if (vcontent.length == 0) {
712                                     vcontent = $('<div>', {
713                                         id: di.id,
714                                     });
715                                     accordion.append(vcontent);
716                                 }
717
718                                 // Do we have pre-rendered content?
719                                 if ('render' in di.options &&
720                                     typeof(di.options.render) === 'function') {
721                                     vcontent.html(di.options.render(fulldata));
722                                 }
723
724                                 if ('draw' in di.options &&
725                                     typeof(di.options.draw) === 'function') {
726                                     di.options.draw(fulldata, vcontent, options, 'storage_devlist_' + key);
727                                 }
728
729                                 if ('finalize' in di.options &&
730                                     typeof(di.options.finalize) === 'function') {
731                                     di.options.finalize(fulldata, vcontent, options, 'storage_devlist_' + key);
732                                 }
733                             }
734                             accordion.accordion({ heightStyle: 'fill' });
735                         })
736                         .fail(function(jqxhr, texterror) {
737                             content.html("<div style=\"padding: 10px;\"><h1>Oops!</h1><p>An error occurred loading device details for key <code>" + key + 
738                                 "</code>: HTTP code <code>" + jqxhr.status + "</code>, " + texterror + "</div>");
739                         })
740                         .always(function() {
741                             panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
742                         })
743                 } else {
744                     panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
745                 }
746
747             };
748
749             panel.updater();
750
751             new ClipboardJS('.copyuri');
752         },
753
754         function(panel, options) {
755             clearTimeout(panel.timerid);
756             panel.active = false;
757             window['storage_devlist_' + key] = {};
758         });
759
760 };
761
762 exports.RenderTrimmedTime = function(opts) {
763     return (new Date(opts['value'] * 1000).toString()).substring(4, 25);
764 }
765
766 exports.RenderHumanSize = function(opts) {
767     return kismet.HumanReadableSize(opts['value']);
768 };
769
770 // Central location to register channel conversion lists.  Conversion can
771 // be a function or a fixed dictionary.
772 exports.freq_channel_list = { };
773 exports.human_freq_channel_list = { };
774
775 exports.AddChannelList = function(phyname, humanname, channellist) {
776     exports.freq_channel_list[phyname] = channellist;
777     exports.human_freq_channel_list[humanname] = channellist;
778 }
779
780 // Get a list of human frequency conversions
781 exports.GetChannelListKeys = function() {
782     return Object.keys(exports.human_freq_channel_list);
783 }
784
785 // Get a converted channel name, or the raw frequency if we can't help
786 exports.GetConvertedChannel = function(humanname, frequency) {
787     if (humanname in exports.human_freq_channel_list) {
788         var conv = exports.human_freq_channel_list[humanname];
789
790         if (typeof(conv) === "function") {
791             // Call the conversion function if one exists
792             return conv(frequency);
793         } else if (frequency in conv) {
794             // Return the mapped value
795             return conv[frequency];
796         }
797     }
798
799     // Return the frequency if we couldn't figure out what to do
800     return frequency;
801 }
802
803 // Get a converted channel name, or the raw frequency if we can't help
804 exports.GetPhyConvertedChannel = function(phyname, frequency) {
805     if (phyname in exports.freq_channel_list) {
806         var conv = exports.freq_channel_list[phyname];
807
808         if (typeof(conv) === "function") {
809             // Call the conversion function if one exists
810             return conv(frequency);
811         } else if (frequency in conv) {
812             // Return the mapped value
813             return conv[frequency];
814         }
815     }
816
817     // Return the frequency if we couldn't figure out what to do
818     return kismet.HumanReadableFrequency(frequency);
819 }
820
821 exports.connection_error = 0;
822 exports.connection_error_panel = null;
823
824 exports.HealthCheck = function() {
825     var timerid;
826
827     if (exports.window_visible) {
828         $.get(local_uri_prefix + "system/status.json")
829             .done(function(data) {
830                 data = kismet.sanitizeObject(data);
831
832                 if (exports.connection_error && exports.connection_error_panel) {
833                     try {
834                         exports.connection_error_panel.close();
835                         exports.connection_error_panel = null;
836                     } catch (e) {
837                         ;
838                     }
839                 }
840
841                 exports.connection_error = 0;
842
843                 exports.last_timestamp = data['kismet.system.timestamp.sec'];
844             })
845             .fail(function() {
846                 if (exports.connection_error >= 3 && exports.connection_error_panel == null) {
847                     exports.connection_error_panel = $.jsPanel({
848                         id: "connection-alert",
849                         headerTitle: 'Cannot Connect to Kismet',
850                         headerControls: {
851                             controls: 'none',
852                             iconfont: 'jsglyph',
853                         },
854                         contentSize: "auto auto",
855                         paneltype: 'modal',
856                         content: '<div style="padding: 10px;"><h3><i class="fa fa-exclamation-triangle" style="color: red;" /> Sorry!</h3><p>Cannot connect to the Kismet webserver.  Make sure Kismet is still running on this host!<p><i class="fa fa-refresh fa-spin" style="margin-right: 5px" /> Connecting to the Kismet server...</div>',
857                     });
858                 }
859
860                 exports.connection_error++;
861             })
862             .always(function() {
863                 if (exports.connection_error)
864                     timerid = setTimeout(exports.HealthCheck, 1000);
865                 else
866                     timerid = setTimeout(exports.HealthCheck, 5000);
867             }); 
868     } else {
869         if (exports.connection_error)
870             timerid = setTimeout(exports.HealthCheck, 1000);
871         else
872             timerid = setTimeout(exports.HealthCheck, 5000);
873     }
874
875 }
876
877
878 exports.DegToDir = function(deg) {
879     var directions = [
880         "N", "NNE", "NE", "ENE",
881         "E", "ESE", "SE", "SSE",
882         "S", "SSW", "SW", "WSW",
883         "W", "WNW", "NW", "NNW"
884     ];
885
886     var degrees = [
887         0, 23, 45, 68,
888         90, 113, 135, 158,
889         180, 203, 225, 248,
890         270, 293, 315, 338
891     ];
892
893     for (var p = 1; p < degrees.length; p++) {
894         if (deg < degrees[p])
895             return directions[p - 1];
896     }
897
898     return directions[directions.length - 1];
899 }
900
901 // Use our settings to make some conversion functions for distance and temperature
902 exports.renderDistance = function(k, precision = 5) {
903     if (kismet.getStorage('kismet.base.unit.distance') === 'metric' ||
904             kismet.getStorage('kismet.base.unit.distance') === '') {
905         if (k < 1) {
906             return (k * 1000).toFixed(precision) + ' m';
907         }
908
909         return k.toFixed(precision) + ' km';
910     } else {
911         var m = (k * 0.621371);
912
913         if (m < 1) {
914             return (5280 * m).toFixed(precision) + ' feet';
915         }
916         return (k * 0.621371).toFixed(precision) + ' miles';
917     }
918 }
919
920 // Use our settings to make some conversion functions for distance and temperature
921 exports.renderHeightDistance = function(m, precision = 5) {
922     if (kismet.getStorage('kismet.base.unit.distance') === 'metric' ||
923             kismet.getStorage('kismet.base.unit.distance') === '') {
924         if (m < 1000) {
925             return m.toFixed(precision) + ' m';
926         }
927
928         return (m / 1000).toFixed(precision) + ' km';
929     } else {
930         var f = (m * 3.2808399);
931
932         if (f < 5280) {
933             return f.toFixed(precision) + ' feet';
934         }
935         return (f / 5280).toFixed(precision) + ' miles';
936     }
937 }
938
939 exports.renderSpeed = function(kph, precision = 5) {
940     if (kismet.getStorage('kismet.base.unit.speed') === 'metric' ||
941             kismet.getStorage('kismet.base.unit.speed') === '') {
942         return kph.toFixed(precision) + ' KPH';
943     } else {
944         return (kph * 0.621371).toFixed(precision) + ' MPH';
945     }
946 }
947
948 exports.renderTemperature = function(c, precision = 5) {
949     if (kismet.getStorage('kismet.base.unit.temp') === 'celsius' ||
950             kismet.getStorage('kismet.base.unit.temp') === '') {
951         return c.toFixed(precision) + '&deg; C';
952     } else {
953         return (c * (9/5) + 32).toFixed(precision) + '&deg; F';
954     }
955 }
956
957 var deviceTid;
958
959 var devicetableElement = null;
960
961 function ScheduleDeviceSummary() {
962     try {
963         if (exports.window_visible && devicetableElement.is(":visible")) {
964
965             var dt = devicetableElement.DataTable();
966
967             // Save the state.  We can't use proper state saving because it seems to break
968             // the table position
969             kismet.putStorage('kismet.base.devicetable.order', JSON.stringify(dt.order()));
970             kismet.putStorage('kismet.base.devicetable.search', JSON.stringify(dt.search()));
971
972             // Snapshot where we are, because the 'don't reset page' in ajax.reload
973             // DOES still reset the scroll position
974             var prev_pos = {
975                 'top': $(dt.settings()[0].nScrollBody).scrollTop(),
976                 'left': $(dt.settings()[0].nScrollBody).scrollLeft()
977             };
978             dt.ajax.reload(function(d) {
979                 // Restore our scroll position
980                 $(dt.settings()[0].nScrollBody).scrollTop( prev_pos.top );
981                 $(dt.settings()[0].nScrollBody).scrollLeft( prev_pos.left );
982             }, false);
983         }
984
985     } catch (error) {
986         ;
987     }
988     
989     // Set our timer outside of the datatable callback so that we get called even
990     // if the ajax load fails
991     deviceTid = setTimeout(ScheduleDeviceSummary, 2000);
992
993     return;
994 }
995
996 function CancelDeviceSummary() {
997     clearTimeout(deviceTid);
998 }
999
1000 /* Create the device table */
1001 exports.CreateDeviceTable = function(element) {
1002     devicetableElement = element;
1003     var statuselement = $('#' + element.attr('id') + '_status');
1004
1005     var dt = exports.InitializeDeviceTable(element);
1006
1007     dt.draw(false);
1008
1009     // Start the auto-updating
1010     ScheduleDeviceSummary();
1011 }
1012
1013 exports.InitializeDeviceTable = function(element) {
1014     var statuselement = $('#' + element.attr('id') + '_status');
1015
1016     /* Make the fields list json and set the wrapper object to aData to make the DT happy */
1017     var cols = exports.GetDeviceColumns();
1018     var colmap = exports.GetDeviceColumnMap(cols);
1019     var fields = exports.GetDeviceFields();
1020
1021     var json = {
1022         fields: fields,
1023         colmap: colmap,
1024         datatable: true,
1025     };
1026
1027     if ($.fn.dataTable.isDataTable(element)) {
1028         element.DataTable().destroy();
1029         element.empty();
1030     }
1031
1032     element
1033         .on('xhr.dt', function (e, settings, json, xhr) {
1034             json = kismet.sanitizeObject(json);
1035
1036             if (json['recordsFiltered'] != json['recordsTotal'])
1037                 statuselement.html(json['recordsTotal'] + " devices (" + json['recordsFiltered'] + " shown after filter)");
1038             else
1039                 statuselement.html(json['recordsTotal'] + " devices");
1040         } )
1041         .DataTable( {
1042
1043         destroy: true,
1044
1045         scrollResize: true,
1046         scrollY: 200,
1047         scrollX: "100%",
1048
1049         serverSide: true,
1050         processing: true,
1051
1052         dom: '<"viewselector">ft',
1053
1054         deferRender: true,
1055         lengthChange: false,
1056
1057         scroller: {
1058             loadingIndicator: true,
1059         },
1060
1061         // Create a complex post to get our summary fields only
1062         ajax: {
1063             url: local_uri_prefix + "devices/views/" + kismet.getStorage('kismet.ui.deviceview.selected', 'all') + "/devices.json",
1064             data: {
1065                 json: JSON.stringify(json)
1066             },
1067             error: function(jqxhr, status, error) {
1068                 // Catch missing views and reset
1069                 if (jqxhr.status == 404) {
1070                     device_dt.ajax.url(local_uri_prefix + "devices/views/all/devices.json");
1071                     kismet.putStorage('kismet.ui.deviceview.selected', 'all');
1072                     exports.BuildDeviceViewSelector($('div.viewselector'));
1073                 }
1074             },
1075             method: "POST",
1076             timeout: 5000,
1077         },
1078
1079         // Get our dynamic columns
1080         columns: cols,
1081
1082         columnDefs: [
1083             { className: "dt_td", targets: "_all" },
1084         ],
1085
1086         order:
1087             [ [ 0, "desc" ] ],
1088
1089         // Map our ID into the row
1090         createdRow : function( row, data, index ) {
1091             row.id = data['kismet.device.base.key'];
1092         },
1093
1094         // Opportunistic draw on new rows
1095         drawCallback: function( settings ) {
1096             var dt = this.api();
1097
1098             dt.rows({
1099                 page: 'current'
1100             }).every(function(rowIdx, tableLoop, rowLoop) {
1101                 for (var c in DeviceColumns) {
1102                     var col = DeviceColumns[c];
1103
1104                     if (!('kismetdrawfunc' in col)) {
1105                         continue;
1106                     }
1107
1108                     // Call the draw callback if one exists
1109                     try {
1110                         col.kismetdrawfunc(col, dt, this);
1111                     } catch (error) {
1112                         ;
1113                     }
1114                 }
1115
1116                 for (var r in DeviceRowHighlights) {
1117                     try {
1118                         var rowh = DeviceRowHighlights[r];
1119
1120                         if (rowh['enable']) {
1121                             if (rowh['selector'](this.data())) {
1122                                 $('td', this.node()).css('background-color', rowh['color']);
1123                                 break;
1124                             }
1125                         }
1126                     } catch (error) {
1127                         ;
1128                     }
1129                 }
1130             }
1131             );
1132         }
1133
1134     });
1135
1136     device_dt = element.DataTable();
1137     // var dt_base_height = element.height();
1138     
1139     // $('div.viewselector').html("View picker");
1140     exports.BuildDeviceViewSelector($('div.viewselector'));
1141
1142     // Restore the order
1143     var saved_order = kismet.getStorage('kismet.base.devicetable.order', "");
1144     if (saved_order !== "")
1145         device_dt.order(JSON.parse(saved_order));
1146
1147     // Restore the search
1148     var saved_search = kismet.getStorage('kismet.base.devicetable.search', "");
1149     if (saved_search !== "")
1150         device_dt.search(JSON.parse(saved_search));
1151
1152     // Set an onclick handler to spawn the device details dialog
1153     $('tbody', element).on('click', 'tr', function () {
1154         kismet_ui.DeviceDetailWindow(this.id);
1155
1156         // Use the ID above we insert in the row creation, instead of looking in the
1157         // device list data
1158         // Fetch the data of the row that got clicked
1159         // var device_dt = element.DataTable();
1160         // var data = device_dt.row( this ).data();
1161         // var key = data['kismet.device.base.key'];
1162         // kismet_ui.DeviceDetailWindow(key);
1163     } );
1164
1165     $('tbody', element)
1166         .on( 'mouseenter', 'td', function () {
1167             try {
1168                 var device_dt = element.DataTable();
1169
1170                 if (typeof(device_dt.cell(this).index()) === 'Undefined')
1171                     return;
1172
1173                 var colIdx = device_dt.cell(this).index().column;
1174                 var rowIdx = device_dt.cell(this).index().row;
1175
1176                 // Remove from all cells
1177                 $(device_dt.cells().nodes()).removeClass('kismet-highlight');
1178                 // Highlight the td in this row
1179                 $('td', device_dt.row(rowIdx).nodes()).addClass('kismet-highlight');
1180             } catch (e) {
1181
1182             }
1183         } );
1184
1185
1186     return device_dt;
1187 }
1188
1189 exports.ResizeDeviceTable = function(element) {
1190     // console.log(element.height());
1191     // exports.ResetDeviceTable(element);
1192 }
1193
1194 exports.ResetDeviceTable = function(element) {
1195     CancelDeviceSummary();
1196
1197     exports.InitializeDeviceTable(element);
1198
1199     ScheduleDeviceSummary();
1200 }
1201
1202 kismet_ui_settings.AddSettingsPane({
1203     id: 'core_devicelist_columns',
1204     listTitle: 'Device List Columns',
1205     create: function(elem) {
1206
1207         var rowcontainer =
1208             $('<div>', {
1209                 id: 'k-c-p-rowcontainer'
1210             });
1211
1212         var cols = exports.GetDeviceColumns(true);
1213
1214         for (var ci in cols) {
1215             var c = cols[ci];
1216
1217             if (! c.user_selectable)
1218                 continue;
1219
1220             var crow =
1221                 $('<div>', {
1222                     class: 'k-c-p-column',
1223                     id: c.kismetId,
1224                 })
1225                 .append(
1226                     $('<i>', {
1227                         class: 'k-c-p-c-mover fa fa-arrows-v'
1228                     })
1229                 )
1230                 .append(
1231                     $('<div>', {
1232                         class: 'k-c-p-c-enable',
1233                     })
1234                     .append(
1235                         $('<input>', {
1236                             type: 'checkbox',
1237                             id: 'k-c-p-c-enable'
1238                         })
1239                         .on('change', function() {
1240                             kismet_ui_settings.SettingsModified();
1241                             })
1242                     )
1243                 )
1244                 .append(
1245                     $('<div>', {
1246                         class: 'k-c-p-c-name',
1247                     })
1248                     .text(c.description)
1249                 )
1250                 .append(
1251                     $('<div>', {
1252                         class: 'k-c-p-c-title',
1253                     })
1254                     .text(c.sTitle)
1255                 )
1256                 .append(
1257                     $('<div>', {
1258                         class: 'k-c-p-c-notes',
1259                         id: 'k-c-p-c-notes',
1260                     })
1261                 );
1262
1263             var notes = new Array;
1264
1265             if (c.bVisible != false) {
1266                 $('#k-c-p-c-enable', crow).prop('checked', true);
1267             }
1268
1269             if (c.bSortable != false) {
1270                 notes.push("sortable");
1271             }
1272
1273             if (c.bSearchable != false) {
1274                 notes.push("searchable");
1275             }
1276
1277             $('#k-c-p-c-notes', crow).html(notes.join(", "));
1278
1279             rowcontainer.append(crow);
1280         }
1281
1282         elem.append(
1283             $('<div>', { })
1284             .append(
1285                 $('<p>', { })
1286                 .html('Drag and drop columns to re-order the device display table.  Columns may also be shown or hidden individually.')
1287             )
1288         )
1289         .append(
1290             $('<div>', {
1291                 class: 'k-c-p-header',
1292             })
1293             .append(
1294                 $('<i>', {
1295                     class: 'k-c-p-c-mover fa fa-arrows-v',
1296                     style: 'color: transparent !important',
1297                 })
1298             )
1299             .append(
1300                 $('<div>', {
1301                     class: 'k-c-p-c-enable',
1302                 })
1303                 .append(
1304                     $('<i>', {
1305                         class: 'fa fa-eye'
1306                     })
1307                 )
1308             )
1309             .append(
1310                 $('<div>', {
1311                     class: 'k-c-p-c-name',
1312                 })
1313                 .html('<i>Column</i>')
1314             )
1315             .append(
1316                 $('<div>', {
1317                     class: 'k-c-p-c-title',
1318                 })
1319                 .html('<i>Title</i>')
1320             )
1321             .append(
1322                 $('<div>', {
1323                     class: 'k-c-p-c-notes',
1324                 })
1325                 .html('<i>Info</i>')
1326             )
1327         );
1328
1329         elem.append(rowcontainer);
1330
1331         rowcontainer.sortable({
1332             change: function(event, ui) {
1333                 kismet_ui_settings.SettingsModified();
1334             }
1335         });
1336
1337
1338     },
1339     save: function(elem) {
1340         // Generate a config array of objects which defines the user config for
1341         // the datatable; save it; then kick the datatable redraw
1342         var col_defs = new Array();
1343
1344         $('.k-c-p-column', elem).each(function(i, e) {
1345             col_defs.push({
1346                 id: $(this).attr('id'),
1347                 enable: $('#k-c-p-c-enable', $(this)).is(':checked')
1348             });
1349         });
1350
1351         kismet.putStorage('kismet.datatable.columns', col_defs);
1352         exports.ResetDeviceTable(devicetableElement);
1353     },
1354 });
1355
1356 // Add the row highlighting
1357 kismet_ui_settings.AddSettingsPane({
1358     id: 'core_device_row_highlights',
1359     listTitle: 'Device Row Highlighting',
1360     create: function(elem) {
1361         elem.append(
1362             $('<form>', {
1363                 id: 'form'
1364             })
1365             .append(
1366                 $('<fieldset>', {
1367                     id: 'fs_devicerows'
1368                 })
1369                 .append(
1370                     $('<legend>', {})
1371                     .html('Device Row Highlights')
1372                 )
1373                 .append(
1374                     $('<table>', {
1375                         id: "devicerow_table",
1376                         width: "100%",
1377                     })
1378                     .append(
1379                         $('<tr>', {})
1380                         .append(
1381                             $('<th>')
1382                         )
1383                         .append(
1384                             $('<th>')
1385                             .html("Name")
1386                         )
1387                         .append(
1388                             $('<th>')
1389                             .html("Color")
1390                         )
1391                         .append(
1392                             $('<th>')
1393                             .html("Description")
1394                         )
1395                     )
1396                 )
1397             )
1398         );
1399
1400         $('#form', elem).on('change', function() {
1401             kismet_ui_settings.SettingsModified();
1402         });
1403
1404         for (var ri in DeviceRowHighlights) {
1405             var rh = DeviceRowHighlights[ri];
1406
1407             var row =
1408                 $('<tr>')
1409                 .attr('hlname', rh['name'])
1410                 .append(
1411                     $('<td>')
1412                     .append(
1413                         $('<input>', {
1414                             type: "checkbox",
1415                             class: "k-dt-enable",
1416                         })
1417                     )
1418                 )
1419                 .append(
1420                     $('<td>')
1421                     .html(rh['name'])
1422                 )
1423                 .append(
1424                     $('<td>')
1425                     .append(
1426                         $('<input>', {
1427                             type: "text",
1428                             value: rh['color'],
1429                             class: "k-dt-colorwidget"
1430                         })
1431                     )
1432                 )
1433                 .append(
1434                     $('<td>')
1435                     .html(rh['description'])
1436                 );
1437
1438             $('#devicerow_table', elem).append(row);
1439
1440             if (rh['enable']) {
1441                 $('.k-dt-enable', row).prop('checked', true);
1442             }
1443
1444             $(".k-dt-colorwidget", row).spectrum({
1445                 showInitial: true,
1446                 preferredFormat: "rgb",
1447             });
1448
1449         }
1450     },
1451     save: function(elem) {
1452         $('tr', elem).each(function() {
1453             kismet.putStorage('kismet.rowhighlight.color' + $(this).attr('hlname'), $('.k-dt-colorwidget', $(this)).val());
1454
1455             kismet.putStorage('kismet.rowhighlight.enable' + $(this).attr('hlname'), $('.k-dt-enable', $(this)).is(':checked'));
1456
1457             for (var ri in DeviceRowHighlights) {
1458                 if (DeviceRowHighlights[ri]['name'] === $(this).attr('hlname')) {
1459                     DeviceRowHighlights[ri]['color'] = $('.k-dt-colorwidget', $(this)).val();
1460                     DeviceRowHighlights[ri]['enable'] = $('.k-dt-enable', $(this)).is(':checked');
1461                 }
1462             }
1463         });
1464     },
1465 });
1466
1467 return exports;
1468
1469 });