40f30cbf5497f22963d200bb455abe2f13f3d0ca
[kismet-logviewer.git] / logviewer / static / js / jquery.kismet.channeldisplay.js
1 // Display the channel records system from Kismet
2 //
3 // Requires js-storage and jquery be loaded first
4 //
5 // dragorn@kismetwireless.net
6 // MIT/GPL License (pick one); the web ui code is licensed more
7 // freely than GPL to reflect the generally MIT-licensed nature
8 // of the web/jquery environment
9 //
10
11
12 (function($) {
13     var local_uri_prefix = "";
14     if (typeof(KISMET_URI_PREFIX) !== 'undefined')
15         local_uri_prefix = KISMET_URI_PREFIX;
16
17     var base_options = {
18         url: ""
19     };
20
21     var channeldisplay_refresh = function(state) {
22         clearTimeout(state.timerid);
23
24         if (!kismet_ui.window_visible || state.element.is(':hidden')) {
25             state.timerid = -1;
26             return;
27         }
28
29         $.get(local_uri_prefix + state.options.url + "channels/channels.json")
30         .done(function(data) {
31             data = kismet.sanitizeObject(data);
32
33             var devtitles = new Array();
34             var devnums = new Array();
35
36             // Chart type from radio buttons
37             var charttype = $("input[name='graphtype']:checked", state.graphtype).val();
38             // Chart timeline from selector
39             var charttime = $('select#historyrange option:selected', state.element).val();
40             // Frequency translation from selector
41             var freqtrans = $('select#k_cd_freq_selector option:selected', state.element).val();
42             // Pull from the stored value instead of the live
43             var filter_string = state.storage.get('jquery.kismet.channels.filter');
44
45             // historical line chart
46             if (charttype === 'history') {
47                 var pointtitles = new Array();
48                 var datasets = new Array();
49                 var title = "";
50
51                 var rrd_data = null;
52
53                 if (charttime === 'min') {
54
55                     for (var x = 60; x > 0; x--) {
56                         if (x % 5 == 0) {
57                             pointtitles.push(x);
58                         } else {
59                             pointtitles.push('');
60                         }
61                     }
62
63                     rrd_type = kismet.RRD_SECOND;
64                     rrd_data = "kismet.channelrec.device_rrd/kismet.common.rrd.minute_vec";
65                 } else if (charttime === 'hour') {
66
67                     for (var x = 60; x > 0; x--) {
68                         if (x % 5 == 0) {
69                             pointtitles.push(x);
70                         } else {
71                             pointtitles.push('');
72                         }
73                     }
74
75                     rrd_type = kismet.RRD_MINUTE;
76                     rrd_data = "kismet.channelrec.device_rrd/kismet.common.rrd.hour_vec";
77
78                 } else /* day */ {
79
80                     for (var x = 24; x > 0; x--) {
81                         if (x % 4 == 0) {
82                             pointtitles.push(x);
83                         } else {
84                             pointtitles.push('');
85                         }
86                     }
87
88                     rrd_type = kismet.RRD_HOUR;
89                     rrd_data = "kismet.channelrec.device.rrd/kismet.common.rrd.day_vec";
90
91                 }
92
93                 // Position in the color map
94                 var colorpos = 0;
95                 var nkeys = Object.keys(data['kismet.channeltracker.frequency_map']).length;
96
97                 var filter = $('select#gh_filter', state.element);
98                 filter.empty();
99
100                 if (filter_string === '' || filter_string === 'any') {
101                     filter.append(
102                         $('<option>', {
103                             value: "",
104                             selected: "selected",
105                         })
106                         .text("Any")
107                     );
108                 }  else {
109                     filter.append(
110                         $('<option>', {
111                             value: "any",
112                         })
113                         .text("Any")
114                     );
115                 }
116
117                 for (var fk in data['kismet.channeltracker.frequency_map']) {
118                     var linedata =
119                         kismet.RecalcRrdData(
120                             data['kismet.channeltracker.frequency_map'][fk]['kismet.channelrec.device_rrd']['kismet.common.rrd.last_time'],
121                             data['kismet.channeltracker.frequency_map'][fk]['kismet.channelrec.device_rrd']['kismet.common.rrd.last_time'],
122                             rrd_type,
123                             kismet.ObjectByString(
124                                 data['kismet.channeltracker.frequency_map'][fk],
125                                 rrd_data),
126                             {});
127
128                     // Convert the freq name
129                     var cfk = kismet_ui.GetConvertedChannel(freqtrans, fk);
130
131                     var label = "";
132
133                     if (cfk == fk)
134                         label = kismet.HumanReadableFrequency(parseInt(fk));
135                     else
136                         label = cfk;
137
138                     // Make a filter option
139                     if (filter_string === fk) {
140                         filter.append(
141                             $('<option>', {
142                                 value: fk,
143                                 selected: "selected",
144                             })
145                             .text(label)
146                         );
147                     } else {
148                         filter.append(
149                             $('<option>', {
150                                 value: fk,
151                             })
152                             .text(label)
153                         );
154                     }
155
156                     // Rotate through the color wheel
157                     var color = parseInt(255 * (colorpos / nkeys));
158                     colorpos++;
159
160                     // Build the dataset record
161                     var ds = {
162                         label:  label,
163                         fill: true,
164                         lineTension: 0.1,
165                         data: linedata,
166                         borderColor: "hsl(" + color + ", 100%, 50%)",
167                         backgroundColor: "hsl(" + color + ", 100%, 50%)",
168                     };
169
170                     // Add it to the dataset if we're not filtering
171                     if (filter_string === fk ||
172                         filter_string === '' ||
173                         filter_string === 'any') {
174                             datasets.push(ds);
175                     }
176                 }
177
178                 state.devgraph_canvas.hide();
179                 state.timegraph_canvas.show();
180                 state.coming_soon.hide();
181
182                 if (state.timegraph_chart == null) {
183                     var device_options = {
184                         type: "bar",
185                         responsive: true,
186                         options: {
187                             maintainAspectRatio: false,
188                             scales: {
189                                 yAxes: [{
190                                     ticks: {
191                                         beginAtZero: true,
192                                     },
193                                     stacked: true,
194                                 }],
195                                 xAxes: [{
196                                     stacked: true,
197                                 }],
198                             },
199                             legend: {
200                                 labels: {
201                                     boxWidth: 15,
202                                     fontSize: 9,
203                                     padding: 5,
204                                 },
205                             },
206                         },
207                         data: {
208                             labels: pointtitles,
209                             datasets: datasets,
210                         }
211                     };
212
213                     state.timegraph_chart = new Chart(state.timegraph_canvas,
214                         device_options);
215                 } else {
216                     state.timegraph_chart.data.datasets = datasets;
217                     state.timegraph_chart.data.labels = pointtitles;
218
219                     state.timegraph_chart.update(0);
220                 }
221             } else {
222                 // 'now', but default - if for some reason we didn't get a
223                 // value from the selector, this falls through to the bar graph
224                 // which is what we probably really want
225                 for (var fk in data['kismet.channeltracker.frequency_map']) {
226                     var slot_now =
227                         (data['kismet.channeltracker.frequency_map'][fk]['kismet.channelrec.device_rrd']['kismet.common.rrd.last_time']) % 60;
228                     var dev_now = data['kismet.channeltracker.frequency_map'][fk]['kismet.channelrec.device_rrd']['kismet.common.rrd.minute_vec'][slot_now];
229
230                     var cfk = kismet_ui.GetConvertedChannel(freqtrans, fk);
231
232                     if (cfk == fk)
233                         devtitles.push(kismet.HumanReadableFrequency(parseInt(fk)));
234                     else
235                         devtitles.push(cfk);
236
237                     devnums.push(dev_now);
238                 }
239
240                 state.devgraph_canvas.show();
241                 state.timegraph_canvas.hide();
242
243                 if (state.devgraph_chart == null) {
244                     state.coming_soon.hide();
245
246                     var device_options = {
247                         type: "bar",
248                         options: {
249                             responsive: true,
250                             maintainAspectRatio: false,
251                             scales: {
252                                 yAxes: [{
253                                     ticks: {
254                                         beginAtZero: true,
255                                     }
256                                 }],
257                             },
258                         },
259                         data: {
260                             labels: devtitles,
261                             datasets: [
262                                 {
263                                     label: "Devices per Channel",
264                                     backgroundColor: 'rgba(160, 160, 160, 1)',
265                                     data: devnums,
266                                     borderWidth: 1,
267                                 }
268                             ],
269                         }
270                     };
271
272                     state.devgraph_chart = new Chart(state.devgraph_canvas,
273                         device_options);
274
275                 } else {
276                     state.devgraph_chart.data.datasets[0].data = devnums;
277                     state.devgraph_chart.data.labels = devtitles;
278
279                     state.devgraph_chart.update();
280                 }
281             }
282
283         })
284         .always(function() {
285             state.timerid = setTimeout(function() { channeldisplay_refresh(state); }, 5000);
286         });
287     };
288
289     var update_graphtype = function(state, gt = null) {
290
291         if (gt == null)
292             gt = $("input[name='graphtype']:checked", state.graphtype).val();
293
294         state.storage.set('jquery.kismet.channels.graphtype', gt);
295
296         if (gt === 'now') {
297             $("select#historyrange", state.graphtype).hide();
298             $("select#gh_filter", state.graphtype).hide();
299             $("label#gh_filter_label", state.graphtype).hide();
300         } else {
301             $("select#historyrange", state.graphtype).show();
302             $("label#gh_filter_label", state.graphtype).show();
303             $("select#gh_filter", state.graphtype).show();
304         }
305
306         charttime = $('select#historyrange option:selected', state.element).val();
307         state.storage.set('jquery.kismet.channels.range', charttime);
308     }
309
310     var channels_resize = function(state) {
311         console.log('resize container ', state.devgraph_container.width(), state.devgraph_container.height());
312
313         if (state.devgraph_canvas != null)
314             state.devgraph_canvas
315                 .prop('width', state.devgraph_container.width())
316                 .prop('height', state.devgraph_container.height());
317
318         if (state.timegraph_canvas != null)
319             state.timegraph_canvas
320                 .prop('width', state.devgraph_container.width())
321                 .prop('height', state.devgraph_container.height());
322
323         if (state.devgraph_chart != null)
324             state.devgraph_chart.resize();
325
326         if (state.timegraph_chart != null)
327             state.timegraph_chart.resize();
328
329         channeldisplay_refresh(state);
330     }
331
332     $.fn.channels = function(inopt) {
333         var state = {
334             element: $(this),
335             options: base_options,
336             timerid: -1,
337             devgraph_container: null,
338             devgraph_chart: null,
339             timegraph_chart: null,
340             devgraph_canvas: null,
341             timegraph_canvas: null,
342             picker: null,
343             graphtype: null,
344             coming_soon: null,
345             visible: false,
346             storage: null,
347             resizer: null,
348             reset_size: null,
349         };
350
351         // Modeled on the datatables resize function
352         state.resizer = $('<iframe/>')
353             .css({
354                 position: 'absolute',
355                 top: 0,
356                 left: 0,
357                 height: '100%',
358                 width: '100%',
359                 zIndex: -1,
360                 border: 0
361             })
362         .attr('frameBorder', '0')
363         .attr('src', 'about:blank');
364
365         state.resizer[0].onload = function() {
366                         var body = this.contentDocument.body;
367                         var height = body.offsetHeight;
368             var width = body.offsetWidth;
369                         var contentDoc = this.contentDocument;
370                         var defaultView = contentDoc.defaultView || contentDoc.parentWindow;
371
372                         defaultView.onresize = function () {
373                                 var newHeight = body.clientHeight || body.offsetHeight;
374                                 var docClientHeight = contentDoc.documentElement.clientHeight;
375
376                                 var newWidth = body.clientWidth || body.offsetWidth;
377                                 var docClientWidth = contentDoc.documentElement.clientWidth;
378
379                                 if ( ! newHeight && docClientHeight ) {
380                                         newHeight = docClientHeight;
381                                 }
382
383                                 if ( ! newWidth && docClientWidth ) {
384                                         newWidth = docClientWidth;
385                                 }
386
387                                 if ( newHeight !== height || newWidth !== width ) {
388                                         height = newHeight;
389                                         width = newWidth;
390                     console.log("triggered resizer", height, width);
391                     channels_resize(state);
392                                 }
393                         };
394                 };
395
396         state.resizer
397             .appendTo(state.element)
398             .attr('data', 'about:blank');
399
400         state.element.on('resize', function() {
401             console.log("element resize");
402             channels_resize(state);
403         });
404
405         state.element.addClass(".channels");
406
407         state.storage = Storages.localStorage;
408
409         var stored_gtype = "now";
410         var stored_channel = "Frequency";
411         var stored_range = "min";
412
413         if (state.storage.isSet('jquery.kismet.channels.graphtype'))
414             stored_gtype = state.storage.get('jquery.kismet.channels.graphtype');
415
416         if (state.storage.isSet('jquery.kismet.channels.channeltype'))
417             stored_channel = state.storage.get('jquery.kismet.channels.channeltype');
418
419         if (state.storage.isSet('jquery.kismet.channels.range'))
420             stored_range = state.storage.get('jquery.kismet.channels.range');
421
422
423         if (!state.storage.isSet('jquery.kismet.channels.filter'))
424             state.storage.set('jquery.kismet.channels.filter', 'any');
425
426         state.visible = state.element.is(":visible");
427
428         if (typeof(inopt) === "string") {
429
430         } else {
431             state.options = $.extend(base_options, inopt);
432         }
433
434         var banner = $('div.k_cd_banner', state.element);
435         if (banner.length == 0) {
436             banner = $('<div>', {
437                 id: "banner",
438                 class: "k_cd_banner"
439             });
440
441             state.element.append(banner);
442         }
443
444         if (state.graphtype == null) {
445             state.graphtype = $('<div>', {
446                 "id": "graphtype",
447                 "class": "k_cd_type",
448             })
449             .append(
450                 $('<label>', {
451                     for: "gt_bar",
452                 })
453                 .text("Current")
454                 .tooltipster({ content: 'Realtime devices-per-channel'})
455             )
456             .append($('<input>', {
457                 type: "radio",
458                 id: "gt_bar",
459                 name: "graphtype",
460                 value: "now",
461                 })
462             )
463             .append(
464                 $('<label>', {
465                     for: "gt_line",
466                 })
467                 .text("Historical")
468                 .tooltipster({ content: 'Historical RRD device records'})
469             )
470             .append($('<input>', {
471                 type: "radio",
472                 id: "gt_line",
473                 name: "graphtype",
474                 value: "history"
475             })
476             )
477             .append(
478                 $('<select>', {
479                     id: "historyrange",
480                     class: "k_cd_hr_list"
481                 })
482                 .append(
483                     $('<option>', {
484                         id: "hr_min",
485                         value: "min",
486                     })
487                     .text("Past Minute")
488                 )
489                 .append(
490                     $('<option>', {
491                         id: "hr_hour",
492                         value: "hour",
493                     })
494                     .text("Past Hour")
495                 )
496                 .append(
497                     $('<option>', {
498                         id: "hr_day",
499                         value: "day",
500                     })
501                     .text("Past Day")
502                 )
503                 .hide()
504             )
505             .append(
506                 $('<label>', {
507                     id: "gh_filter_label",
508                     for: "gh_filter",
509                 })
510                 .text("Filter")
511                 .append(
512                     $('<select>', {
513                         id: "gh_filter"
514                     })
515                     .append(
516                         $('<option>', {
517                             id: "any",
518                             value: "any",
519                             selected: "selected",
520                         })
521                         .text("Any")
522                     )
523                 )
524                 .hide()
525             );
526
527             // Select time range from stored value
528             $('option#hr_' + stored_range, state.graphtype).attr('selected', 'selected');
529
530             // Select graph type from stored value
531             if (stored_gtype == 'now') {
532                 $('input#gt_bar', state.graphtype).attr('checked', 'checked');
533             } else {
534                 $('input#gt_line', state.graphtype).attr('checked', 'checked');
535             }
536
537             banner.append(state.graphtype);
538
539             update_graphtype(state, stored_gtype);
540
541             state.graphtype.on('change', function() {
542                 update_graphtype(state);
543                 channeldisplay_refresh(state);
544             });
545
546             $('select#gh_filter', state.graphtype).on('change', function() {
547                 var filter_string = $('select#gh_filter option:selected', state.element).val();
548                 state.storage.set('jquery.kismet.channels.filter', filter_string);
549                 channeldisplay_refresh(state);
550             });
551
552         }
553
554         if (state.picker == null) {
555             state.picker = $('<div>', {
556                 id: "picker",
557                 class: "k_cd_picker",
558             });
559
560             var sel = $('<select>', {
561                 id: "k_cd_freq_selector",
562                 class: "k_cd_list",
563             });
564
565             state.picker.append(sel);
566
567             var chlist = new Array();
568             chlist.push("Frequency");
569
570             chlist = chlist.concat(kismet_ui.GetChannelListKeys());
571
572             for (var c in chlist) {
573                 var e = $('<option>', {
574                     value: chlist[c],
575                 }).html(chlist[c]);
576
577                 if (chlist[c] === stored_channel)
578                     e.attr("selected", "selected");
579
580                 sel.append(e);
581             }
582
583             banner.append(state.picker);
584
585             state.picker.on('change', function() {
586                 var freqtrans =
587                     $('select#k_cd_freq_selector option:selected', state.element).val();
588
589                 state.storage.set('jquery.kismet.channels.channeltype', freqtrans);
590
591                 channeldisplay_refresh(state);
592             });
593
594         }
595
596         if (state.devgraph_container == null) {
597             state.devgraph_container =
598                 $('<div>', {
599                     class: "k_cd_container"
600                 });
601
602             state.devgraph_canvas = $('<canvas>', {
603                 class: "k_cd_dg",
604             });
605
606             state.devgraph_container.append(state.devgraph_canvas);
607
608             state.timegraph_canvas = $('<canvas>', {
609                 class: "k_cd_dg",
610             });
611
612             state.timegraph_canvas.hide();
613             state.devgraph_container.append(state.timegraph_canvas);
614
615             state.element.append(state.devgraph_container);
616         }
617
618         // Add a 'coming soon' item
619         if (state.coming_soon == null)  {
620             state.coming_soon = $('<i>', {
621                 "id": "coming_soon",
622             });
623             state.coming_soon.html("Channel data loading...");
624             state.element.append(state.coming_soon);
625         }
626
627         // Hook an observer to see when we become visible
628         var observer = new MutationObserver(function(mutations) {
629             if (state.element.is(":hidden") && state.timerid >= 0) {
630                 state.visible = false;
631                 clearTimeout(state.timerid);
632             } else if (state.element.is(":visible") && !state.visible) {
633                 state.visible = true;
634                 channeldisplay_refresh(state);
635             }
636         });
637
638         observer.observe(state.element[0], {
639             attributes: true
640         });
641
642         if (state.visible) {
643             channeldisplay_refresh(state);
644         }
645     };
646
647 }(jQuery));
648