dc13809e03e1546fa317d800f60debd99fa04e46
[kismet-logviewer.git] / logviewer / static / js / kismet.ui.datasources.js
1 "use strict";
2
3 var local_uri_prefix = ""; 
4 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
5     local_uri_prefix = KISMET_URI_PREFIX;
6
7 // Load our css
8 $('<link>')
9     .appendTo('head')
10     .attr({
11         type: 'text/css',
12         rel: 'stylesheet',
13         href: local_uri_prefix + 'css/kismet.ui.datasources.css'
14     });
15
16 /* Convert a hop rate to human readable */
17 export const hop_to_human = (hop) => {
18     if (hop >= 1) {
19         return hop + "/second";
20     }
21
22     var s = (hop / 60.0);
23
24     if (s < 60) {
25         return s + "/minute";
26     }
27
28     return s + " seconds";
29 }
30
31 /* Sidebar:  Channel coverage
32  *
33  * The channel coverage looks at the data sources and plots a moving graph
34  * of all channels and how they're covered; it reflects how the pattern will
35  * work, but not, necessarily, reality itself.
36  */
37 kismet_ui_sidebar.AddSidebarItem({
38     id: 'datasource_channel_coverage',
39     listTitle: '<i class="fa fa-bar-chart-o"></i> Channel Coverage',
40     clickCallback: function() {
41         ChannelCoverage();
42     },
43 });
44
45 var channelcoverage_backend_tid;
46 var channelcoverage_display_tid;
47 var channelcoverage_panel = null;
48 var channelcoverage_canvas = null;
49 var channelhop_canvas = null;
50 var channelcoverage_chart = null;
51 var channelhop_chart = null;
52 var cc_uuid_pos_map = {};
53
54 export const ChannelCoverage = () => {
55     var w = $(window).width() * 0.85;
56     var h = $(window).height() * 0.75;
57     var offy = 20;
58
59     if ($(window).width() < 450 || $(window).height() < 450) {
60         w = $(window).width() - 5;
61         h = $(window).height() - 5;
62         offy = 0;
63     }
64
65     channelcoverage_chart = null;
66     channelhop_chart = null;
67
68     var content =
69         $('<div>', {
70             id: "k-cc-main",
71             class: "k-cc-main",
72         })
73         .append(
74             $('<ul>', {
75                 id: "k-cc-tab-ul"
76             })
77             .append(
78                 $('<li>', { })
79                 .append(
80                     $('<a>', {
81                         href: '#k-cc-tab-coverage'
82                     })
83                     .html("Channel Coverage")
84                 )
85             )
86             .append(
87                 $('<li>', { })
88                 .append(
89                     $('<a>', {
90                         href: '#k-cc-tab-estimate'
91                     })
92                     .html("Estimated Hopping")
93                 )
94             )
95         )
96         .append(
97             $('<div>', {
98                 id: 'k-cc-tab-coverage',
99                 class: 'k-cc-canvas'
100             })
101             .append(
102                 $('<canvas>', {
103                     id: 'k-cc-cover-canvas',
104                     class: 'k-cc-canvas'
105                 })
106             )
107         )
108         .append(
109             $('<div>', {
110                 id: 'k-cc-tab-estimate',
111                 class: 'k-cc-canvas'
112             })
113             .append(
114                 $('<canvas>', {
115                     id: 'k-cc-canvas',
116                     class: 'k-cc-canvas'
117                 })
118             )
119         );
120
121     channelcoverage_panel = $.jsPanel({
122         id: 'channelcoverage',
123         headerTitle: '<i class="fa fa-bar-chart-o" /> Channel Coverage',
124         headerControls: {
125             iconfont: 'jsglyph',
126             minimize: 'remove',
127             smallify: 'remove',
128         },
129         content: content,
130         onclosed: function() {
131             clearTimeout(channelcoverage_backend_tid);
132             clearTimeout(channelcoverage_display_tid);
133             channelhop_chart = null;
134             channelhop_canvas = null;
135             channelcoverage_canvas = null;
136             channelcoverage_chart = null;
137         },
138         onresized: resize_channelcoverage,
139         onmaximized: resize_channelcoverage,
140         onnormalized: resize_channelcoverage,
141     })
142     .on('resize', function() {
143         resize_channelcoverage();
144     }).resize({
145         width: w,
146         height: h
147     }).reposition({
148         my: 'center-top',
149         at: 'center-top',
150         of: 'window',
151         offsetY: offy,
152     });
153
154     content.tabs({
155         heightStyle: 'fill'
156     });
157
158
159     channelcoverage_backend_refresh();
160     channelcoverage_display_refresh();
161 }
162
163 function channelcoverage_backend_refresh() {
164     clearTimeout(channelcoverage_backend_tid);
165
166     if (channelcoverage_panel == null)
167         return;
168
169     if (channelcoverage_panel.is(':hidden'))
170         return;
171
172     $.get(local_uri_prefix + "datasource/all_sources.json")
173     .done(function(data) {
174         // Build a list of all devices we haven't seen before and set their
175         // initial positions to match
176         for (var di = 0; di < data.length; di++) {
177             if (data[di]['kismet.datasource.running'] == 0) {
178                 if ((data[di]['kismet.datasource.uuid'] in cc_uuid_pos_map)) {
179                    delete cc_uuid_pos_map[data[di]['kismet.datasource.uuid']];
180                 }
181             } else if (!(data[di]['kismet.datasource.uuid'] in cc_uuid_pos_map)) {
182                 cc_uuid_pos_map[data[di]['kismet.datasource.uuid']] = {
183                     uuid: data[di]['kismet.datasource.uuid'],
184                     name: data[di]['kismet.datasource.name'],
185                     interface: data[di]['kismet.datasource.interface'],
186                     hopping: data[di]['kismet.datasource.hopping'],
187                     channel: data[di]['kismet.datasource.channel'],
188                     channels: data[di]['kismet.datasource.hop_channels'],
189                     offset: data[di]['kismet.datasource.hop_offset'],
190                     position: data[di]['kismet.datasource.hop_offset'],
191                     skip: data[di]['kismet.datasource.hop_shuffle_skip'],
192                 };
193             }
194
195         }
196     })
197     .always(function() {
198         channelcoverage_backend_tid = setTimeout(channelcoverage_backend_refresh, 5000);
199     });
200 }
201
202 function resize_channelcoverage() {
203     if (channelcoverage_panel == null)
204         return;
205
206     var container = $('#k-cc-main', channelcoverage_panel.content);
207
208     var tabs = $('#k-cc-tab-ul', container);
209
210     var w = container.width();
211     var h = container.height() - tabs.outerHeight();
212
213     $('#k-cc-tab-estimate', container)
214         .css('width', w)
215         .css('height', h);
216
217     if (channelhop_canvas != null) {
218         channelhop_canvas
219             .css('width', w)
220             .css('height', h);
221
222         if (channelhop_chart != null)
223              channelhop_chart.resize();
224     }
225
226     $('#k-cc-tab-coverage', container)
227         .css('width', w)
228         .css('height', h);
229
230     if (channelcoverage_canvas != null) {
231         channelcoverage_canvas
232             .css('width', w)
233             .css('height', h);
234
235         if (channelcoverage_chart != null)
236              channelcoverage_chart.resize();
237     }
238
239 }
240
241 function channelcoverage_display_refresh() {
242     clearTimeout(channelcoverage_display_tid);
243
244     if (channelcoverage_panel == null)
245         return;
246
247     if (channelcoverage_panel.is(':hidden'))
248         return;
249
250     // Now we know all the sources; make a list of all channels and figure
251     // out if we're on any of them; each entry in total_channel_list contains
252     // an array of UUIDs on this channel in this sequence
253     var total_channel_list = {}
254
255     for (var du in cc_uuid_pos_map) {
256         var d = cc_uuid_pos_map[du];
257
258         if (d['hopping']) {
259             for (var ci = 0; ci < d['channels'].length; ci++) {
260                 var chan = d['channels'][ci];
261                 if (!(chan in total_channel_list)) {
262                     total_channel_list[chan] = [ ];
263                 }
264
265                 if ((d['position'] % d['channels'].length) == ci) {
266                     total_channel_list[chan].push(du);
267                 }
268             }
269
270             // Increment the virtual channel position for the graph
271             if (d['skip'] == 0) {
272                 d['position'] = d['position'] + 1;
273             } else {
274                 d['position'] = d['position'] + d['skip'];
275             }
276         } else {
277             // Non-hopping sources are always on their channel
278             var chan = d['channel'];
279
280             if (!(chan in total_channel_list)) {
281                 total_channel_list[chan] = [ du ];
282             } else {
283                 total_channel_list[chan].push(du);
284             }
285         }
286     }
287
288     // Create the channel index for the x-axis, used in both the hopping and the coverage
289     // graphs
290     var chantitles = new Array();
291     for (var ci in total_channel_list) {
292         chantitles.push(ci);
293     }
294
295     // Perform a natural sort on it to get it in order
296     var ncollator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
297     chantitles.sort(ncollator.compare);
298
299     // Create the source datasets for the animated estimated hopping graph, covering all
300     // channels and highlighting the channels we have a UUID in
301     var source_datasets = new Array()
302
303     var ndev = 0;
304
305     for (var du in cc_uuid_pos_map) {
306         var d = cc_uuid_pos_map[du];
307
308         var dset = [];
309
310         for (var ci in chantitles) {
311             var clist = total_channel_list[chantitles[ci]];
312
313             if (clist.indexOf(du) < 0) {
314                 dset.push(0);
315             } else {
316                 dset.push(1);
317             }
318         }
319
320         var color = "hsl(" + parseInt(255 * (ndev / Object.keys(cc_uuid_pos_map).length)) + ", 100%, 50%)";
321
322         source_datasets.push({
323             label: d['name'],
324             data: dset,
325             borderColor: color,
326             backgroundColor: color,
327         });
328
329             ndev++;
330     }
331
332     // Create the source list for the Y axis of the coverage graph; we make an intermediary
333     // which is sorted by name but includes UUID, then assemble the final one
334     var sourcetitles_tmp = new Array();
335     var sourcetitles = new Array();
336
337     for (var ci in cc_uuid_pos_map) {
338         sourcetitles_tmp.push({
339             name: cc_uuid_pos_map[ci].name,
340             uuid: ci
341         });
342     }
343
344     sourcetitles_tmp.sort(function(a, b) {
345         return a.name.localeCompare(b.name);
346     });
347
348     // Build the titles
349     for (var si in sourcetitles_tmp) {
350         sourcetitles.push(sourcetitles_tmp[si].name);
351     }
352
353     var bubble_dataset = new Array();
354
355     // Build the bubble data
356     ndev = 0;
357     for (var si in sourcetitles_tmp) {
358         var d = cc_uuid_pos_map[sourcetitles_tmp[si].uuid];
359         var ds = new Array;
360
361         if (d.hopping) {
362             for (var ci in d.channels) {
363                 var c = d.channels[ci];
364
365                 var cp = chantitles.indexOf(c);
366
367                 if (cp < 0)
368                     continue;
369
370                 ds.push({
371                     x: cp,
372                     y: si,
373                     r: 5
374                 });
375             }
376         } else {
377             var cp = chantitles.indexOf(d.channel);
378             if (cp >= 0) {
379                 ds.push({
380                     x: cp,
381                     y: si,
382                     r: 5
383                 });
384             }
385         }
386
387         var color = "hsl(" + parseInt(255 * (ndev / Object.keys(cc_uuid_pos_map).length)) + ", 100%, 50%)";
388
389         bubble_dataset.push({
390             label: d.name,
391             data: ds,
392             borderColor: color,
393             backgroundColor: color,
394         });
395
396         ndev++;
397
398     }
399
400     if (channelhop_canvas == null) {
401         channelhop_canvas = $('#k-cc-canvas', channelcoverage_panel.content);
402
403         var bp = 5.0;
404
405         if (chantitles.length < 14)
406             bp = 2;
407
408         channelhop_chart = new Chart(channelhop_canvas, {
409             type: "bar",
410             options: {
411                 responsive: true,
412                 maintainAspectRatio: false,
413                 scales: {
414                     xAxes: [{ barPercentage: bp, }],
415                 },
416             },
417             data: {
418                 labels: chantitles,
419                 datasets: source_datasets,
420             },
421         });
422     } else {
423         channelhop_chart.data.datasets = source_datasets;
424         channelhop_chart.data.labels = chantitles;
425         channelhop_chart.update(0);
426     }
427
428     if (channelcoverage_canvas == null && sourcetitles.length != 0) {
429         channelcoverage_canvas = $('#k-cc-cover-canvas', channelcoverage_panel.content);
430
431         channelcoverage_chart = new Chart(channelcoverage_canvas, {
432             type: 'bubble',
433             options: {
434                 title: {
435                     display: true,
436                     text: 'Per-Source Channel Coverage',
437                 },
438                 legend: {
439                     display: false,
440                 },
441                 responsive: true,
442                 maintainAspectRatio: false,
443                 scales: {
444                     xAxes: [{
445                         ticks: {
446                             autoSkip: false,
447                             stepSize: 1,
448                             callback: function(value, index, values) {
449                                 return chantitles[value];
450                             },
451                             min: 0,
452                             max: chantitles.length,
453                             position: 'bottom',
454                             type: 'linear',
455                         }
456                     }],
457                     yAxes: [{
458                         ticks: {
459                             autoSkip: false,
460                             stepSize: 1,
461                             callback: function(value, index, values) {
462                                 return sourcetitles[value];
463                             },
464                             min: 0,
465                             max: sourcetitles.length,
466                             position: 'left',
467                             type: 'linear',
468                         },
469                     }],
470                 },
471             },
472             data: {
473                 labels: chantitles,
474                 yLabels: sourcetitles,
475                 datasets: bubble_dataset,
476             },
477         });
478     } else if (sourcetitles.length != 0) {
479         channelcoverage_chart.data.datasets = bubble_dataset;
480
481         channelcoverage_chart.data.labels = chantitles;
482         channelcoverage_chart.data.yLabels = sourcetitles;
483
484         channelcoverage_chart.options.scales.xAxes[0].ticks.min = 0;
485         channelcoverage_chart.options.scales.xAxes[0].ticks.max = chantitles.length;
486
487         channelcoverage_chart.options.scales.yAxes[0].ticks.min = 0;
488         channelcoverage_chart.options.scales.yAxes[0].ticks.max = sourcetitles.length;
489
490         channelcoverage_chart.update(0);
491     }
492
493     channelcoverage_display_tid = setTimeout(channelcoverage_display_refresh, 500);
494 }
495
496 /* Sidebar:  Data sources (new)
497  *
498  * Data source management panel
499  */
500 kismet_ui_sidebar.AddSidebarItem({
501     id: 'datasource_sources2',
502     listTitle: '<i class="fa fa-cogs"></i> Data Sources',
503     priority: -500,
504     clickCallback: function() {
505         DataSources2();
506     },
507 });
508
509 var ds_state = {};
510
511 function update_datasource2(data) {
512     if (!"ds_content" in ds_state)
513         return;
514
515     var set_row = function(sdiv, id, title, content) {
516         var r = $('tr#' + id, sdiv);
517
518         if (r.length == 0) {
519             r = $('<tr>', { id: id })
520             .append($('<td>'))
521             .append($('<td>'));
522
523             $('.k-ds-table', sdiv).append(r);
524         }
525
526         $('td:eq(0)', r).replaceWith($('<td>').append(title));
527         $('td:eq(1)', r).replaceWith($('<td>').append(content));
528     }
529
530     var top_row = function(sdiv, id, title, content) {
531         var r = $('tr#' + id, sdiv);
532
533         if (r.length == 0) {
534             r = $('<tr>', { id: id })
535             .append($('<td>'))
536             .append($('<td>'));
537
538             $('.k-ds-table', sdiv).prepend(r);
539         }
540
541         $('td:eq(0)', r).replaceWith($('<td>').append(title));
542         $('td:eq(1)', r).replaceWith($('<td>').append(content));
543     }
544
545     for (var uuid of ds_state['remove_pending']) {
546         var sdiv = $('#' + uuid, ds_state['ds_content']);
547         $('.k-ds-modal', sdiv).hide();
548     }
549     ds_state['remove_pending'] = [];
550
551     /*
552     // Defer if we're waiting for a command to finish; do NOTHING else
553     if ('defer_command_progress' in ds_state && ds_state['defer_command_progress'])
554         return;
555         */
556
557     // Mark that we're loading interfaces
558     if (ds_state['done_interface_update']) {
559         $('#ds_loading_interfaces', ds_state['ds_content']).remove();
560     } else {
561         var loading_intf = $('#ds_loading_interfaces', ds_state['ds_content']);
562
563         if (loading_intf.length == 0) {
564             loading_intf = $('<div>', {
565                 id: 'ds_loading_interfaces',
566                 class: 'accordion',
567                 })
568             .append(
569                 $('<h3>', {
570                     id: 'header',
571                 })
572                 .append(
573                     $('<span>', {
574                         class: 'k-ds-source',
575                     })
576                     .html("<i class=\"fa fa-spin fa-cog\"></i> Finding available interfaces...")
577                 )
578             ).append(
579                 $('<div>').html("Kismet is probing for available capture interfaces...")
580             );
581
582             loading_intf.accordion({ collapsible: true, active: false });
583
584             ds_state['ds_content'].append(loading_intf);
585         }
586     }
587
588
589     // Clean up missing probed interfaces
590     $('.interface', ds_state['ds_content']).each(function(i) {
591         var found = false;
592
593         for (var intf of ds_state['kismet_interfaces']) {
594             if ($(this).attr('id') === intf['kismet.datasource.probed.interface']) {
595                 if (intf['kismet.datasource.probed.in_use_uuid'] !== '00000000-0000-0000-0000-000000000000') {
596                     break;
597                 }
598                 found = true;
599                 break;
600             }
601         }
602
603         if (!found) {
604             // console.log("removing interface", $(this).attr('id'));
605             $(this).remove();
606         }
607     });
608
609     // Clean up missing sources
610     $('.source', ds_state['ds_content']).each(function(i) {
611         var found = false;
612
613         for (var source of ds_state['kismet_sources']) {
614             if ($(this).attr('id') === source['kismet.datasource.uuid']) {
615                 found = true;
616                 break;
617             }
618         }
619
620         if (!found) {
621             // console.log("removing source", $(this).attr('id'));
622             $(this).remove();
623         }
624     });
625
626     for (var intf of ds_state['kismet_interfaces']) {
627         if (intf['kismet.datasource.probed.in_use_uuid'] !== '00000000-0000-0000-0000-000000000000') {
628             $('#' + intf['kismet.datasource.probed.interface'], ds_state['ds_content']).remove();
629             continue;
630         }
631
632         var idiv = $('#' + intf['kismet.datasource.probed.interface'], ds_state['ds_content']);
633
634         if (idiv.length == 0) {
635             idiv = $('<div>', {
636                 id: intf['kismet.datasource.probed.interface'],
637                 class: 'accordion interface',
638                 })
639             .append(
640                 $('<h3>', {
641                     id: 'header',
642                 })
643                 .append(
644                     $('<span>', {
645                         class: 'k-ds-source',
646                     })
647                     .html("Available Interface: " + intf['kismet.datasource.probed.interface'] + ' (' + intf['kismet.datasource.type_driver']['kismet.datasource.driver.type'] + ')')
648                 )
649             ).append(
650                 $('<div>', {
651                     // id: 'content',
652                     class: 'k-ds_content',
653                 })
654             );
655
656             var table = $('<table>', {
657                 class: 'k-ds-table'
658                 });
659
660             var wrapper = $('<div>', {
661                 "style": "position: relative;",
662             });
663
664             var modal = $('<div>', {
665                 class: 'k-ds-modal',
666             }).append(
667                 $('<div>', {
668                     class: 'k-ds-modal-content',
669                 })
670                 .append(
671                     $('<div>', {
672                         class: "k-ds-modal-message",
673                         style: "font-size: 125%; margin-bottom: 5px;",
674                     }).html("Loading...")
675                 ).append(
676                     $('<i>', {
677                         class: "fa fa-3x fa-cog fa-spin",
678                     })
679                 )
680             );
681
682             wrapper.append(table);
683             wrapper.append(modal);
684             modal.hide();
685
686             $('.k-ds_content', idiv).append(wrapper);
687
688             idiv.accordion({ collapsible: true, active: false });
689
690             ds_state['ds_content'].append(idiv);
691         }
692
693         set_row(idiv, 'interface', '<b>Interface</b>', intf['kismet.datasource.probed.interface']);
694         set_row(idiv, 'driver', '<b>Capture Driver</b>', intf['kismet.datasource.type_driver']['kismet.datasource.driver.type']);
695         if (intf['kismet.datasource.probed.hardware'] !== '')
696             set_row(idiv, 'hardware', '<b>Hardware</b>', intf['kismet.datasource.probed.hardware']);
697         set_row(idiv, 'description', '<b>Type</b>', intf['kismet.datasource.type_driver']['kismet.datasource.driver.description']);
698
699         var addbutton = $('#add', idiv);
700         if (addbutton.length == 0) {
701             addbutton =
702                 $('<button>', {
703                     id: 'addbutton',
704                     interface: intf['kismet.datasource.probed.interface'],
705                     intftype: intf['kismet.datasource.type_driver']['kismet.datasource.driver.type'],
706                 })
707                 .html('Enable Source')
708                 .button()
709                 .on('click', function() {
710                     var intf = $(this).attr('interface');
711                     var idiv = $('#' + intf, ds_state['ds_content']);
712
713                     $('.k-ds-modal-message', idiv).html("Opening datasource...");
714                     $('.k-ds-modal', idiv).show();
715
716                     var jscmd = {
717                         "definition": $(this).attr('interface') + ':type=' + $(this).attr('intftype')
718                     };
719
720                     ds_state['defer_command_progress'] = true;
721
722                     var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
723                     $.post(local_uri_prefix + "datasource/add_source.cmd", postdata, "json")
724                     .always(function() {
725                         ds_state['defer_command_progress'] = false;
726                         idiv.remove();
727                     });
728
729                 });
730         }
731
732         set_row(idiv, 'addsource', $('<span>'), addbutton);
733
734         idiv.accordion("refresh");
735     }
736     // console.log("updating with ", ds_state['kismet_sources'].length);
737
738     for (var source of ds_state['kismet_sources']) {
739         var sdiv = $('#' + source['kismet.datasource.uuid'], ds_state['ds_content']);
740
741         if (sdiv.length == 0) {
742             sdiv = $('<div>', {
743                 id: source['kismet.datasource.uuid'],
744                 class: 'accordion source',
745                 })
746             .append(
747                 $('<h3>', {
748                     id: 'header',
749                 })
750                 .append(
751                     $('<span>', {
752                         id: 'error',
753                     })
754                 )
755                 .append(
756                     $('<span>', {
757                         id: 'paused',
758                     })
759                 )
760                 .append(
761                     $('<span>', {
762                         class: 'k-ds-source',
763                     })
764                     .html(source['kismet.datasource.name'])
765                 )
766                 .append(
767                     $('<span>', {
768                         id: 'rrd',
769                         class: 'k-ds-rrd',
770                     })
771                 )
772             ).append(
773                 $('<div>', {
774                     // id: 'content',
775                     class: 'k-ds_content',
776                 })
777             );
778
779             var wrapper = $('<div>', {
780                 "style": "position: relative;",
781             });
782
783             var table = $('<table>', {
784                 class: 'k-ds-table'
785                 });
786
787             var modal = $('<div>', {
788                 class: 'k-ds-modal',
789             }).append(
790                 $('<div>', {
791                     class: 'k-ds-modal-content',
792                 })
793                 .append(
794                     $('<div>', {
795                         class: "k-ds-modal-message",
796                         style: "font-size: 150%; margin-bottom: 10px;",
797                     }).html("Loading...")
798                 ).append(
799                     $('<i>', {
800                         class: "fa fa-3x fa-cog fa-spin",
801                     })
802                 )
803             );
804
805             wrapper.append(table);
806             wrapper.append(modal);
807             modal.hide();
808
809             $('.k-ds_content', sdiv).append(wrapper);
810
811             sdiv.accordion({ collapsible: true, active: false });
812
813             ds_state['ds_content'].append(sdiv);
814         }
815
816         if (typeof(source['kismet.datasource.packets_rrd']) !== 'undefined' &&
817                 source['kismet.datasource.packets_rrd'] != 0) {
818
819             var simple_rrd =
820                 kismet.RecalcRrdData(
821                     source['kismet.datasource.packets_rrd'],
822                     source['kismet.datasource.packets_rrd']['kismet.common.rrd.last_time'],
823                     kismet.RRD_SECOND,
824                     source['kismet.datasource.packets_rrd']['kismet.common.rrd.minute_vec'], {
825                         transform: function(data, opt) {
826                         var slices = 3;
827                         var peak = 0;
828                         var ret = new Array();
829
830                         for (var ri = 0; ri < data.length; ri++) {
831                             peak = Math.max(peak, data[ri]);
832
833                             if ((ri % slices) == (slices - 1)) {
834                                 ret.push(peak);
835                                 peak = 0;
836                             }
837                         }
838
839                         return ret;
840                     }
841                     });
842
843             // Render the sparkline
844             $('#rrd', sdiv).sparkline(simple_rrd, {
845                 type: "bar",
846                 width: 100,
847                 height: 12,
848                 barColor: '#000000',
849                 nullColor: '#000000',
850                 zeroColor: '#000000'
851                 });
852         }
853
854         // Find the channel buttons
855         var chanbuttons = $('#chanbuttons', sdiv);
856
857         if (chanbuttons.length == 0) {
858             // Make a new one of all possible channels
859             chanbuttons = $('<div>', {
860                 id: 'chanbuttons',
861                 uuid: source['kismet.datasource.uuid']
862             });
863
864             chanbuttons.append(
865               $('<button>', {
866                 id: "all",
867                 uuid: source['kismet.datasource.uuid']
868               }).html("All")
869               .button()
870               .on('click', function(){
871                 ds_state['defer_command_progress'] = true;
872
873                 var uuid = $(this).attr('uuid');
874                 var chans = [];
875                 $('button.chanbutton[uuid=' + uuid + ']', ds_state['ds_content']).each(function(i) {
876                     chans.push($(this).attr('channel'));
877                 });
878                 var jscmd = {
879                     "cmd": "hop",
880                     "channels": chans,
881                     "uuid": uuid
882                 };
883                 var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
884
885                   try {
886                       $.ajax({
887                           url: `${local_uri_prefix}datasource/by-uuid/${uuid}/set_channel.cmd`, 
888                           method: 'POST',
889                           data: postdata,
890                           dataType: 'json',
891                           success: function(data) { },
892                           timeout: 30000,
893                       });
894                   } finally {
895                       ds_state['defer_command_progress'] = false;
896                   }
897                 $('button.chanbutton[uuid=' + uuid + ']', ds_state['ds_content']).each(function(i){
898                       $(this).removeClass('disable-chan-system');
899                       $(this).removeClass('enable-chan-system');
900                       $(this).removeClass('disable-chan-user');
901                       $(this).addClass('enable-chan-user');
902                     })
903                 })
904               );
905
906             for (var c of source['kismet.datasource.channels']) {
907                 chanbuttons.append(
908                     $('<button>', {
909                         id: c,
910                         channel: c,
911                         uuid: source['kismet.datasource.uuid'],
912                         class: 'chanbutton'
913                     }).html(c)
914                     .button()
915                     .on('click', function() {
916                         var uuid = $(this).attr('uuid');
917
918                         var sdiv = $('#' + uuid, ds_state['ds_content']);
919                         sdiv.addClass("channel_pending");
920
921                         // If we're in channel lock mode, we highlight a single channel
922                         if ($('#lock[uuid=' + uuid + ']', ds_state['ds_content']).hasClass('enable-chan-user')) {
923                             // Only do something if we're not selected
924                             if (!($(this).hasClass('enable-chan-user'))) {
925                                 // Remove from all lock channels
926                                 $('button.chanbutton[uuid=' + uuid + ']').each(function(i) {
927                                         $(this).removeClass('enable-chan-user');
928                                     });
929                                 $('button.chanbutton[uuid=' + uuid + ']').removeClass('enable-chan-system');
930                                 // Set this channel
931                                 $(this).addClass('enable-chan-user');
932
933                             } else {
934                                 return;
935                             }
936
937                             ds_state['defer_source_update'] = true;
938                             ds_state['defer_command_progress'] = true;
939
940                             // Clear any existing timer
941                             if (uuid in ds_state['chantids'])
942                                clearTimeout(ds_state['chantids'][uuid]);
943
944                             // Immediately post w/out a timeout
945                             var jscmd = {
946                                 "cmd": "lock",
947                                 "uuid": uuid,
948                                 "channel": $(this).attr('channel'),
949                             };
950
951                             $('.k-ds-modal-message', sdiv).html("Setting channel...");
952                             $('.k-ds-modal', sdiv).show();
953
954                             var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
955
956                             try {
957                                 $.ajax({
958                                     url: `${local_uri_prefix}datasource/by-uuid/${uuid}/set_channel.cmd`,
959                                     method: 'POST',
960                                     data: postdata,
961                                     dataType: 'json',
962                                     success: function(data) {
963                                         data = kismet.sanitizeObject(data);
964                                         for (var u in ds_state['datasources']) {
965                                             if (ds_state['datasources'][u]['kismet.datasource.uuid'] == data['kismet.datasource.uuid']) {
966                                                 ds_state['datasources'][u] = data;
967                                                 ds_state['remove_pending'].push(uuid);
968                                                 update_datasource2(null);
969                                                 break;
970                                             }
971                                         }
972                                     },
973                                     timeout: 30000,
974                                 });
975                             } finally {
976                                 ds_state['remove_pending'].push(uuid);
977                                 ds_state['defer_command_progress'] = false;
978                                 sdiv.removeClass("channel_pending");
979                             }
980
981                             return;
982                         } else {
983                             // we're in hop mode
984                             if ($(this).hasClass('enable-chan-user') || $(this).hasClass('enable-chan-system')) {
985                                 $(this).removeClass('enable-chan-user');
986                                 $(this).removeClass('enable-chan-system');
987
988                                 $(this).addClass('disable-chan-user');
989                             } else {
990                                 $(this).removeClass('disable-chan-user');
991                                 $(this).addClass('enable-chan-user');
992                             }
993
994                             // Clear any old timer
995                             if (uuid in ds_state['chantids'])
996                                 clearTimeout(ds_state['chantids'][uuid]);
997
998                             // Set a timer to trigger in the future setting any channels
999                             ds_state['chantids'][uuid] = setTimeout(function() {
1000                                 ds_state['defer_command_progress'] = true;
1001                                 ds_state['defer_source_update'] = true;
1002
1003                                 var sdiv = $('#' + uuid, ds_state['ds_content']);
1004                                 sdiv.addClass("channel_pending");
1005
1006                                 $('.k-ds-modal-message', sdiv).html("Setting channels...");
1007                                 $('.k-ds-modal', sdiv).show();
1008
1009                                 var chans = [];
1010
1011                                 $('button.chanbutton[uuid=' + uuid + ']', ds_state['ds_content']).each(function(i) {
1012                                     // If we're hopping, collect user and system
1013                                     if ($(this).hasClass('enable-chan-user') ||
1014                                         $(this).hasClass('enable-chan-system')) {
1015                                         ds_state['refresh' + uuid] = true;
1016                                         chans.push($(this).attr('channel'));
1017                                     }
1018                                 });
1019
1020                                 var jscmd = {
1021                                     "cmd": "hop",
1022                                     "uuid": uuid,
1023                                     "channels": chans
1024                                 };
1025
1026                                 var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
1027                                 try {
1028                                     $.ajax({
1029                                         url: `${local_uri_prefix}datasource/by-uuid/${uuid}/set_channel.cmd`,
1030                                         method: 'POST',
1031                                         data: postdata,
1032                                         dataType: 'json',
1033                                         success: function(data) {
1034                                             data = kismet.sanitizeObject(data);
1035                                             for (var u in ds_state['datasources']) {
1036                                                 if (ds_state['datasources'][u]['kismet.datasource.uuid'] == data['kismet.datasource.uuid']) {
1037                                                     ds_state['datasources'][u] = data;
1038                                                     ds_state['remove_pending'].push(uuid);
1039                                                     update_datasource2(null);
1040                                                     break;
1041                                                 }
1042                                             }
1043                                         },
1044                                         timeout: 30000,
1045                                     });
1046                                 } finally {
1047                                     ds_state['remove_pending'].push(uuid);
1048                                     ds_state['defer_command_progress'] = false;
1049                                     sdiv.removeClass("channel_pending");
1050                                 }
1051                             }, 2000);
1052                         }
1053                     })
1054                 );
1055             }
1056         }
1057
1058         var pausediv = $('#pausediv', sdiv);
1059         if (pausediv.length == 0) {
1060             pausediv = $('<div>', {
1061                 id: 'pausediv',
1062                 uuid: source['kismet.datasource.uuid']
1063             });
1064
1065             pausediv.append(
1066                 $('<button>', {
1067                     id: "opencmd",
1068                     uuid: source['kismet.datasource.uuid']
1069                 }).html('Activate')
1070                 .button()
1071                 .on('click', function() {
1072                     ds_state['defer_command_progress'] = true;
1073                     ds_state['defer_source_update'] = true;
1074
1075                     var uuid = $(this).attr('uuid');
1076                     var sdiv = $('#' + uuid, ds_state['ds_content']);
1077
1078                     $('.k-ds-modal-message', sdiv).html("Activating datasource...");
1079                     $('.k-ds-modal', sdiv).show();
1080
1081                     $('#closecmd[uuid=' + uuid + ']', ds_state['ds_content']).removeClass('enable-chan-user');
1082                     $(this).addClass('enable-chan-user');
1083
1084                     $.get(local_uri_prefix + '/datasource/by-uuid/' + uuid + '/open_source.cmd')
1085                     .done(function(data) {
1086                         data = kismet.sanitizeObject(data);
1087
1088                         for (var u in ds_state['datasources']) {
1089                             if (ds_state['datasources'][u]['kismet.datasource.uuid'] == data['kismet.datasource.uuid']) {
1090                                 ds_state['datasources'][u] = data;
1091                                 update_datasource2(null);
1092                                 break;
1093                             }
1094                         }
1095                     })
1096                     .always(function() {
1097                             ds_state['defer_command_progress'] = false;
1098                             ds_state['remove_pending'].push(uuid);
1099                     });
1100
1101                 })
1102             );
1103
1104             pausediv.append(
1105                 $('<button>', {
1106                     id: "closecmd",
1107                     uuid: source['kismet.datasource.uuid']
1108                 }).html('Close')
1109                 .button()
1110                 .on('click', function() {
1111                     ds_state['defer_command_progress'] = true;
1112                     ds_state['defer_source_update'] = true;
1113
1114                     var uuid = $(this).attr('uuid');
1115                     var sdiv = $('#' + uuid, ds_state['ds_content']);
1116
1117                     $('.k-ds-modal-message', sdiv).html("Closing datasource...");
1118                     $('.k-ds-modal', sdiv).show();
1119                         
1120                     $(this).addClass('enable-chan-user');
1121                     $('#opencmd[uuid=' + uuid + ']', ds_state['ds_content']).removeClass('enable-chan-user');
1122
1123                     $.get(local_uri_prefix + '/datasource/by-uuid/' + uuid + '/close_source.cmd')
1124                     .done(function(data) {
1125                         data = kismet.sanitizeObject(data);
1126
1127                         for (var u in ds_state['datasources']) {
1128                             if (ds_state['datasources'][u]['kismet.datasource.uuid'] == data['kismet.datasource.uuid']) {
1129                                 ds_state['remove_pending'].push(uuid);
1130                                 ds_state['datasources'][u] = data;
1131                                 update_datasource2(null);
1132                                 break;
1133                             }
1134                         }
1135                     })
1136                     .always(function() {
1137                         ds_state['remove_pending'].push(uuid);
1138                         ds_state['defer_command_progress'] = false;
1139                     });
1140
1141                 })
1142             );
1143
1144             pausediv.append(
1145                 $('<button>', {
1146                     id: "disablecmd",
1147                     uuid: source['kismet.datasource.uuid']
1148                 }).html('Disable')
1149                 .button()
1150                 .on('click', function() {
1151                     ds_state['defer_command_progress'] = true;
1152                     ds_state['defer_source_update'] = true;
1153
1154                     var uuid = $(this).attr('uuid');
1155                     var sdiv = $('#' + uuid, ds_state['ds_content']);
1156
1157                     $('.k-ds-modal-message', sdiv).html("Disabling datasource...");
1158                     $('.k-ds-modal', sdiv).show();
1159                         
1160                     $(this).addClass('enable-chan-user');
1161                     $('#opencmd[uuid=' + uuid + ']', ds_state['ds_content']).removeClass('enable-chan-user');
1162
1163                     $.get(local_uri_prefix + '/datasource/by-uuid/' + uuid + '/disable_source.cmd')
1164                     .done(function(data) {
1165                         data = kismet.sanitizeObject(data);
1166
1167                         for (var u in ds_state['datasources']) {
1168                             if (ds_state['datasources'][u]['kismet.datasource.uuid'] == data['kismet.datasource.uuid']) {
1169                                 ds_state['remove_pending'].push(uuid);
1170                                 ds_state['datasources'][u] = data;
1171                                 update_datasource2(null);
1172                                 break;
1173                             }
1174                         }
1175                     })
1176                     .always(function() {
1177                         ds_state['remove_pending'].push(uuid);
1178                         ds_state['defer_command_progress'] = false;
1179                     });
1180
1181                 })
1182             );
1183
1184             pausediv.append(
1185                 $('<p>', {
1186                     id: 'pausetext',
1187                     uuid: source['kismet.datasource.uuid']
1188                 })
1189                 .html('Source is currently closed and inactive.')
1190             );
1191         }
1192
1193         if (source['kismet.datasource.running']) {
1194             $('button#closecmd', sdiv).html("Close");
1195             $('button#opencmd', sdiv).html("Running");
1196         } else {
1197             $('button#closecmd', sdiv).html("Closed");
1198             $('button#opencmd', sdiv).html("Activate");
1199         }
1200
1201         var quickopts = $('#quickopts', sdiv);
1202         if (quickopts.length == 0) {
1203           quickopts = $('<div>', {
1204               id: 'quickopts',
1205               uuid: source['kismet.datasource.uuid']
1206           });
1207
1208           quickopts.append(
1209             $('<button>', {
1210               id: "lock",
1211               uuid: source['kismet.datasource.uuid']
1212             }).html("Lock")
1213             .button()
1214             .on('click', function(){
1215               ds_state['defer_source_update'] = true;
1216               ds_state['defer_command_progress'] = true;
1217
1218               var uuid = $(this).attr('uuid');
1219               var sdiv = $('#' + uuid, ds_state['ds_content']);
1220
1221               $('.k-ds-modal-message', sdiv).html("Locking channels...");
1222               $('.k-ds-modal', sdiv).show();
1223
1224               $('#hop[uuid=' + uuid + ']', ds_state['ds_content']).removeClass('enable-chan-user');
1225               $('#lock[uuid=' + uuid + ']', ds_state['ds_content']).addClass('enable-chan-user');
1226
1227               var firstchanobj = $('button.chanbutton[uuid=' + uuid + ']', ds_state['ds_content']).first();
1228
1229               var chan = firstchanobj.attr('channel');
1230
1231               var jscmd = {
1232                   "cmd": "lock",
1233                   "channel": chan,
1234                   "uuid": uuid
1235               };
1236               var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
1237
1238                 try {
1239                     $.ajax({
1240                         url: `${local_uri_prefix}datasource/by-uuid/${uuid}/set_channel.cmd`,
1241                         method: 'POST',
1242                         data: postdata,
1243                         dataType: 'json',
1244                         success: function(data) {
1245                             data = kismet.sanitizeObject(data);
1246                             for (var u in ds_state['datasources']) {
1247                                 if (ds_state['datasources'][u]['kismet.datasource.uuid'] == data['kismet.datasource.uuid']) {
1248                                     ds_state['datasources'][u] = data;
1249                                     ds_state['remove_pending'].push(uuid);
1250                                     update_datasource2(null);
1251                                     break;
1252                                 }
1253                             }
1254                         },
1255                         timeout: 30000,
1256                     });
1257                 } finally {
1258                     ds_state['remove_pending'].push(uuid);
1259                 }
1260
1261               $('button.chanbutton[uuid='+ uuid + ']', ds_state['ds_content']).each(function(i) {
1262                       $(this).removeClass('enable-chan-system');
1263                       $(this).removeClass('disable-chan-user');
1264               });
1265
1266               // Disable all but the first available channel
1267               firstchanobj.removeClass('disabled-chan-user');
1268               firstchanobj.removeClass('enable-chan-system');
1269               firstchanobj.addClass('enable-chan-user')
1270
1271               })
1272             );
1273
1274           quickopts.append(
1275             $('<button>', {
1276               id: "hop",
1277               uuid: source['kismet.datasource.uuid']
1278             }).html("Hop")
1279             .button()
1280             .on('click', function(){
1281               ds_state['defer_source_update'] = true;
1282               ds_state['defer_command_progress'] = true;
1283
1284               var uuid = $(this).attr('uuid');
1285               var sdiv = $('#' + uuid, ds_state['ds_content']);
1286
1287               $('.k-ds-modal-message', sdiv).html("Setting channel hopping...");
1288               $('.k-ds-modal', sdiv).show();
1289
1290               $('#hop[uuid=' + uuid + ']', ds_state['ds_content']).addClass('enable-chan-user');
1291               $('#lock[uuid=' + uuid + ']', ds_state['ds_content']).removeClass('enable-chan-user');
1292
1293               var chans = [];
1294               $('button.chanbutton[uuid=' + uuid + ']', ds_state['ds_content']).each(function(i) {
1295                       chans.push($(this).attr('channel'));
1296                 });
1297
1298               var jscmd = {
1299                   "cmd": "hop",
1300                   "channels": chans,
1301                   "uuid": uuid
1302               };
1303
1304               var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
1305
1306                 try {
1307                     $.ajax({
1308                         url: `${local_uri_prefix}datasource/by-uuid/${uuid}/set_channel.cmd`,
1309                         method: 'POST',
1310                         data: postdata,
1311                         dataType: 'json',
1312                         success: function(data) {
1313                             data = kismet.sanitizeObject(data);
1314                             for (var u in ds_state['datasources']) {
1315                                 if (ds_state['datasources'][u]['kismet.datasource.uuid'] == data['kismet.datasource.uuid']) {
1316                                     ds_state['datasources'][u] = data;
1317                                     ds_state['remove_pending'].push(uuid);
1318                                     update_datasource2(null);
1319                                     break;
1320                                 }
1321                             }
1322                         },
1323                         timeout: 30000,
1324                     });
1325                 } finally {
1326                     ds_state['remove_pending'].push(uuid);
1327                 }
1328
1329               $('button.chanbutton[uuid='+ uuid + ']', ds_state['ds_content']).each(function(i) {
1330                   // Disable all but the first available channel
1331                   if ($(this).attr('channel') == 1) {
1332                       $(this).removeClass('disabled-chan-user');
1333                       $(this).removeClass('enable-chan-system');
1334                       $(this).addClass('enable-chan-user')
1335                   } else {
1336                       $(this).removeClass('enable-chan-system');
1337                       $(this).removeClass('disable-chan-user');
1338                   }
1339               });
1340               })
1341             );
1342
1343           quickopts.append(
1344             $('<span>', {
1345               id: "hoprate"
1346               }).html("")
1347             );
1348         }
1349
1350         var uuid = source['kismet.datasource.uuid'];
1351         var hop_chans = source['kismet.datasource.hop_channels'];
1352         var lock_chan = source['kismet.datasource.channel'];
1353         var hopping = source['kismet.datasource.hopping'];
1354
1355         if (!sdiv.hasClass('channel_pending')) {
1356             if (source['kismet.datasource.hopping']) {
1357                 $('#hop', quickopts).addClass('enable-chan-user');
1358                 $('#lock', quickopts).removeClass('enable-chan-user');
1359                 $('#hoprate', quickopts).html("  (Hopping at " + 
1360                         hop_to_human(source['kismet.datasource.hop_rate']) + ")");
1361                 $('#hoprate', quickopts).show();
1362             } else {
1363                 $('#hop', quickopts).removeClass('enable-chan-user');
1364                 $('#lock', quickopts).addClass('enable-chan-user');
1365                 $('#hoprate', quickopts).hide();
1366             }
1367
1368             $('button.chanbutton', chanbuttons).each(function(i) {
1369                 var chan = $(this).attr('channel');
1370
1371                 // If locked, only highlight locked channel
1372                 if (!hopping) {
1373                     if (chan === lock_chan) {
1374                         $(this).addClass('enable-chan-user');
1375                         $(this).removeClass('enable-chan-system');
1376                     } else {
1377                         $(this).removeClass('enable-chan-user');
1378                         $(this).removeClass('enable-chan-system');
1379                     }
1380
1381                     return;
1382                 }
1383
1384                 // Flag the channel if it's found, and not explicitly disabled
1385                 if (hop_chans.indexOf(chan) != -1 && !($(this).hasClass('disable-chan-user'))) {
1386                     $(this)
1387                     .addClass('enable-chan-system');
1388                 } else {
1389                     $(this)
1390                     .removeClass('enable-chan-system');
1391                 }
1392             });
1393
1394             if (source['kismet.datasource.running']) {
1395                 $('#closecmd', pausediv).removeClass('enable-chan-user');
1396                 $('#opencmd', pausediv).addClass('enable-chan-user');
1397                 $('#pausetext', pausediv).hide();
1398             } else {
1399                 $('#closecmd', pausediv).addClass('enable-chan-user');
1400                 $('#opencmd', pausediv).removeClass('enable-chan-user');
1401                 $('#pausetext', pausediv).show();
1402             }
1403
1404         }
1405
1406         var s = source['kismet.datasource.interface'];
1407
1408         if (source['kismet.datasource.interface'] !==
1409                 source['kismet.datasource.capture_interface']) {
1410             s = s + "(" + source['kismet.datasource.capture_interface'] + ")";
1411         }
1412
1413         if (source['kismet.datasource.error']) {
1414             $('#error', sdiv).html('<i class="k-ds-error fa fa-exclamation-circle"></i>');
1415             top_row(sdiv, 'error', '<i class="k-ds-error fa fa-exclamation-circle"></i><b>Error</b>',
1416                     source['kismet.datasource.error_reason']);
1417         } else {
1418             $('#error', sdiv).empty();
1419             $('tr#error', sdiv).remove();
1420         }
1421
1422         if (!source['kismet.datasource.running']) {
1423             $('#paused', sdiv).html('<i class="k-ds-paused fa fa-pause-circle"></i>');
1424         } else {
1425             $('#paused', sdiv).empty();
1426         }
1427
1428         set_row(sdiv, 'interface', '<b>Interface</b>', s);
1429         if (source['kismet.datasource.hardware'] !== '')
1430             set_row(sdiv, 'hardware', '<b>Hardware</b>', source['kismet.datasource.hardware']);
1431         set_row(sdiv, 'uuid', '<b>UUID</b>', source['kismet.datasource.uuid']);
1432         set_row(sdiv, 'packets', '<b>Packets</b>', source['kismet.datasource.num_packets']);
1433
1434         var rts = "";
1435         if (source['kismet.datasource.remote']) {
1436             rts = 'Remote sources are not re-opened by Kismet, but will be re-opened when the ' +
1437                 'remote source reconnects.';
1438         } else if (source['kismet.datasource.passive']) {
1439             rts = 'Passive sources are not directly managed by Kismet, they accept data ' +
1440                 'from external services.';
1441         } else if (source['kismet.datasource.retry']) {
1442             rts = 'Kismet will try to re-open this source if an error occurs';
1443             if (source['kismet.datasource.retry_attempts'] && 
1444                     source['kismet.datasource.running'] == 0) {
1445                 rts = rts + ' (Tried ' + source['kismet.datasource.retry_attempts'] + ' times)';
1446             }
1447         } else {
1448             rts = 'Kismet will not re-open this source';
1449         }
1450
1451         set_row(sdiv, 'retry', '<b>Retry on Error</b>', rts);
1452         set_row(sdiv, 'pausing', '<b>Active</b>', pausediv);
1453
1454         if (source['kismet.datasource.running']) {
1455             if (source['kismet.datasource.type_driver']['kismet.datasource.driver.tuning_capable']) {
1456                 set_row(sdiv, 'chanopts', '<b>Channel Options</b>', quickopts);
1457                 set_row(sdiv, 'channels', '<b>Channels</b>', chanbuttons);
1458             } else {
1459                 $('tr#chanopts', sdiv).remove();
1460                 $('tr#channels', sdiv).remove();
1461             }
1462         } else {
1463             $('tr#chanopts', sdiv).remove();
1464             $('tr#channels', sdiv).remove();
1465         }
1466
1467         try {
1468             sdiv.accordion("refresh");
1469         } catch (e) { 
1470             ;
1471         }
1472     }
1473 }
1474
1475 export const DataSources2 = () => {
1476     var w = $(window).width() * 0.95;
1477     var h = $(window).height() * 0.75;
1478     var offy = 20;
1479
1480     if ($(window).width() < 450 || $(window).height() < 450) {
1481         w = $(window).width() - 5;
1482         h = $(window).height() - 5;
1483         offy = 0;
1484     }
1485
1486     ds_state = {};
1487     ds_state['remove_pending'] = []
1488     ds_state['chantids'] = {}
1489
1490     var content =
1491         $('<div class="k-ds-contentdiv">');
1492
1493     ds_state['closed'] = 0;
1494
1495     ds_state['panel'] = $.jsPanel({
1496         id: 'datasources',
1497         headerTitle: '<i class="fa fa-cogs" /> Data Sources',
1498         headerControls: {
1499             iconfont: 'jsglyph',
1500             minimize: 'remove',
1501             smallify: 'remove',
1502         },
1503         content: content,
1504
1505         resizable: {
1506             stop: function(event, ui) {
1507                 $('div.accordion', ui.element).accordion("refresh");
1508             }
1509         },
1510
1511         onmaximized: function() {
1512             $('div.accordion', this.content).accordion("refresh");
1513         },
1514
1515         onnormalized: function() {
1516             $('div.accordion', this.content).accordion("refresh");
1517         },
1518
1519         onclosed: function() {
1520             ds_state['closed'] = 1;
1521
1522             if ('datasource_get_tid' in ds_state)
1523                 clearTimeout(ds_state['datasource_get_tid']);
1524             if ('datasource_interface_tid' in ds_state)
1525                 clearTimeout(ds_state['datasource_interface_tid']);
1526         }
1527     })
1528     .resize({
1529         width: w,
1530         height: h
1531     })
1532     .reposition({
1533         my: 'center-top',
1534         at: 'center-top',
1535         of: 'window',
1536         offsetY: offy,
1537     })
1538     .contentResize();
1539
1540     ds_state["content"] = content;
1541     ds_state["ds_content"] = content;
1542     ds_state["kismet_sources"] = [];
1543     ds_state["kismet_interfaces"] = [];
1544
1545     datasource_source_refresh(function(data) {
1546         update_datasource2(data);
1547         });
1548     datasource_interface_refresh(function(data) {
1549         update_datasource2(data);
1550         });
1551 }
1552
1553 /* Get the list of active sources */
1554 function datasource_source_refresh(cb) {
1555     var grab_sources = function(cb) {
1556         $.get(local_uri_prefix + "datasource/all_sources.json")
1557         .done(function(data) {
1558             ds_state['kismet_sources'] = kismet.sanitizeObject(data);
1559             cb(data);
1560             ds_state['defer_source_update'] = false;
1561         })
1562         .always(function() {
1563             if (ds_state['closed'] == 1)
1564                 return;
1565
1566             ds_state['datasource_get_tid'] = setTimeout(function() {
1567                 datasource_source_refresh(cb)
1568             }, 1000);
1569         });
1570     };
1571
1572     grab_sources(cb);
1573
1574 }
1575
1576 /* Get the list of potential interfaces */
1577 function datasource_interface_refresh(cb) {
1578     var grab_interfaces = function(cb) {
1579         try {
1580             $.ajax({
1581                 url: local_uri_prefix + "datasource/list_interfaces.json",
1582                 success: function(data) {
1583                     ds_state['kismet_interfaces'] = kismet.sanitizeObject(data);
1584                     ds_state['done_interface_update'] = true;
1585                     cb(data);
1586                     ds_state['defer_interface_update'] = false;
1587                 },
1588                 timeout: 30000,
1589             });
1590         } finally {
1591             if (ds_state['closed'] == 1)
1592                 return;
1593
1594             ds_state['datasource_interface_tid'] = setTimeout(function() {
1595                 datasource_interface_refresh(cb)
1596             }, 3000);
1597         }
1598     };
1599
1600     grab_interfaces(cb);
1601 }
1602