dark mode and websockets
[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         theme: 'dark',
560
561         id: dialogid,
562         headerTitle: title,
563
564         headerControls: {
565             iconfont: 'jsglyph',
566             controls: 'closeonly',
567         },
568
569         position: {
570             "my": "left-top",
571             "at": "left-top",
572             "of": "window",
573             "offsetX": 2,
574             "offsetY": 2,
575             "autoposition": "RIGHT"
576         },
577
578         resizable: {
579             minWidth: 450,
580             minHeight: 400,
581             stop: function(event, ui) {
582                 $('div#accordion', ui.element).accordion("refresh");
583             }
584         },
585
586         onmaximized: function() {
587             $('div#accordion', this.content).accordion("refresh");
588         },
589
590         onnormalized: function() {
591             $('div#accordion', this.content).accordion("refresh");
592         },
593
594         onclosed: function() {
595             close_cb(this, options);
596         },
597
598         callback: function() {
599             window_cb(this, options);
600         },
601     }).resize({
602         width: w,
603         height: h,
604         callback: function(panel) {
605             $('div#accordion', this.content).accordion("refresh");
606         },
607     });
608
609     // Did we creep off the screen in our autopositioning?  Put this panel in
610     // the left if so (or if it's a single-panel situation like mobile, just
611     // put it front and center)
612     if (panel.offset().left + panel.width() > $(window).width()) {
613         panel.reposition({
614             "my": "left-top",
615             "at": "left-top",
616             "of": "window",
617             "offsetX": 2,
618             "offsetY": 2,
619         });
620     }
621
622 }
623
624 exports.DeviceDetails = new Array();
625
626 /* Register a device detail accordion panel, taking an id for the panel
627  * content, a title presented to the user, a position in the list, and
628  * options.  Because details are directly rendered all the time and
629  * can't be moved around / saved as configs like columns can, callbacks
630  * are just direct functions here.
631  *
632  * filter and render take one argument, the data to be shown
633  * filter: function(data) {
634  *  return false;
635  * }
636  *
637  * render: function(data) {
638  *  return "Some content";
639  * }
640  *
641  * draw takes the device data and a container element as an argument:
642  * draw: function(data, element) {
643  *  e.append("hi");
644  * }
645  * */
646 exports.AddDeviceDetail = function(id, title, pos, options) {
647     exports.AddDetail(exports.DeviceDetails, id, title, pos, options);
648 }
649
650 exports.GetDeviceDetails = function() {
651     return exports.DeviceDetails;
652 }
653
654 exports.DeviceDetailWindow = function(key) {
655     exports.DetailWindow(key, "Device Details", 
656         {
657             storage: {}
658         },
659
660         function(panel, options) {
661             var content = panel.content;
662
663             panel.active = true;
664
665             window['storage_devlist_' + key] = {};
666
667             window['storage_devlist_' + key]['foobar'] = 'bar';
668
669             panel.updater = function() {
670                 if (exports.window_visible) {
671                     $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
672                         .done(function(fulldata) {
673                             fulldata = kismet.sanitizeObject(fulldata);
674
675                             panel.headerTitle("Device: " + kismet.censorMAC(fulldata['kismet.device.base.commonname']));
676
677                             var accordion = $('div#accordion', content);
678
679                             if (accordion.length == 0) {
680                                 accordion = $('<div />', {
681                                     id: 'accordion'
682                                 });
683
684                                 content.append(accordion);
685                             }
686
687                             var detailslist = kismet_ui.GetDeviceDetails();
688
689                             for (var dii in detailslist) {
690                                 var di = detailslist[dii];
691
692                                 // Do we skip?
693                                 if ('filter' in di.options &&
694                                     typeof(di.options.filter) === 'function') {
695                                     if (di.options.filter(fulldata) == false) {
696                                         continue;
697                                     }
698                                 }
699
700                                 var vheader = $('h3#header_' + di.id, accordion);
701
702                                 if (vheader.length == 0) {
703                                     vheader = $('<h3>', {
704                                         id: "header_" + di.id,
705                                     })
706                                         .html(di.title);
707
708                                     accordion.append(vheader);
709                                 }
710
711                                 var vcontent = $('div#' + di.id, accordion);
712
713                                 if (vcontent.length == 0) {
714                                     vcontent = $('<div>', {
715                                         id: di.id,
716                                     });
717                                     accordion.append(vcontent);
718                                 }
719
720                                 // Do we have pre-rendered content?
721                                 if ('render' in di.options &&
722                                     typeof(di.options.render) === 'function') {
723                                     vcontent.html(di.options.render(fulldata));
724                                 }
725
726                                 if ('draw' in di.options &&
727                                     typeof(di.options.draw) === 'function') {
728                                     di.options.draw(fulldata, vcontent, options, 'storage_devlist_' + key);
729                                 }
730
731                                 if ('finalize' in di.options &&
732                                     typeof(di.options.finalize) === 'function') {
733                                     di.options.finalize(fulldata, vcontent, options, 'storage_devlist_' + key);
734                                 }
735                             }
736                             accordion.accordion({ heightStyle: 'fill' });
737                         })
738                         .fail(function(jqxhr, texterror) {
739                             content.html("<div style=\"padding: 10px;\"><h1>Oops!</h1><p>An error occurred loading device details for key <code>" + key + 
740                                 "</code>: HTTP code <code>" + jqxhr.status + "</code>, " + texterror + "</div>");
741                         })
742                         .always(function() {
743                             panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
744                         })
745                 } else {
746                     panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
747                 }
748
749             };
750
751             panel.updater();
752
753             new ClipboardJS('.copyuri');
754         },
755
756         function(panel, options) {
757             clearTimeout(panel.timerid);
758             panel.active = false;
759             window['storage_devlist_' + key] = {};
760         });
761
762 };
763
764 exports.RenderTrimmedTime = function(opts) {
765     return (new Date(opts['value'] * 1000).toString()).substring(4, 25);
766 }
767
768 exports.RenderHumanSize = function(opts) {
769     return kismet.HumanReadableSize(opts['value']);
770 };
771
772 // Central location to register channel conversion lists.  Conversion can
773 // be a function or a fixed dictionary.
774 exports.freq_channel_list = { };
775 exports.human_freq_channel_list = { };
776
777 exports.AddChannelList = function(phyname, humanname, channellist) {
778     exports.freq_channel_list[phyname] = channellist;
779     exports.human_freq_channel_list[humanname] = channellist;
780 }
781
782 // Get a list of human frequency conversions
783 exports.GetChannelListKeys = function() {
784     return Object.keys(exports.human_freq_channel_list);
785 }
786
787 // Get a converted channel name, or the raw frequency if we can't help
788 exports.GetConvertedChannel = function(humanname, frequency) {
789     if (humanname in exports.human_freq_channel_list) {
790         var conv = exports.human_freq_channel_list[humanname];
791
792         if (typeof(conv) === "function") {
793             // Call the conversion function if one exists
794             return conv(frequency);
795         } else if (frequency in conv) {
796             // Return the mapped value
797             return conv[frequency];
798         }
799     }
800
801     // Return the frequency if we couldn't figure out what to do
802     return frequency;
803 }
804
805 // Get a converted channel name, or the raw frequency if we can't help
806 exports.GetPhyConvertedChannel = function(phyname, frequency) {
807     if (phyname in exports.freq_channel_list) {
808         var conv = exports.freq_channel_list[phyname];
809
810         if (typeof(conv) === "function") {
811             // Call the conversion function if one exists
812             return conv(frequency);
813         } else if (frequency in conv) {
814             // Return the mapped value
815             return conv[frequency];
816         }
817     }
818
819     // Return the frequency if we couldn't figure out what to do
820     return kismet.HumanReadableFrequency(frequency);
821 }
822
823 exports.connection_error = 0;
824 exports.connection_error_panel = null;
825
826 exports.HealthCheck = function() {
827     var timerid;
828
829     if (exports.window_visible) {
830         $.get(local_uri_prefix + "system/status.json")
831             .done(function(data) {
832                 data = kismet.sanitizeObject(data);
833
834                 if (exports.connection_error && exports.connection_error_panel) {
835                     try {
836                         exports.connection_error_panel.close();
837                         exports.connection_error_panel = null;
838                     } catch (e) {
839                         ;
840                     }
841                 }
842
843                 exports.connection_error = 0;
844
845                 exports.last_timestamp = data['kismet.system.timestamp.sec'];
846             })
847             .fail(function() {
848                 if (exports.connection_error >= 3 && exports.connection_error_panel == null) {
849                     exports.connection_error_panel = $.jsPanel({
850                         theme: 'dark',
851                         id: "connection-alert",
852                         headerTitle: 'Cannot Connect to Kismet',
853                         headerControls: {
854                             controls: 'none',
855                             iconfont: 'jsglyph',
856                         },
857                         contentSize: "auto auto",
858                         paneltype: 'modal',
859                         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>',
860                     });
861                 }
862
863                 exports.connection_error++;
864             })
865             .always(function() {
866                 if (exports.connection_error)
867                     timerid = setTimeout(exports.HealthCheck, 1000);
868                 else
869                     timerid = setTimeout(exports.HealthCheck, 5000);
870             }); 
871     } else {
872         if (exports.connection_error)
873             timerid = setTimeout(exports.HealthCheck, 1000);
874         else
875             timerid = setTimeout(exports.HealthCheck, 5000);
876     }
877
878 }
879
880
881 exports.DegToDir = function(deg) {
882     var directions = [
883         "N", "NNE", "NE", "ENE",
884         "E", "ESE", "SE", "SSE",
885         "S", "SSW", "SW", "WSW",
886         "W", "WNW", "NW", "NNW"
887     ];
888
889     var degrees = [
890         0, 23, 45, 68,
891         90, 113, 135, 158,
892         180, 203, 225, 248,
893         270, 293, 315, 338
894     ];
895
896     for (var p = 1; p < degrees.length; p++) {
897         if (deg < degrees[p])
898             return directions[p - 1];
899     }
900
901     return directions[directions.length - 1];
902 }
903
904 // Use our settings to make some conversion functions for distance and temperature
905 exports.renderDistance = function(k, precision = 5) {
906     if (kismet.getStorage('kismet.base.unit.distance') === 'metric' ||
907             kismet.getStorage('kismet.base.unit.distance') === '') {
908         if (k < 1) {
909             return (k * 1000).toFixed(precision) + ' m';
910         }
911
912         return k.toFixed(precision) + ' km';
913     } else {
914         var m = (k * 0.621371);
915
916         if (m < 1) {
917             return (5280 * m).toFixed(precision) + ' feet';
918         }
919         return (k * 0.621371).toFixed(precision) + ' miles';
920     }
921 }
922
923 // Use our settings to make some conversion functions for distance and temperature
924 exports.renderHeightDistance = function(m, precision = 5) {
925     if (kismet.getStorage('kismet.base.unit.distance') === 'metric' ||
926             kismet.getStorage('kismet.base.unit.distance') === '') {
927         if (m < 1000) {
928             return m.toFixed(precision) + ' m';
929         }
930
931         return (m / 1000).toFixed(precision) + ' km';
932     } else {
933         var f = (m * 3.2808399);
934
935         if (f < 5280) {
936             return f.toFixed(precision) + ' feet';
937         }
938         return (f / 5280).toFixed(precision) + ' miles';
939     }
940 }
941
942 exports.renderSpeed = function(kph, precision = 5) {
943     if (kismet.getStorage('kismet.base.unit.speed') === 'metric' ||
944             kismet.getStorage('kismet.base.unit.speed') === '') {
945         return kph.toFixed(precision) + ' KPH';
946     } else {
947         return (kph * 0.621371).toFixed(precision) + ' MPH';
948     }
949 }
950
951 exports.renderTemperature = function(c, precision = 5) {
952     if (kismet.getStorage('kismet.base.unit.temp') === 'celsius' ||
953             kismet.getStorage('kismet.base.unit.temp') === '') {
954         return c.toFixed(precision) + '&deg; C';
955     } else {
956         return (c * (9/5) + 32).toFixed(precision) + '&deg; F';
957     }
958 }
959
960 var deviceTid;
961
962 var devicetableElement = null;
963
964 function ScheduleDeviceSummary() {
965     try {
966         if (exports.window_visible && devicetableElement.is(":visible")) {
967
968             var dt = devicetableElement.DataTable();
969
970             // Save the state.  We can't use proper state saving because it seems to break
971             // the table position
972             kismet.putStorage('kismet.base.devicetable.order', JSON.stringify(dt.order()));
973             kismet.putStorage('kismet.base.devicetable.search', JSON.stringify(dt.search()));
974
975             dt.ajax.reload(function(d) { }, false);
976         }
977
978     } catch (error) {
979         console.log(error);
980     }
981     
982     // Set our timer outside of the datatable callback so that we get called even
983     // if the ajax load fails
984     deviceTid = setTimeout(ScheduleDeviceSummary, 2000);
985
986     return;
987 }
988
989 function CancelDeviceSummary() {
990     clearTimeout(deviceTid);
991 }
992
993 /* Create the device table */
994 exports.CreateDeviceTable = function(element) {
995     devicetableElement = element;
996     // var statuselement = $('#' + element.attr('id') + '_status');
997
998     var dt = exports.InitializeDeviceTable(element);
999
1000     dt.draw(false);
1001
1002     // Start the auto-updating
1003     ScheduleDeviceSummary();
1004 }
1005
1006 exports.InitializeDeviceTable = function(element) {
1007     // var statuselement = $('#' + element.attr('id') + '_status');
1008
1009     /* Make the fields list json and set the wrapper object to aData to make the DT happy */
1010     var cols = exports.GetDeviceColumns();
1011     var colmap = exports.GetDeviceColumnMap(cols);
1012     var fields = exports.GetDeviceFields();
1013
1014     var json = {
1015         fields: fields,
1016         colmap: colmap,
1017         datatable: true,
1018     };
1019
1020     if ($.fn.dataTable.isDataTable(element)) {
1021         element.DataTable().destroy();
1022         element.empty();
1023     }
1024
1025     element
1026         .on('xhr.dt', function (e, settings, json, xhr) {
1027             json = kismet.sanitizeObject(json);
1028
1029             /*
1030             if (json['recordsFiltered'] != json['recordsTotal'])
1031                 statuselement.html(json['recordsTotal'] + " devices (" + json['recordsFiltered'] + " shown after filter)");
1032             else
1033                 statuselement.html(json['recordsTotal'] + " devices");
1034                 */
1035         } )
1036         .DataTable( {
1037
1038         destroy: true,
1039
1040         scrollResize: true,
1041         // scrollY: 200,
1042         scrollX: "100%",
1043
1044         pageResize: true,
1045         serverSide: true,
1046         processing: true,
1047
1048         // stateSave: true,
1049
1050         dom: '<"viewselector">ftip',
1051
1052         deferRender: true,
1053         lengthChange: false,
1054
1055             /*
1056         scroller: {
1057             loadingIndicator: true,
1058         },
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     try { 
1140         device_dt.stateRestore.state.add("AJAX");
1141     } catch (_err) { }
1142     
1143     // $('div.viewselector').html("View picker");
1144     exports.BuildDeviceViewSelector($('div.viewselector'));
1145
1146     // Restore the order
1147     var saved_order = kismet.getStorage('kismet.base.devicetable.order', "");
1148     if (saved_order !== "")
1149         device_dt.order(JSON.parse(saved_order));
1150
1151     // Restore the search
1152     var saved_search = kismet.getStorage('kismet.base.devicetable.search', "");
1153     if (saved_search !== "")
1154         device_dt.search(JSON.parse(saved_search));
1155
1156     // Set an onclick handler to spawn the device details dialog
1157     $('tbody', element).on('click', 'tr', function () {
1158         kismet_ui.DeviceDetailWindow(this.id);
1159
1160         // Use the ID above we insert in the row creation, instead of looking in the
1161         // device list data
1162         // Fetch the data of the row that got clicked
1163         // var device_dt = element.DataTable();
1164         // var data = device_dt.row( this ).data();
1165         // var key = data['kismet.device.base.key'];
1166         // kismet_ui.DeviceDetailWindow(key);
1167     } );
1168
1169     $('tbody', element)
1170         .on( 'mouseenter', 'td', function () {
1171             try {
1172                 var device_dt = element.DataTable();
1173
1174                 if (typeof(device_dt.cell(this).index()) === 'Undefined')
1175                     return;
1176
1177                 var colIdx = device_dt.cell(this).index().column;
1178                 var rowIdx = device_dt.cell(this).index().row;
1179
1180                 // Remove from all cells
1181                 $(device_dt.cells().nodes()).removeClass('kismet-highlight');
1182                 // Highlight the td in this row
1183                 $('td', device_dt.row(rowIdx).nodes()).addClass('kismet-highlight');
1184             } catch (e) {
1185
1186             }
1187         } );
1188
1189
1190     return device_dt;
1191 }
1192
1193 exports.ResizeDeviceTable = function(element) {
1194     // console.log(element.height());
1195     // exports.ResetDeviceTable(element);
1196 }
1197
1198 exports.ResetDeviceTable = function(element) {
1199     CancelDeviceSummary();
1200
1201     exports.InitializeDeviceTable(element);
1202
1203     ScheduleDeviceSummary();
1204 }
1205
1206 kismet_ui_settings.AddSettingsPane({
1207     id: 'core_devicelist_columns',
1208     listTitle: 'Device List Columns',
1209     create: function(elem) {
1210
1211         var rowcontainer =
1212             $('<div>', {
1213                 id: 'k-c-p-rowcontainer'
1214             });
1215
1216         var cols = exports.GetDeviceColumns(true);
1217
1218         for (var ci in cols) {
1219             var c = cols[ci];
1220
1221             if (! c.user_selectable)
1222                 continue;
1223
1224             var crow =
1225                 $('<div>', {
1226                     class: 'k-c-p-column',
1227                     id: c.kismetId,
1228                 })
1229                 .append(
1230                     $('<i>', {
1231                         class: 'k-c-p-c-mover fa fa-arrows-v'
1232                     })
1233                 )
1234                 .append(
1235                     $('<div>', {
1236                         class: 'k-c-p-c-enable',
1237                     })
1238                     .append(
1239                         $('<input>', {
1240                             type: 'checkbox',
1241                             id: 'k-c-p-c-enable'
1242                         })
1243                         .on('change', function() {
1244                             kismet_ui_settings.SettingsModified();
1245                             })
1246                     )
1247                 )
1248                 .append(
1249                     $('<div>', {
1250                         class: 'k-c-p-c-name',
1251                     })
1252                     .text(c.description)
1253                 )
1254                 .append(
1255                     $('<div>', {
1256                         class: 'k-c-p-c-title',
1257                     })
1258                     .text(c.sTitle)
1259                 )
1260                 .append(
1261                     $('<div>', {
1262                         class: 'k-c-p-c-notes',
1263                         id: 'k-c-p-c-notes',
1264                     })
1265                 );
1266
1267             var notes = new Array;
1268
1269             if (c.bVisible != false) {
1270                 $('#k-c-p-c-enable', crow).prop('checked', true);
1271             }
1272
1273             if (c.bSortable != false) {
1274                 notes.push("sortable");
1275             }
1276
1277             if (c.bSearchable != false) {
1278                 notes.push("searchable");
1279             }
1280
1281             $('#k-c-p-c-notes', crow).html(notes.join(", "));
1282
1283             rowcontainer.append(crow);
1284         }
1285
1286         elem.append(
1287             $('<div>', { })
1288             .append(
1289                 $('<p>', { })
1290                 .html('Drag and drop columns to re-order the device display table.  Columns may also be shown or hidden individually.')
1291             )
1292         )
1293         .append(
1294             $('<div>', {
1295                 class: 'k-c-p-header',
1296             })
1297             .append(
1298                 $('<i>', {
1299                     class: 'k-c-p-c-mover fa fa-arrows-v',
1300                     style: 'color: transparent !important',
1301                 })
1302             )
1303             .append(
1304                 $('<div>', {
1305                     class: 'k-c-p-c-enable',
1306                 })
1307                 .append(
1308                     $('<i>', {
1309                         class: 'fa fa-eye'
1310                     })
1311                 )
1312             )
1313             .append(
1314                 $('<div>', {
1315                     class: 'k-c-p-c-name',
1316                 })
1317                 .html('<i>Column</i>')
1318             )
1319             .append(
1320                 $('<div>', {
1321                     class: 'k-c-p-c-title',
1322                 })
1323                 .html('<i>Title</i>')
1324             )
1325             .append(
1326                 $('<div>', {
1327                     class: 'k-c-p-c-notes',
1328                 })
1329                 .html('<i>Info</i>')
1330             )
1331         );
1332
1333         elem.append(rowcontainer);
1334
1335         rowcontainer.sortable({
1336             change: function(event, ui) {
1337                 kismet_ui_settings.SettingsModified();
1338             }
1339         });
1340
1341
1342     },
1343     save: function(elem) {
1344         // Generate a config array of objects which defines the user config for
1345         // the datatable; save it; then kick the datatable redraw
1346         var col_defs = new Array();
1347
1348         $('.k-c-p-column', elem).each(function(i, e) {
1349             col_defs.push({
1350                 id: $(this).attr('id'),
1351                 enable: $('#k-c-p-c-enable', $(this)).is(':checked')
1352             });
1353         });
1354
1355         kismet.putStorage('kismet.datatable.columns', col_defs);
1356         exports.ResetDeviceTable(devicetableElement);
1357     },
1358 });
1359
1360 // Add the row highlighting
1361 kismet_ui_settings.AddSettingsPane({
1362     id: 'core_device_row_highlights',
1363     listTitle: 'Device Row Highlighting',
1364     create: function(elem) {
1365         elem.append(
1366             $('<form>', {
1367                 id: 'form'
1368             })
1369             .append(
1370                 $('<fieldset>', {
1371                     id: 'fs_devicerows'
1372                 })
1373                 .append(
1374                     $('<legend>', {})
1375                     .html('Device Row Highlights')
1376                 )
1377                 .append(
1378                     $('<table>', {
1379                         id: "devicerow_table",
1380                         width: "100%",
1381                     })
1382                     .append(
1383                         $('<tr>', {})
1384                         .append(
1385                             $('<th>')
1386                         )
1387                         .append(
1388                             $('<th>')
1389                             .html("Name")
1390                         )
1391                         .append(
1392                             $('<th>')
1393                             .html("Color")
1394                         )
1395                         .append(
1396                             $('<th>')
1397                             .html("Description")
1398                         )
1399                     )
1400                 )
1401             )
1402         );
1403
1404         $('#form', elem).on('change', function() {
1405             kismet_ui_settings.SettingsModified();
1406         });
1407
1408         for (var ri in DeviceRowHighlights) {
1409             var rh = DeviceRowHighlights[ri];
1410
1411             var row =
1412                 $('<tr>')
1413                 .attr('hlname', rh['name'])
1414                 .append(
1415                     $('<td>')
1416                     .append(
1417                         $('<input>', {
1418                             type: "checkbox",
1419                             class: "k-dt-enable",
1420                         })
1421                     )
1422                 )
1423                 .append(
1424                     $('<td>')
1425                     .html(rh['name'])
1426                 )
1427                 .append(
1428                     $('<td>')
1429                     .append(
1430                         $('<input>', {
1431                             type: "text",
1432                             value: rh['color'],
1433                             class: "k-dt-colorwidget"
1434                         })
1435                     )
1436                 )
1437                 .append(
1438                     $('<td>')
1439                     .html(rh['description'])
1440                 );
1441
1442             $('#devicerow_table', elem).append(row);
1443
1444             if (rh['enable']) {
1445                 $('.k-dt-enable', row).prop('checked', true);
1446             }
1447
1448             $(".k-dt-colorwidget", row).spectrum({
1449                 showInitial: true,
1450                 preferredFormat: "rgb",
1451             });
1452
1453         }
1454     },
1455     save: function(elem) {
1456         $('tr', elem).each(function() {
1457             kismet.putStorage('kismet.rowhighlight.color' + $(this).attr('hlname'), $('.k-dt-colorwidget', $(this)).val());
1458
1459             kismet.putStorage('kismet.rowhighlight.enable' + $(this).attr('hlname'), $('.k-dt-enable', $(this)).is(':checked'));
1460
1461             for (var ri in DeviceRowHighlights) {
1462                 if (DeviceRowHighlights[ri]['name'] === $(this).attr('hlname')) {
1463                     DeviceRowHighlights[ri]['color'] = $('.k-dt-colorwidget', $(this)).val();
1464                     DeviceRowHighlights[ri]['enable'] = $('.k-dt-enable', $(this)).is(':checked');
1465                 }
1466             }
1467         });
1468     },
1469 });
1470
1471 return exports;
1472
1473 });