dark mode and websockets
[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                         stack: 'bar',
163                         label:  label,
164                         fill: true,
165                         lineTension: 0.1,
166                         data: linedata,
167                         borderColor: "hsl(" + color + ", 100%, 50%)",
168                         backgroundColor: "hsl(" + color + ", 100%, 50%)",
169                     };
170
171                     // Add it to the dataset if we're not filtering
172                     if (filter_string === fk ||
173                         filter_string === '' ||
174                         filter_string === 'any') {
175                             datasets.push(ds);
176                     }
177                 }
178
179                 state.devgraph_canvas.hide();
180                 state.timegraph_canvas.show();
181                 state.coming_soon.hide();
182
183                 if (state.timegraph_chart == null) {
184                     var device_options = {
185                         type: "bar",
186                         responsive: true,
187                         options: {
188                             maintainAspectRatio: false,
189                             scales: {
190                                 yAxes: [{
191                                     ticks: {
192                                         beginAtZero: true,
193                                     },
194                                     stacked: true,
195                                 }],
196                                 xAxes: [{
197                                     stacked: true,
198                                 }],
199                             },
200                             legend: {
201                                 labels: {
202                                     boxWidth: 15,
203                                     fontSize: 9,
204                                     padding: 5,
205                                 },
206                             },
207                         },
208                         data: {
209                             labels: pointtitles,
210                             datasets: datasets,
211                         }
212                     };
213
214                     state.timegraph_chart = new Chart(state.timegraph_canvas,
215                         device_options);
216                 } else {
217                     state.timegraph_chart.data.datasets = datasets;
218                     state.timegraph_chart.data.labels = pointtitles;
219
220                     state.timegraph_chart.update();
221                 }
222             } else {
223                 // 'now', but default - if for some reason we didn't get a
224                 // value from the selector, this falls through to the bar graph
225                 // which is what we probably really want
226                 for (var fk in data['kismet.channeltracker.frequency_map']) {
227                     var slot_now =
228                         (data['kismet.channeltracker.frequency_map'][fk]['kismet.channelrec.device_rrd']['kismet.common.rrd.last_time']) % 60;
229                     var dev_now = data['kismet.channeltracker.frequency_map'][fk]['kismet.channelrec.device_rrd']['kismet.common.rrd.minute_vec'][slot_now];
230
231                     var cfk = kismet_ui.GetConvertedChannel(freqtrans, fk);
232
233                     if (cfk == fk)
234                         devtitles.push(kismet.HumanReadableFrequency(parseInt(fk)));
235                     else
236                         devtitles.push(cfk);
237
238                     devnums.push(dev_now);
239                 }
240
241                 state.devgraph_canvas.show();
242                 state.timegraph_canvas.hide();
243
244                 if (state.devgraph_chart == null) {
245                     state.coming_soon.hide();
246
247                     var device_options = {
248                         type: "bar",
249                         options: {
250                             responsive: true,
251                             maintainAspectRatio: false,
252                             scales: {
253                                 yAxes: [{
254                                     ticks: {
255                                         beginAtZero: true,
256                                     }
257                                 }],
258                             },
259                         },
260                         data: {
261                             labels: devtitles,
262                             datasets: [
263                                 {
264                                     label: "Devices per Channel",
265                                     backgroundColor: kismet_theme.graphBasicColor,
266                                     data: devnums,
267                                     borderWidth: 1,
268                                 }
269                             ],
270                         }
271                     };
272
273                     state.devgraph_chart = new Chart(state.devgraph_canvas,
274                         device_options);
275
276                 } else {
277                     state.devgraph_chart.data.datasets[0].data = devnums;
278                     state.devgraph_chart.data.labels = devtitles;
279
280                     state.devgraph_chart.update();
281                 }
282             }
283
284         })
285         .always(function() {
286             state.timerid = setTimeout(function() { channeldisplay_refresh(state); }, 5000);
287         });
288     };
289
290     var update_graphtype = function(state, gt = null) {
291
292         if (gt == null)
293             gt = $("input[name='graphtype']:checked", state.graphtype).val();
294
295         state.storage.set('jquery.kismet.channels.graphtype', gt);
296
297         if (gt === 'now') {
298             $("select#historyrange", state.graphtype).hide();
299             $("select#gh_filter", state.graphtype).hide();
300             $("label#gh_filter_label", state.graphtype).hide();
301         } else {
302             $("select#historyrange", state.graphtype).show();
303             $("label#gh_filter_label", state.graphtype).show();
304             $("select#gh_filter", state.graphtype).show();
305         }
306
307         charttime = $('select#historyrange option:selected', state.element).val();
308         state.storage.set('jquery.kismet.channels.range', charttime);
309     }
310
311     var channels_resize = function(state) {
312         console.log('resize container ', state.devgraph_container.width(), state.devgraph_container.height());
313
314         if (state.devgraph_canvas != null)
315             state.devgraph_canvas
316                 .prop('width', state.devgraph_container.width())
317                 .prop('height', state.devgraph_container.height());
318
319         if (state.timegraph_canvas != null)
320             state.timegraph_canvas
321                 .prop('width', state.devgraph_container.width())
322                 .prop('height', state.devgraph_container.height());
323
324         if (state.devgraph_chart != null)
325             state.devgraph_chart.resize();
326
327         if (state.timegraph_chart != null)
328             state.timegraph_chart.resize();
329
330         channeldisplay_refresh(state);
331     }
332
333     $.fn.channels = function(inopt) {
334         var state = {
335             element: $(this),
336             options: base_options,
337             timerid: -1,
338             devgraph_container: null,
339             devgraph_chart: null,
340             timegraph_chart: null,
341             devgraph_canvas: null,
342             timegraph_canvas: null,
343             picker: null,
344             graphtype: null,
345             coming_soon: null,
346             visible: false,
347             storage: null,
348             resizer: null,
349             reset_size: null,
350         };
351
352         // Modeled on the datatables resize function
353         state.resizer = $('<iframe/>')
354             .css({
355                 position: 'absolute',
356                 top: 0,
357                 left: 0,
358                 height: '100%',
359                 width: '100%',
360                 zIndex: -1,
361                 border: 0
362             })
363         .attr('frameBorder', '0')
364         .attr('src', 'about:blank');
365
366         state.resizer[0].onload = function() {
367                         var body = this.contentDocument.body;
368                         var height = body.offsetHeight;
369             var width = body.offsetWidth;
370                         var contentDoc = this.contentDocument;
371                         var defaultView = contentDoc.defaultView || contentDoc.parentWindow;
372
373                         defaultView.onresize = function () {
374                                 var newHeight = body.clientHeight || body.offsetHeight;
375                                 var docClientHeight = contentDoc.documentElement.clientHeight;
376
377                                 var newWidth = body.clientWidth || body.offsetWidth;
378                                 var docClientWidth = contentDoc.documentElement.clientWidth;
379
380                                 if ( ! newHeight && docClientHeight ) {
381                                         newHeight = docClientHeight;
382                                 }
383
384                                 if ( ! newWidth && docClientWidth ) {
385                                         newWidth = docClientWidth;
386                                 }
387
388                                 if ( newHeight !== height || newWidth !== width ) {
389                                         height = newHeight;
390                                         width = newWidth;
391                     console.log("triggered resizer", height, width);
392                     channels_resize(state);
393                                 }
394                         };
395                 };
396
397         state.resizer
398             .appendTo(state.element)
399             .attr('data', 'about:blank');
400
401         state.element.on('resize', function() {
402             console.log("element resize");
403             channels_resize(state);
404         });
405
406         state.element.addClass(".channels");
407
408         state.storage = Storages.localStorage;
409
410         var stored_gtype = "now";
411         var stored_channel = "Frequency";
412         var stored_range = "min";
413
414         if (state.storage.isSet('jquery.kismet.channels.graphtype'))
415             stored_gtype = state.storage.get('jquery.kismet.channels.graphtype');
416
417         if (state.storage.isSet('jquery.kismet.channels.channeltype'))
418             stored_channel = state.storage.get('jquery.kismet.channels.channeltype');
419
420         if (state.storage.isSet('jquery.kismet.channels.range'))
421             stored_range = state.storage.get('jquery.kismet.channels.range');
422
423
424         if (!state.storage.isSet('jquery.kismet.channels.filter'))
425             state.storage.set('jquery.kismet.channels.filter', 'any');
426
427         state.visible = state.element.is(":visible");
428
429         if (typeof(inopt) === "string") {
430
431         } else {
432             state.options = $.extend(base_options, inopt);
433         }
434
435         var banner = $('div.k_cd_banner', state.element);
436         if (banner.length == 0) {
437             banner = $('<div>', {
438                 id: "banner",
439                 class: "k_cd_banner"
440             });
441
442             state.element.append(banner);
443         }
444
445         if (state.graphtype == null) {
446             state.graphtype = $('<div>', {
447                 "id": "graphtype",
448                 "class": "k_cd_type",
449             })
450             .append(
451                 $('<label>', {
452                     for: "gt_bar",
453                 })
454                 .text("Current")
455                 .tooltipster({ content: 'Realtime devices-per-channel'})
456             )
457             .append($('<input>', {
458                 type: "radio",
459                 id: "gt_bar",
460                 name: "graphtype",
461                 value: "now",
462                 })
463             )
464             .append(
465                 $('<label>', {
466                     for: "gt_line",
467                 })
468                 .text("Historical")
469                 .tooltipster({ content: 'Historical RRD device records'})
470             )
471             .append($('<input>', {
472                 type: "radio",
473                 id: "gt_line",
474                 name: "graphtype",
475                 value: "history"
476             })
477             )
478             .append(
479                 $('<select>', {
480                     id: "historyrange",
481                     class: "k_cd_hr_list"
482                 })
483                 .append(
484                     $('<option>', {
485                         id: "hr_min",
486                         value: "min",
487                     })
488                     .text("Past Minute")
489                 )
490                 .append(
491                     $('<option>', {
492                         id: "hr_hour",
493                         value: "hour",
494                     })
495                     .text("Past Hour")
496                 )
497                 .append(
498                     $('<option>', {
499                         id: "hr_day",
500                         value: "day",
501                     })
502                     .text("Past Day")
503                 )
504                 .hide()
505             )
506             .append(
507                 $('<label>', {
508                     id: "gh_filter_label",
509                     for: "gh_filter",
510                 })
511                 .text("Filter")
512                 .append(
513                     $('<select>', {
514                         id: "gh_filter"
515                     })
516                     .append(
517                         $('<option>', {
518                             id: "any",
519                             value: "any",
520                             selected: "selected",
521                         })
522                         .text("Any")
523                     )
524                 )
525                 .hide()
526             );
527
528             // Select time range from stored value
529             $('option#hr_' + stored_range, state.graphtype).attr('selected', 'selected');
530
531             // Select graph type from stored value
532             if (stored_gtype == 'now') {
533                 $('input#gt_bar', state.graphtype).attr('checked', 'checked');
534             } else {
535                 $('input#gt_line', state.graphtype).attr('checked', 'checked');
536             }
537
538             banner.append(state.graphtype);
539
540             update_graphtype(state, stored_gtype);
541
542             state.graphtype.on('change', function() {
543                 update_graphtype(state);
544                 channeldisplay_refresh(state);
545             });
546
547             $('select#gh_filter', state.graphtype).on('change', function() {
548                 var filter_string = $('select#gh_filter option:selected', state.element).val();
549                 state.storage.set('jquery.kismet.channels.filter', filter_string);
550                 channeldisplay_refresh(state);
551             });
552
553         }
554
555         if (state.picker == null) {
556             state.picker = $('<div>', {
557                 id: "picker",
558                 class: "k_cd_picker",
559             });
560
561             var sel = $('<select>', {
562                 id: "k_cd_freq_selector",
563                 class: "k_cd_list",
564             });
565
566             state.picker.append(sel);
567
568             var chlist = new Array();
569             chlist.push("Frequency");
570
571             chlist = chlist.concat(kismet_ui.GetChannelListKeys());
572
573             for (var c in chlist) {
574                 var e = $('<option>', {
575                     value: chlist[c],
576                 }).html(chlist[c]);
577
578                 if (chlist[c] === stored_channel)
579                     e.attr("selected", "selected");
580
581                 sel.append(e);
582             }
583
584             banner.append(state.picker);
585
586             state.picker.on('change', function() {
587                 var freqtrans =
588                     $('select#k_cd_freq_selector option:selected', state.element).val();
589
590                 state.storage.set('jquery.kismet.channels.channeltype', freqtrans);
591
592                 channeldisplay_refresh(state);
593             });
594
595         }
596
597         if (state.devgraph_container == null) {
598             state.devgraph_container =
599                 $('<div>', {
600                     class: "k_cd_container"
601                 });
602
603             state.devgraph_canvas = $('<canvas>', {
604                 class: "k_cd_dg",
605             });
606
607             state.devgraph_container.append(state.devgraph_canvas);
608
609             state.timegraph_canvas = $('<canvas>', {
610                 class: "k_cd_dg",
611             });
612
613             state.timegraph_canvas.hide();
614             state.devgraph_container.append(state.timegraph_canvas);
615
616             state.element.append(state.devgraph_container);
617         }
618
619         // Add a 'coming soon' item
620         if (state.coming_soon == null)  {
621             state.coming_soon = $('<i>', {
622                 "id": "coming_soon",
623             });
624             state.coming_soon.html("Channel data loading...");
625             state.element.append(state.coming_soon);
626         }
627
628         // Hook an observer to see when we become visible
629         var observer = new MutationObserver(function(mutations) {
630             if (state.element.is(":hidden") && state.timerid >= 0) {
631                 state.visible = false;
632                 clearTimeout(state.timerid);
633             } else if (state.element.is(":visible") && !state.visible) {
634                 state.visible = true;
635                 channeldisplay_refresh(state);
636             }
637         });
638
639         observer.observe(state.element[0], {
640             attributes: true
641         });
642
643         if (state.visible) {
644             channeldisplay_refresh(state);
645         }
646     };
647
648 }(jQuery));
649