Subversion Repositories ALCASAR

Rev

Details | Last modification | View Log

Rev Author Line No. Line
3241 rexy 1
var enable_graph = false,
2
    config,
3
    dygraph,
4
    dygraph_config,
5
    dygraph_data,
6
    dygraph_rangeselector_active,
7
    dygraph_daterange,
8
    dygraph_did_zoom,
9
    footable_data,
10
    date_range,
11
    date_range_interval,
12
    api_last_query,
13
    api_graph_options,
14
    api_flows_options,
15
    api_statistics_options,
16
    nfdump_translation = {ff: 'flow record flags in hex', ts: 'Start Time - first seen', te: 'End Time - last seen', tr: 'Time the flow was received by the collector', td: 'Duration', pr: 'Protocol', exp: 'Exporter ID', eng: 'Engine Type/ID', sa: 'Source Address', da: 'Destination Address', sap: 'Source Address:Port', dap: 'Destination Address:Port', sp: 'Source Port', dp: 'Destination Port', sn: 'Source Network (mask applied)', dn: 'Destination Network (mask applied)', nh: 'Next-hop IP Address', nhb: 'BGP Next-hop IP Address', ra: 'Router IP Address', sas: 'Source AS', das: 'Destination AS', nas: 'Next AS', pas: 'Previous AS', in: 'Input Interface num', out: 'Output Interface num', pkt: 'Packets - default input', ipkt: 'Input Packets', opkt: 'Output Packets', byt: 'Bytes - default input', ibyt: 'Input Bytes', obyt: 'Output Bytes', fl: 'Flows', flg: 'TCP Flags', tos: 'Tos - default src', stos: 'Src Tos', dtos: 'Dst Tos', dir: 'Direction: ingress, egress', smk: 'Src mask', dmk: 'Dst mask', fwd: 'Forwarding Status', svln: 'Src vlan label', dvln: 'Dst vlan label', ismc: 'Input Src Mac Addr', odmc: 'Output Dst Mac Addr', idmc: 'Input Dst Mac Addr', osmc: 'Output Src Mac Addr', pps: 'Packets per second', bps: 'Bytes per second', bpp: 'Bytes per packet', flP: 'Flows (%)', ipktP: 'Input Packets (%)', opktP: 'Output Packets (%)', ibytP: 'Input Bytes (%)', obytP: 'Output Bytes (%)', ipps: 'Input Packets/s', ibps: 'Input Bytes/s', ibpp: 'Input Bytes/Packet', pktP: 'Packets (%)', bytP: 'Bytes (%)'},
17
    views_view_status = {graphs: false, flows: false, statistics: false},
18
    ip_link_handler = (a) => {
19
        const ip = a.innerHTML;
20
        const ignoredFields = ['country_', 'timezone_', 'currency_'];
21
        const checkIp = async (ip) => {
22
            const ipWhoisResponse = await fetch('https://ipwhois.app/json/' + ip);
23
            const ipWhoisData = await ipWhoisResponse.json();
24
 
25
            const hostResponse = await fetch('../api/host/?ip=' + ip);
26
            const hostData = (!hostResponse.ok) ? 'IP could not be resolved' : await hostResponse.json();
27
 
28
            return {
29
                ipWhoisData: ipWhoisData,
30
                hostData: hostData
31
            }
32
        }
33
 
34
        const modal = new bootstrap.Modal('#modal', {});
35
        const modalTitle = document.querySelector('#modal .modal-title');
36
        const modalBody = document.querySelector('#modal .modal-body');
37
        const modalLoader = document.querySelector('#modal .modal-loader');
38
        modalBody.innerHTML = modalLoader.outerHTML;
39
        modalBody.querySelector('.modal-loader').classList.remove('d-none');
40
        modalTitle.innerHTML = 'Info for IP: ' + ip;
41
        modal.show();
42
 
43
        // make request and display data
44
        checkIp(ip).then((data) => {
45
            console.log(data);
46
 
47
            // create table
48
            let markup = '<table class="table table-striped">';
49
            for (const [key, value] of Object.entries(data.ipWhoisData)) {
50
                // if key starts with any of ignoredFields values, skip it
51
                if (ignoredFields.some(field => key.startsWith(field))) continue;
52
                markup += '<tr><th>' + key + '</th><td>' + value + '</td></tr>';
53
            }
54
            markup += '</table>';
55
 
56
            // add heading and flag
57
            let flag = data.ipWhoisData.country_flag ? '<img src="' + data.ipWhoisData.country_flag + '" alt="' + data.ipWhoisData.country + '" title="' + data.ipWhoisData.country + '" style="width: 3rem" />' : '';
58
            let heading = '<h3>' + ip + ' ' + flag + '</h3>';
59
            heading += '<h4>Host: ' + data.hostData + '</h4>';
60
 
61
            // replace loader with content
62
            modalBody.innerHTML = heading + markup;
63
        });
64
    };
65
 
66
$(document).ready(function() {
67
 
68
    /**
69
     * get config from backend
70
     * example data:
71
     *
72
     *  config object {
73
     *    "sources": ["gate", "swi6"],
74
     *    "ports": [ 80, 23, 22 ],
75
     *    "stored_output_formats": [],
76
     *    "stored_filters": [],
77
     *    "daemon_running": true,
78
     *  }
79
     */
80
    $.get('../api/config', function(data, status) {
81
        if (status === 'success') {
82
            config = data;
83
            init();
84
 
85
            if (config.daemon_running === true) {
86
 
87
                var reload_seconds = 60;
88
                if (typeof config.frontend.reload_interval !== 'undefined') reload_seconds = config.frontend.reload_interval;
89
 
90
                display_message('info', 'Daemon is running, graph is reloading each ' + ((reload_seconds === 60) ? 'minute' : reload_seconds + ' seconds') + '.');
91
 
92
                date_range_interval = setInterval(function() {
93
                    if (date_range.options.max === date_range.options.to) {
94
                        var now = new Date();
95
                        date_range.update({ max: now.getTime(), to: now.getTime() });
96
                    }
97
                }, reload_seconds*1000);
98
            }
99
        } else {
100
            display_message('danger', 'Error getting the config!')
101
        }
102
    });
103
 
104
    /**
105
     * general ajax error handler
106
     */
107
    $(document).on('ajaxError', function(e, jqXHR) {
108
        console.log(jqXHR);
109
        if (typeof jqXHR === 'undefined') {
110
            display_message('danger', 'General error, please file a ticket on github!');
111
        } else if (typeof jqXHR.responseJSON === 'undefined') {
112
            display_message('danger', 'General error: ' + jqXHR.responseText);
113
        } else {
114
            display_message('danger', 'Got ' + jqXHR.responseJSON.error);
115
        }
116
    });
117
 
118
    /**
119
     * navigation functionality
120
     * show/hides the correct containers, which are identified by the data-view attribute
121
     */
122
    $(document).on('click', 'header li a', function(e) {
123
        e.preventDefault();
124
        var view = $(this).attr('data-view');
125
        var $filter = $('#filter').find('[data-view]');
126
        var $content = $('#contentDiv').find('div.content');
127
 
128
        $('header li a').removeClass('active');
129
        $(this).addClass('active');
130
 
131
        var showDivs = function(id, el) {
132
            if ($(el).attr('data-view').indexOf(view) !== -1) $(el).removeClass('d-none');
133
            else $(el).addClass('d-none');
134
        };
135
 
136
        // show the right divs
137
        $filter.each(showDivs);
138
        $content.each(showDivs);
139
 
140
        // re-initialize form
141
        if (view === 'graphs') $('#filterDisplaySelect').trigger('change');
142
        if (view === 'flows') $('#statsFilterForSelection').val('record').trigger('change');
143
 
144
        // trigger resize for the graph
145
        if (typeof dygraph !== 'undefined') dygraph.resize();
146
 
147
        // set defaults for the view
148
        init_defaults(view);
149
 
150
        // set view state to true
151
        views_view_status[view] = true;
152
    });
153
 
154
    /**
155
     * home-button functionality
156
     * reloads the page
157
     */
158
    $(document).on('click', 'header .reload', function(e) {
159
        e.preventDefault();
160
        window.location.reload(true);
161
    });
162
 
163
    /**
164
     * date range slider
165
     * set next/previous time slot
166
     */
167
    $(document).on('click', '#date_slot_nav button', function() {
168
        var slot = parseInt($('#date_slot').find('input[name=range]:checked').val()),
169
            prev = $(this).hasClass('prev');
170
 
171
        // if the date_range was modified manually, get the difference
172
        if (isNaN(slot)) slot = date_range.options.to-date_range.options.from;
173
 
174
        date_range.update({
175
            from: prev === true ? date_range.options.from-slot : date_range.options.from+slot,
176
            to: prev === true ? date_range.options.to-slot : date_range.options.to+slot
177
        });
178
 
179
        // disable buttons if slot is too big or end is near
180
        check_daterange_boundaries(slot);
181
    });
182
 
183
    /**
184
     * date range slider
185
     * set predefined time range like day/week/month/year
186
     */
187
    $(document).on('change', '#date_slot input[name=range]', function() {
188
        var range = parseInt($(this).val());
189
 
190
        date_range.update({
191
            from: date_range.options.to - range,
192
            to: date_range.options.to // the current "to" value should stay
193
        });
194
 
195
        check_daterange_boundaries(range);
196
    });
197
 
198
    /**
199
     * sync button
200
     * gets the time range from the graph and updates the date range slider
201
     */
202
    $(document).on('click', '#date_syncing button.sync-date', function() {
203
        var from = dygraph_daterange[0].getTime(),
204
            to = dygraph_daterange[1].getTime();
205
 
206
        date_range.update({
207
            from: from,
208
            to: to
209
        });
210
 
211
        // remove active state of date slot button
212
        $('#date_slot').find('label.active').removeClass('active').find('input').prop('checked', false);
213
 
214
        check_daterange_boundaries(to-from);
215
    });
216
 
217
    /**
218
     * source filter
219
     * reload the graph when the source selection changes
220
     */
221
    $(document).on('change', '#filterSourcesSelect', updateGraph);
222
 
223
    /**
224
     * displays the right filter
225
     */
226
    $(document).on('change', '#filterDisplaySelect', function() {
227
        var display = $(this).val(), displayId;
228
        var $filters = $('#filter').find('[data-display]').addClass('d-none');
229
 
230
        // show only wanted filters
231
        $filters.filter('[data-display*=' + display + ']').removeClass('d-none');
232
 
233
        switch (display) {
234
            case 'sources':
235
                displayId = '#filterSources';
236
                displaySourcesHelper();
237
                break;
238
            case 'protocols':
239
                displayId = '#filterProtocols';
240
                displayProtocolsHelper();
241
                break;
242
            case 'ports':
243
                displayId = '#filterPorts';
244
                displayPortsHelper();
245
                break;
246
        }
247
 
248
        // initialize tooltips
249
        const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
250
        const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
251
 
252
        // try to update graph
253
        updateGraph();
254
    });
255
 
256
    /**
257
     * protocols filter
258
     * reload the graph when the protocol selection changes
259
     */
260
    $(document).on('change', '#filterProtocols input', function() {
261
        var $filter = $('#filterProtocols');
262
        if ($(this).val() === 'any') {
263
            // uncheck all other input elements
264
            $(this).parent().addClass('active');
265
            $filter.find('input[value!="any"]').each(function () {
266
                $(this).prop('checked', false).parent().removeClass('active');
267
            });
268
        } else {
269
            // uncheck 'any' input element
270
            $filter.find('input[value="any"]').prop('checked', false).parent().removeClass('active');
271
        }
272
 
273
        // prevent having none checked - select 'any' as fallback
274
        if ($filter.find('input:checked').length === 0) {
275
            $filter.find('input[value="any"]').prop('checked', true).parent().addClass('active');
276
        }
277
        updateGraph();
278
    });
279
 
280
    /**
281
     * datatype filter (flows/packets/traffic)
282
     * reload the graph... you get it by now
283
     */
284
    $(document).on('change', '#filterTypes input', updateGraph);
285
    $(document).on('change', '#trafficUnit input', updateGraph);
286
    $(document).on('change', '#filterPortsSelect', updateGraph);
287
 
288
    /**
289
     * show/hide series in the dygraph
290
     * todo: check if this is needed at all, as it's the same like in the filter
291
     */
292
    $(document).on('change', '#series input', function(e) {
293
        var $checkbox = $(e.target);
294
        dygraph.setVisibility($checkbox.parent().index(), $($checkbox).is(':checked'));
295
    });
296
 
297
    /**
298
     * set graph display to curve or step plot
299
     */
300
    $(document).on('change', '#graph_lineplot input', function() {
301
        dygraph.updateOptions({
302
            stepPlot: ($(this).val() === 'step')
303
        });
304
    })
305
 
306
    /**
307
     * set graph display to lines or stacked
308
     */
309
    $(document).on('change', '#graph_linestacked input', function() {
310
        var stacked = ($(this).val() === 'stacked');
311
 
312
        dygraph.updateOptions({
313
            stackedGraph: ($(this).val() === 'stacked'),
314
            fillGraph: ($(this).val() !== 'line')
315
        });
316
    });
317
 
318
    /**
319
     * scale graph display linear or logarithmic
320
     */
321
    $(document).on('change', '#graph_linlog input', function() {
322
        var linear = ($(this).val() === 'linear');
323
 
324
        dygraph.updateOptions({
325
            logscale : !linear
326
        });
327
    });
328
 
329
    /**
330
     * disable aggregation fields if statistics "for" field is not "flow records"
331
     */
332
    $(document).on('change', '#statsFilterForSelection', function() {
333
        var disabled = ($(this).val() !== 'record');
334
 
335
        $('#filterFlowAggregation').find('label, input, select, button').each(function() {
336
            $(this).prop('disabled', disabled).toggleClass('disabled', disabled);
337
        });
338
 
339
        $('#filterOutputSelection').prop('disabled', disabled).toggleClass('disabled', disabled);
340
    });
341
 
342
    var setButtonLoading = function($button, setTo = true) {
343
        $button.toggleClass('disabled', setTo);
344
        if (setTo === false) {
345
            if ($button.data('old-text') !== undefined) {
346
                $button.html($button.data('old-text'));
347
                $button.data('old-text', undefined);
348
            }
349
        } else {
350
            $button.data('old-text', $button.html());
351
            $button.html('<span class="spinner-border spinner-border-sm" aria-hidden="true"></span><span role="status">&nbsp;Loading&#133;</span>');
352
        }
353
    }
354
 
355
    /**
356
     * Process flows/statistics form submission
357
     */
358
    $(document).on('click', '#filterCommands .submit', function () {
359
        var current_view = $('.nav-link.active').attr('data-view'),
360
            do_continue = true,
361
            date_diff = date_range.options.to-date_range.options.from,
362
            count_sources = $('#filterSourcesSelect').val().length,
363
            count_days = Math.round(Number(date_diff/1000/24/60/60));
364
 
365
        // warn user of long-running query
366
        if (count_days > 7 && date_diff*count_sources > 1000*24*60*60*12) {
367
            var calc_info = count_days + ' days and ' + count_sources + ' sources';
368
            do_continue = confirm('Be aware that nfdump will scan 288 capture files per day and source. You selected ' + calc_info + '. This might take a long time and lots of server resources. Are you sure you want to submit this query?');
369
        }
370
 
371
        if (do_continue === false) return false;
372
        if (current_view === 'statistics') submit_statistics();
373
        if (current_view === 'flows') submit_flows();
374
 
375
        // remove success errors
376
        $('#error').find('div.alert-success').fadeOut(1500, function () {
377
            $(this).remove();
378
        });
379
 
380
        // set button to loading state
381
        setButtonLoading($(this));
382
    });
383
 
384
    /**
385
     * Get a CSV of the currently selected data
386
     */
387
    $(document).on('click', '#filterCommands .csv', function() {
388
        $('#filterCommands .submit:visible').trigger('click');
389
        window.open(api_last_query + '&csv', '_blank');
390
    });
391
 
392
    /**
393
     * Reset flows/statistics form
394
     */
395
    $(document).on('click', '#filterCommands .reset', function() {
396
        var view = $('header').find('li.active a').attr('data-view'),
397
            $filter = $('#filterContainer');
398
 
399
        $filter.find('form').eq(0).trigger('reset');
400
        $filter.find('input:visible, textarea:visible, select:visible, button:visible').trigger('change');
401
 
402
    });
403
 
404
    /**
405
     * initialize the frontend
406
     * - set the select-list of sources
407
     * - initialize the range slider
408
     * - load the graph
409
     * - select default view if set in the config
410
     */
411
    function init() {
412
        // set version
413
        $('#version').html(config.version);
414
 
415
	var stored_filters = config['stored_filters'];
416
	var local_filters = window.localStorage.getItem('stored_filters');
417
	stored_filters = stored_filters.concat(JSON.parse( local_filters ));
418
	stored_filters = Array.from(new Set(stored_filters));
419
	window.localStorage.setItem('stored_filters', JSON.stringify(stored_filters) )
420
 
421
        // load values for form
422
        updateDropdown('sources', config['sources']);
423
        updateDropdown('ports', config['ports']);
424
        updateDropdown('filters', stored_filters);
425
 
426
        init_rangeslider();
427
 
428
        // load default view
429
        if (typeof config.frontend.defaults !== 'undefined') {
430
            $('header li a[data-view="' + config.frontend.defaults.view + '"]').trigger('click');
431
        }
432
 
433
        enable_graph = true;
434
        // show graph for one year by default
435
        $('#date_slot').find('[data-unit="y"]').trigger('click');
436
    }
437
 
438
    /**
439
     * sets default values for the view (graphs, flows, statistics)
440
     * hides unneeded controls if e.g. only one source or one port is defined
441
     * @param view
442
     */
443
    function init_defaults(view) {
444
        var defaults = {graphs: {}, flows: {}, statistics: {}};
445
        if (typeof config.frontend.defaults !== 'undefined') {
446
            defaults = config.frontend.defaults;
447
        }
448
 
449
        // graphs defaults
450
        if (view === 'graphs' && views_view_status.graphs === false) {
451
            // graphs: set default display (sources, protocols, ports)
452
            if (typeof defaults.graphs.display !== 'undefined') {
453
                $('#filterDisplaySelect').val(defaults.graphs.display).trigger('change');
454
            } else {
455
                $('#filterDisplaySelect').trigger('change');
456
            }
457
 
458
            // graphs: set default datatype
459
            if (typeof defaults.graphs.datatype !== 'undefined') {
460
                $('#filterTypes input[value="' + defaults.graphs.datatype + '"]').trigger('click');
461
            }
462
 
463
            // graphs: set default protocols
464
            if (typeof defaults.graphs.protocols !== 'undefined') {
465
                // multiple possible if on protocols display
466
                if (defaults.graphs.display === 'protocols') {
467
                    $('#filterProtocolButtons input[value="any"]').trigger('click');
468
                    $.each(defaults.graphs.protocols, function (i, proto) {
469
                        $('#filterProtocolButtons input[value="' + proto + '"]').trigger('click');
470
                    });
471
                } else {
472
                    $('#filterProtocolButtons input[value="' + defaults.graphs.protocols[0] + '"]').trigger('click');
473
                }
474
            }
475
 
476
            // graphs: hide unneeded controls
477
            if (config['sources'].length === 1) { // only one source defined
478
                $('#filterDisplaySelect option[value="sources"]').remove();
479
                $('#filterSources').hide();
480
            }
481
 
482
            if (config['ports'].length === 0) { // only one port defined
483
                $('#filterDisplaySelect option[value="ports"]').remove();
484
            }
485
 
486
            if ($('#filterDisplaySelect option').length === 1) { // only one display option left
487
                $('#filterDisplay').hide();
488
            }
489
        }
490
 
491
        // flows defaults
492
        if (view === 'flows' && views_view_status.flows === false) {
493
 
494
            // flows: limit
495
            if (typeof defaults.flows.limit !== 'undefined') {
496
                $('#flowsFilterLimitSelection').val(defaults.flows.limit);
497
            }
498
        }
499
 
500
        // statistics defaults
501
        if (view === 'statistics' && views_view_status.statistics === false) {
502
 
503
            // statistics: order by
504
            if (typeof defaults.statistics.orderby !== 'undefined') {
505
                $('#statsFilterOrderBySelection').val(defaults.statistics.orderby);
506
            }
507
        }
508
 
509
    }
510
 
511
    /**
512
     * initialize the range slider
513
     */
514
    function init_rangeslider() {
515
        // set default date range
516
        var to = new Date();
517
        var from = new Date(config.frontend.data_start * 1000 || to.getTime() - 1000*60*60*24*365*3);
518
        dygraph_daterange = [from, to];
519
 
520
        // initialize date range slider
521
        $('#date_range').ionRangeSlider({
522
            type: 'double',
523
            grid: true,
524
            min: dygraph_daterange[0].getTime(),
525
            max: dygraph_daterange[1].getTime(),
526
            force_edges: true,
527
            drag_interval: true,
528
            prettify: function(ut) {
529
                var date = new Date(ut);
530
                return date.toDateString();
531
            },
532
            onChange: function(data) {
533
                // remove active state of date slot button
534
                $('#date_slot').find('label.active').removeClass('active').find('input').prop('checked', false);
535
            },
536
            onFinish: function(data) {
537
                dygraph_daterange = [new Date(data.from), new Date(data.to)];
538
                date_range.update({ from: data.from, to: data.to });
539
                check_daterange_boundaries(data.to-data.from);
540
 
541
                // deactivate syncing button
542
                $('#date_syncing').find('button.sync-date').prop('disabled', true);
543
 
544
                updateGraph();
545
            },
546
            onUpdate: function(data) {
547
                dygraph_daterange = [new Date(data.from), new Date(data.to)];
548
 
549
                // deactivate syncing button
550
                $('#date_syncing').find('button.sync-date').prop('disabled', true);
551
 
552
                updateGraph();
553
            }
554
        });
555
        date_range = $('#date_range').data('ionRangeSlider');
556
    }
557
 
558
    /**
559
     * initialize two dygraph mods
560
     * they are needed to dynamically load more detailed data as the user zooms in or pans around
561
     * heavily influenced by https://github.com/kaliatech/dygraphs-dynamiczooming-example
562
     */
563
    function init_dygraph_mods() {
564
        dygraph_rangeselector_active = false;
565
        var $rangeEl = $('#flowDiv').find('.dygraph-rangesel-fgcanvas, .dygraph-rangesel-zoomhandle');
566
 
567
        // uninstall existing handler if already installed
568
        $rangeEl.off('mousedown.dygraph touchstart.dygraph');
569
 
570
        // install new mouse down handler
571
        $rangeEl.on('mousedown.dygraph touchstart.dygraph', function () {
572
 
573
            // track that mouse is down on range selector
574
            dygraph_rangeselector_active = true;
575
 
576
            // setup mouse up handler to initiate new data load
577
            $(window).off('mouseup.dygraph touchend.dygraph'); //cancel any existing
578
            $(window).on('mouseup.dygraph touchend.dygraph', function () {
579
                $(window).off('mouseup.dygraph touchend.dygraph');
580
 
581
                // mouse no longer down on range selector
582
                dygraph_rangeselector_active = false;
583
 
584
                // get the new detail window extents
585
                var range = dygraph.xAxisRange();
586
                dygraph_daterange = [new Date(range[0]), new Date(range[1])];
587
                dygraph_did_zoom = true;
588
 
589
                // activate syncing button
590
                $('#date_syncing').find('button.sync-date').prop('disabled', false);
591
 
592
                // update graph
593
                updateGraph();
594
            });
595
        });
596
 
597
        // save original endPan function
598
        var origEndPan = Dygraph.defaultInteractionModel.endPan;
599
 
600
        // replace built-in handling with our own function
601
        Dygraph.defaultInteractionModel.endPan = function (event, g, context) {
602
 
603
            // call the original to let it do it's magic
604
            origEndPan(event, g, context);
605
 
606
            // extract new start/end from the x-axis
607
            var range = g.xAxisRange();
608
            dygraph_daterange = [new Date(range[0]), new Date(range[1])];
609
            dygraph_did_zoom = true;
610
            updateGraph();
611
        };
612
        Dygraph.endPan = Dygraph.defaultInteractionModel.endPan; // see dygraph-interaction-model.js
613
    }
614
 
615
    /**
616
     * zoom callback for dygraph
617
     * updates the graph when the rangeselector is not active
618
     * @param minDate
619
     * @param maxDate
620
     */
621
    function dygraph_zoom(minDate, maxDate) {
622
        dygraph_daterange = [new Date(minDate), new Date(maxDate)];
623
 
624
        //When zoom reset via double-click, there is no mouse-up event in chrome (maybe a bug?),
625
        //so we initiate data load directly
626
        if (dygraph.isZoomed('x') === false) {
627
            dygraph_did_zoom = true;
628
            $(window).off('mouseup touchend'); //Cancel current event handler if any
629
            updateGraph();
630
            return;
631
        }
632
 
633
        //The zoom callback is called when zooming via mouse drag on graph area, as well as when
634
        //dragging the range selector bars. We only want to initiate dataload when mouse-drag zooming. The mouse
635
        //up handler takes care of loading data when dragging range selector bars.
636
        if (!dygraph_rangeselector_active) {
637
            dygraph_did_zoom = true;
638
            updateGraph();
639
        }
640
 
641
    }
642
 
643
    /**
644
     *
645
     * @param e The event object for the click
646
     * @param x The x value that was clicked (for dates, this is milliseconds since epoch)
647
     * @param points The closest points along that date
648
     */
649
    function dygraph_click(e, x, points) {
650
        if (confirm('Zoom in to this data point?')) {
651
            date_range.update({
652
                from: x,
653
                to: x+300000
654
            });
655
 
656
            // remove active state of date slot button
657
            $('#date_slot').find('label.active').removeClass('active').find('input').prop('checked', false);
658
 
659
            check_daterange_boundaries((x+300)-x);
660
        }
661
    }
662
 
663
    /**
664
     * reads options from api_graph_options, performs a request on the API
665
     * and tries to display the received data in the dygraph.
666
     */
667
    function updateGraph() {
668
        if (enable_graph === false) return false;
669
        var sources = $('#filterSourcesSelect').val(),
670
            type = $('#filterTypes input:checked').val(),
671
            ports = $('#filterPortsSelect').val(),
672
            protocols = $('#filterProtocols').find('input:checked').map(function() { return $(this).val(); }).get(),
673
            display = $('#filterDisplaySelect').val(),
674
            title = type + ' for ';
675
 
676
        // check if options valid to request new dygraph
677
        if (typeof sources === 'string') sources = [sources];
678
        if (sources.length === 0) {
679
            if (display === 'ports')
680
                sources = ['any'];
681
            else return;
682
        }
683
        if ($('#flowDiv:visible').length === 0) return;
684
        if (ports.length === 0) ports = [0];
685
        if (type === 'traffic') type = $('#trafficUnit input:checked').val();
686
 
687
        // set options
688
        api_graph_options = {
689
            datestart: parseInt(dygraph_daterange[0].getTime()/1000),
690
            dateend: parseInt(dygraph_daterange[1].getTime()/1000),
691
            type: type,
692
            protocols: protocols.length > 0 ? protocols : ['any'],
693
            sources: sources,
694
            ports: ports,
695
            display: display
696
        };
697
 
698
        // set title
699
        var elements = eval(display);
700
        var cat = elements.length > 1 ? display : display.substr(0, display.length-1); // plural
701
        // if more than 4, only show number of sources instead of names
702
        if (elements.length > 4) title += elements.length + ' ' + cat;
703
        else title += cat + ' ' + elements.join(', ');
704
 
705
 
706
        // make actual request
707
        $.get('../api/graph', api_graph_options, function (data, status) {
708
            if (status !== 'success') {
709
                display_message('warning', 'There somehow was a problem getting data, please check your form values.');
710
                return false;
711
            }
712
 
713
            if (data.data.length === 0) {
714
                return false;
715
            }
716
 
717
            var labels = ['Date'], index_to_insert = false;
718
 
719
            // iterate over labels
720
            $('#series').empty();
721
            $.each(data.legend, function (id, legend) {
722
                labels.push(legend);
723
 
724
                $('#series').append('<label><input type="checkbox" checked> ' + legend + '</label>');
725
            });
726
 
727
            // transform data to something Dygraph understands
728
            if (dygraph_did_zoom !== true) {
729
                // reset dygraph data to get a fresh load
730
                dygraph_data = [];
731
            } else {
732
                // delete values to replace
733
                for (var i = 0; i < dygraph_data.length; i++) {
734
                    if (dygraph_data[i][0].getTime() >= dygraph_daterange[0].getTime() && dygraph_data[i][0].getTime() <= dygraph_daterange[1].getTime()) {
735
                        // set start index for the new values
736
                        if (index_to_insert === false) index_to_insert = i;
737
 
738
                        // delete current element from array
739
                        dygraph_data.splice(i, 1);
740
 
741
                        // decrease current index, as all array elements moved left on deletion
742
                        i--;
743
                    }
744
                }
745
            }
746
 
747
            // Calculate the difference between the server and local timezone offsets
748
            var serverTimezoneOffset = config.tz_offset * 60 * 60;
749
            var localTimezoneOffset = new Date().getTimezoneOffset() * -60;
750
            var timezoneOffset = serverTimezoneOffset - localTimezoneOffset;
751
 
752
            // iterate over API result
753
            $.each(data.data, function (datetime, series) {
754
                var position = [new Date((parseInt(datetime) + timezoneOffset) * 1000)] ;
755
 
756
                // add all serie values to position array
757
                $.each(series, function (y, val) {
758
                    position.push(val);
759
                });
760
 
761
                // push position array to dygraph data
762
                if (dygraph_did_zoom !== true) {
763
                    dygraph_data.push(position);
764
                } else {
765
                    // when zoomed in, insert position array at the start index of replacement data
766
                    dygraph_data.splice(index_to_insert, 0, position);
767
                    index_to_insert++; // increase index, or data will get inserted backwards
768
                }
769
            });
770
 
771
            if (typeof dygraph === 'undefined') {
772
                // initial dygraph config:
773
                dygraph_config = {
774
                    title: title,
775
                    labels: labels,
776
                    ylabel: type.toUpperCase() + '/s',
777
                    xlabel: 'TIME',
778
                    labelsKMB: type === 'flows' || type === 'packets',
779
                    labelsKMG2: type === 'bits' || type === 'bytes', // only show KMG for traffic, not for packets or flows
780
                    labelsDiv: $('#legend')[0],
781
                    labelsSeparateLines: true,
782
                    legend: 'always',
783
                    stepPlot: true,
784
                    showRangeSelector: true,
785
                    dateWindow: [dygraph_data[0][0], dygraph_data[dygraph_data.length - 1][0]],
786
                    zoomCallback: dygraph_zoom,
787
                    clickCallback: dygraph_click,
788
                    highlightSeriesOpts: {
789
                        strokeWidth: 2,
790
                        strokeBorderWidth: 1,
791
                        highlightCircleSize: 5
792
                    },
793
                    rangeSelectorPlotStrokeColor: '#888888',
794
                    rangeSelectorPlotFillColor: '#cccccc',
795
                    stackedGraph: true,
796
                    fillGraph: true,
797
                };
798
                dygraph = new Dygraph($('#flowDiv')[0], dygraph_data, dygraph_config);
799
                init_dygraph_mods();
800
 
801
            } else {
802
                // update dygraph config
803
                dygraph_config = {
804
                    // series: series,
805
                    // axes: axes,
806
                    ylabel: type.toUpperCase() + '/s',
807
                    labelsKMB: type === 'flows' || type === 'packets',
808
                    labelsKMG2: type === 'bits' || type === 'bytes', // only show KMG for traffic, not for packets or flows
809
                    title: title,
810
                    labels: labels,
811
                    file: dygraph_data,
812
                };
813
 
814
                if (dygraph_did_zoom === true) {
815
                    dygraph_config.dateWindow = dygraph_daterange;
816
                } else {
817
                    // reset date window if we want to show entirely new data
818
                    dygraph_config.dateWindow = null;
819
                }
820
 
821
                dygraph.updateOptions(dygraph_config);
822
            }
823
            dygraph_did_zoom = false;
824
        });
825
    }
826
 
827
    /**
828
     * Display a message in the frontend
829
     * @param severity (success, info, warning, danger)
830
     * @param message
831
     */
832
    function display_message(severity, message) {
833
        var current_view = $('header').find('li.active a').attr('data-view'),
834
            $error = $('#error'),
835
            $buttons = $('button.submit'),
836
            icon;
837
 
838
        switch (severity) {
839
            case 'success': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16">\n' +
840
                '  <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>\n' +
841
                '</svg>&nbsp;'; break;
842
            case 'info': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16">\n' +
843
                '  <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2"/>\n' +
844
                '</svg>&nbsp;'; break;
845
            case 'warning': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle" viewBox="0 0 16 16">\n' +
846
                '  <path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z"/>\n' +
847
                '  <path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>\n' +
848
                '</svg>&nbsp;'; break;
849
            case 'danger': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16">\n' +
850
                '  <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>\n' +
851
                '</svg>&nbsp;'; break;
852
        }
853
 
854
        // create new error element
855
        $error.append('<div class="alert alert-dismissible mt-2" role="alert"><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>');
856
 
857
        // fill
858
        $error.find('div.alert').last().addClass('alert-' + severity).prepend(icon + message);
859
 
860
        // set default text to buttons, if needed
861
        $buttons.each(function() {
862
           setButtonLoading($(this), false);
863
        });
864
 
865
        // empty data table
866
        $('#contentDiv').find('table.table').empty();
867
    }
868
 
869
    /**
870
     * checks if with supplied date range, navigation is still possible (e.g. plus 1 month)
871
     * and disables navigation buttons if not
872
     * @param range date difference in milliseconds
873
     */
874
    function check_daterange_boundaries(range) {
875
        var $buttons = $('#date_slot_nav').find('button');
876
 
877
        // reset next/prev buttons (depending on selected range)
878
        $buttons.filter('.next').prop('disabled', (date_range.options.to + range > date_range.options.max));
879
        $buttons.filter('.prev').prop('disabled', (date_range.options.from - range < date_range.options.min));
880
    }
881
 
882
    /**
883
     * Process flows form submission
884
     */
885
    function submit_flows() {
886
        var sources = $('#filterSourcesSelect').val(),
887
            datestart = parseInt(dygraph_daterange[0].getTime()/1000),
888
            dateend = parseInt(dygraph_daterange[1].getTime()/1000),
889
            filter = '' + $('#filterNfdumpTextarea').val(),
890
            limit = $('#flowsFilterLimitSelection').val(),
891
            sort = '',
892
            output = {
893
                format: $('#filterOutputSelection').val(),
894
                custom: $('#customListOutputFormatValue').val(),
895
            };
896
 
897
        // parse form values to generate a proper API request
898
        var aggregate = parse_aggregation_fields();
899
 
900
        if (typeof sources === 'string') sources = [sources];
901
 
902
        if ($('#flowsFilterOther').find('[name=ordertstart]:checked').length > 0) {
903
            sort = $('[name=ordertstart]:checked').val();
904
        }
905
 
906
        api_flows_options = {
907
            datestart: datestart,
908
            dateend: dateend,
909
            sources: sources,
910
            filter: filter,
911
            limit: limit,
912
            aggregate: aggregate,
913
            sort: sort,
914
            output: output
915
        };
916
 
917
        api_last_query = '../api/flows/?' + $.param( api_flows_options );
918
        var req = $.get('../api/flows', api_flows_options, render_table);
919
    }
920
 
921
 
922
    /**
923
     * Process statistics form submission
924
     */
925
    function submit_statistics() {
926
        var sources = $('#filterSourcesSelect').val(),
927
            datestart = parseInt(dygraph_daterange[0].getTime() / 1000),
928
            dateend = parseInt(dygraph_daterange[1].getTime() / 1000),
929
            filter = '' + $('#filterNfdumpTextarea').val(),
930
            top = $('#statsFilterTopSelection').val(),
931
            s_for = $('#statsFilterForSelection').val(),
932
            title = $('#statsFilterForSelection :selected').text(),
933
            sort = $('#statsFilterOrderBySelection').val(),
934
            fmt = $('#filterOutputSelection'),
935
            output = {};
936
 
937
        if (!fmt.prop('disabled')) {
938
            output.format = fmt.val();
939
            output.custom = $('#customListOutputFormatValue').val();
940
        }
941
 
942
        if (typeof sources === 'string') sources = [sources];
943
 
944
        api_statistics_options = {
945
            datestart: datestart,
946
            dateend: dateend,
947
            sources: sources,
948
            filter: filter,
949
            top: top,
950
            for: s_for + '/' + sort,
951
            title: title,
952
            limit: '',
953
            output: output
954
        };
955
 
956
        api_last_query = '../api/stats/?' + $.param( api_statistics_options );
957
        var req = $.get('../api/stats', api_statistics_options, render_table);
958
    }
959
 
960
    /**
961
     * Parse aggregation fields and return something meaningful, e.g. proto,srcip/24
962
     * @returns string
963
     */
964
    function parse_aggregation_fields() {
965
        var $aggregation = $('#filterFlowAggregation');
966
        if ($aggregation.find('[name=bidirectional]:checked').length === 0) {
967
            var validAggregations = ['proto', 'dstport', 'srcport', 'srcip', 'dstip'],
968
                aggregate = '';
969
 
970
            $.each(validAggregations, function(id, val) {
971
                if ($aggregation.find('[name=' + val + ']:checked').length > 0) {
972
                    aggregate += (aggregate === '') ? val : ',' + val;
973
                } else {
974
                    var select = $aggregation.find('[name=' + val + ']').val();
975
                    if (select === 'none') return;
976
                    if (val === 'srcip') {
977
                        var prefix = parseInt($aggregation.find('[name=srcipprefix]:visible').val()),
978
                            srcprefix = (isNaN(prefix) || prefix === 'srcip') ? '' : '/' + prefix,
979
                            srcip = select + srcprefix;
980
                        aggregate += (aggregate === '') ? srcip : ',' + srcip;
981
                    } else if (val === 'dstip') {
982
                        var prefix = parseInt($aggregation.find('[name=dstipprefix]:visible').val()),
983
                            dstprefix = (isNaN(prefix) || prefix === 'dstip') ? '' : '/' + prefix,
984
                            dstip = select + dstprefix;
985
                        aggregate += (aggregate === '') ? dstip : ',' + dstip;
986
                    }
987
                }
988
            });
989
 
990
            return aggregate;
991
 
992
        } else return 'bidirectional';
993
    }
994
 
995
    /**
996
     * @see https://stackoverflow.com/a/2901298/710921
997
     * @param {number} x
998
     * @returns {string}
999
     */
1000
    function numberWithCommas(x) {
1001
        var parts = x.toString().split(".");
1002
        parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1003
        return parts.join(".");
1004
    }
1005
 
1006
    /**
1007
     * parses the provided data, converts it into a better suitable format and populates a html table
1008
     * @param data
1009
     * @param status
1010
     * @returns boolean
1011
     */
1012
    function render_table(data, status) {
1013
        if (status === 'success') {
1014
            footable_data = data;
1015
 
1016
            // print nfdump command
1017
            if (typeof data[0] === 'string') {
1018
                display_message('success', '<b>nfdump command:</b> ' + data[0].toString())
1019
            }
1020
 
1021
            // return if invalid data got returned
1022
            if (typeof data[1] !== 'object') {
1023
                display_message('warning', '<b>something went wrong.</b> ' + data[1].toString());
1024
                return false;
1025
            }
1026
 
1027
            // generate table header
1028
            var tempcolumns = data[1],
1029
                columns = [];
1030
 
1031
            // generate column definitions
1032
            $.each(tempcolumns, function (i, val) {
1033
                // todo optimize breakpoints
1034
                var title = (val === 'val') ? api_statistics_options.title : nfdump_translation[val],
1035
                    column = {
1036
                        name: val,
1037
                        title: title,
1038
                        type: 'text',
1039
                        breakpoints: 'xs sm',
1040
                    };
1041
 
1042
                // add formatter for ip addresses
1043
                if (['sa', 'da'].indexOf(val) !== -1 || val.match(/ip$/i) || title && title.match(/IP address$/)) {
1044
                    column['formatter'] = (ip) => "<a href='#' onclick='return ip_link_handler(this)'>" + ip + "</a>";
1045
                }
1046
 
1047
                // todo add date formatter for timestamps?
1048
                if (['ts', 'te', 'tr'].indexOf(val) !== -1) {
1049
                    column['breakpoints'] = '';
1050
                    column['type'] = 'text'; // 'date' needs moment.js library...
1051
                }
1052
 
1053
                // add formatter for bytes
1054
                if (['ibyt', 'obyt', 'bpp', 'bps', 'byt', 'ibps', 'obps', 'ibpp', 'obpp'].indexOf(val) !== -1) {
1055
                    column['type'] = 'number';
1056
                    column['formatter'] = (x) => filesize(x, {
1057
                        base: 10, // todo make configurable
1058
                    });
1059
                }
1060
 
1061
                // add formatter for big numbers
1062
                if (['td', 'fl', 'pkt', 'ipkt', 'opkt', 'ipps', 'opps'].indexOf(val) !== -1) {
1063
                    column['type'] = 'number';
1064
                    column['formatter'] = numberWithCommas
1065
                }
1066
 
1067
                // define rest of numbers
1068
                if (['sp', 'dp', 'flP', 'ipktP', 'opktP', 'ibytP', 'obytP', 'pktP', 'bytP'].indexOf(val) !== -1) {
1069
                    column['type'] = 'number';
1070
                }
1071
 
1072
                // ip addresses, protocol, value should not be hidden on small screens
1073
                if (['sa', 'da', 'pr', 'val'].indexOf(val) !== -1) {
1074
                    column['breakpoints'] = '';
1075
                }
1076
 
1077
                // least important columns should be hidden on small screens
1078
                if (['flg', 'fwd', 'in', 'out', 'sas', 'das'].indexOf(val) !== -1) {
1079
                    column['breakpoints'] = 'all';
1080
                    column['type'] = 'text';
1081
                }
1082
 
1083
                // add column to columns array
1084
                columns.push(column);
1085
            });
1086
 
1087
            // generate table data
1088
            var temprows = data.slice(2),
1089
                rows = [];
1090
 
1091
            $.each(temprows, function (i, val) {
1092
                var row = {id: i};
1093
 
1094
                $.each(val, function (j, col) {
1095
                    row[tempcolumns[j]] = col;
1096
                });
1097
 
1098
                rows.push(row);
1099
            });
1100
 
1101
            // init footable
1102
            $('table.table:visible').footable({
1103
                columns: columns,
1104
                rows: rows
1105
            });
1106
 
1107
            if (rows.length > 0) $('table.table:visible .footable-empty').remove();
1108
 
1109
            // remove errors (except success)
1110
            $('#error').find('div.alert:not(.alert-success)').fadeOut(1500, function () {
1111
                $(this).remove();
1112
            });
1113
        }
1114
 
1115
        // reset button label
1116
        setButtonLoading($('#filterCommands').find('.submit'), false);
1117
    }
1118
 
1119
 
1120
    /**
1121
     * hide or show the custom output filter
1122
     */
1123
    $(document).on('change', '#filterOutputSelection', function() {
1124
 
1125
        // if "custom" is selected, show "customFlowListOutputFormat" otherwise hide it
1126
        if ($(this).val() === 'custom') $('#customListOutputFormat').removeClass('d-none');
1127
        else $('#customListOutputFormat').addClass('d-none');
1128
    });
1129
 
1130
    /**
1131
     * block not available options on "bi-direction" checked
1132
     */
1133
    $(document).on('change', '#filterFlowAggregationGlobal input[name=bidirectional]', function() {
1134
        var $filterFlowAggregation = $('#filterFlowAggregation');
1135
 
1136
        // if "bi-directional" is checked, block (disable) all other aggregation options
1137
        if ($(this).parent().hasClass('active')) {
1138
 
1139
            $filterFlowAggregation.find('[data-disable-on="bi-directional"]').each(function() {
1140
                $(this).parent().removeClass('active').addClass('disabled');
1141
                $(this).prop('disabled', true);
1142
                if ($(this).prop('tagName') === 'SELECT') $(this).prop('selectedIndex', 0);
1143
                else $(this).val('');
1144
            });
1145
 
1146
        } else {
1147
 
1148
            $filterFlowAggregation.find('[data-disable-on="bi-directional"]').each(function() {
1149
                $(this).parent().removeClass('disabled');
1150
                $(this).prop('disabled', false);
1151
            });
1152
 
1153
        }
1154
    });
1155
 
1156
 
1157
    /**
1158
     * handle "onchange" for source/destination address(es) in aggregation filter
1159
     */
1160
    $(document).on('change', '#filterFlowAggregationSourceAddressSelect, #filterFlowAggregationDestinationAddressSelect', function() {
1161
        var kind = $(this).attr('data-kind'),
1162
            $prefixDiv = $('#' + kind + 'CIDRPrefixDiv');
1163
 
1164
        switch ($(this).val()) {
1165
            case 'none':
1166
            case 'srcip':
1167
            case 'dstip':
1168
                $prefixDiv.addClass('d-none');
1169
                break;
1170
            case 'srcip4':
1171
            case 'dstip4':
1172
                $prefixDiv.removeClass('d-none');
1173
                $prefixDiv.find('input').attr('maxlength', 2).val('24');
1174
                break;
1175
            case 'srcip6':
1176
            case 'dstip6':
1177
                $prefixDiv.removeClass('d-none');
1178
                $prefixDiv.find('input').attr('maxlength', 3).val('128');
1179
                break;
1180
        }
1181
    });
1182
 
1183
    /**
1184
     * handle "onchange/onclick" for filter Filters controls
1185
     */
1186
    $(document).on('change', '#filterFiltersSelect', function() {
1187
	document.getElementById('filterNfdumpTextarea').value = event.target.value;
1188
    });
1189
 
1190
    $(document).on('click', '#filterFiltersButtonRemove', function() {
1191
	var filter = [document.getElementById('filterNfdumpTextarea').value];
1192
	var select = document.getElementById('filterFiltersSelect');
1193
        var stored_filters = JSON.parse(window.localStorage.getItem('stored_filters'));
1194
	stored_filters = stored_filters.filter(element => { return !filter.includes(element); });
1195
	stored_filters = JSON.stringify(stored_filters);
1196
	window.localStorage.setItem('stored_filters', stored_filters);
1197
 
1198
        select.innerHTML = '';
1199
        updateDropdown('filters', JSON.parse(stored_filters));
1200
    });
1201
 
1202
    $(document).on('click', '#filterFiltersButtonSave', function() {
1203
        var stored_filters = JSON.parse(window.localStorage.getItem('stored_filters'));
1204
	var filter = [document.getElementById('filterNfdumpTextarea').value];
1205
 
1206
	if (!stored_filters.includes(filter[0]))
1207
	{
1208
	    stored_filters = JSON.stringify( filter.concat(stored_filters));
1209
	    window.localStorage.setItem('stored_filters', stored_filters);
1210
            updateDropdown('filters', filter);
1211
	}
1212
    });
1213
 
1214
 
1215
    /**
1216
     * modify some GUI elements if the user selected "sources" to display
1217
     */
1218
    function displaySourcesHelper() {
1219
        // add "multiple" to source selection
1220
        var $sourceSelect = $('#filterSourcesSelect'),
1221
            $protocolButtons = $('#filterProtocolButtons');
1222
        $sourceSelect.prop('multiple', true);
1223
 
1224
        // disable 'any' in sources
1225
        $sourceSelect.find('option[value="any"]').prop('disabled', true);
1226
 
1227
        // select all sources
1228
        $sourceSelect.find('option:not([disabled])').prop('selected', true);
1229
 
1230
        // uncheck protocol buttons and transform to radio buttons
1231
        $protocolButtons.find('label').removeClass('active');
1232
        $protocolButtons.find('input').prop('checked', false).attr('type', 'radio');
1233
 
1234
        // select Any proto as default
1235
        $protocolButtons.find('[for="filterProtocolAny"]').click();
1236
    }
1237
 
1238
    /**
1239
     * modify some GUI elements if the user selected "protocols" to display
1240
     */
1241
    function displayProtocolsHelper() {
1242
        // remove "multiple" from source select and select first source
1243
        var $sourceSelect = $('#filterSourcesSelect'),
1244
            $protocolButtons = $('#filterProtocolButtons');
1245
        $sourceSelect.prop('multiple', false);
1246
 
1247
        // disable 'any' in sources
1248
        $sourceSelect.find('option[value="any"]').prop('disabled', true).prop('selected', false);
1249
 
1250
        // select the first element
1251
        $sourceSelect.find('option:not([disabled]):first').prop('selected', true);
1252
 
1253
        // protocol buttons become checkboxes and get checked by default
1254
        $protocolButtons.find('label').removeClass('active').filter(() => $(this).find('input').val() !== 'any').click();
1255
        $protocolButtons.find('input').attr('type', 'checkbox').prop('checked', false).filter('[value!="any"]').click();
1256
    }
1257
 
1258
    /**
1259
     * modify some GUI elements if the user selected "ports" to display
1260
     */
1261
    function displayPortsHelper() {
1262
        // remove "multiple" from source select
1263
        var $sourceSelect = $('#filterSourcesSelect'),
1264
            $portsSelect = $('#filterPortsSelect'),
1265
            $protocolButtons = $('#filterProtocolButtons');
1266
        $sourceSelect.attr('multiple', false);
1267
 
1268
        // enable 'any' in sources
1269
        $sourceSelect.find('option[value="any"]').prop('disabled', false);
1270
 
1271
        // uncheck protocol buttons and transform to radio buttons
1272
        $protocolButtons.find('label').removeClass('active');
1273
        $protocolButtons.find('label input').prop('checked', false).attr('type','radio');
1274
 
1275
        // select TCP proto as default
1276
        $protocolButtons.find('label:first').addClass('active').find('input').prop('checked', true);
1277
 
1278
        // select all ports
1279
        $portsSelect.find('option').prop('selected', true);
1280
    }
1281
 
1282
    /**
1283
     * updates the filter dropdowns with data
1284
     * @param displaytype string: sources/ports/protocols
1285
     * @param array array: the values to add
1286
     */
1287
    function updateDropdown(displaytype, array) {
1288
        var id = '#filter' + displaytype.charAt(0).toUpperCase() + displaytype.slice(1);
1289
        var $select = $(id).find('select');
1290
 
1291
        $.each(array, function(key, value) {
1292
            $select
1293
                .append($('<option></option>')
1294
                .attr('value',value).text(value));
1295
        });
1296
    }
1297
});
1298
 
1299
/*!
1300
 Filesize.js
1301
 2022 Jason Mulligan <jason.mulligan@avoidwork.com>
1302
 @version 9.0.11
1303
*/
1304
!function(i,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(i="undefined"!=typeof globalThis?globalThis:i||self).filesize=t()}(this,(function(){"use strict";function i(t){return i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},i(t)}var t="array",o="bits",e="byte",n="bytes",r="",b="exponent",l="function",a="iec",d="Invalid number",f="Invalid rounding method",u="jedec",s="object",c=".",p="round",y="kbit",m="string",v={symbol:{iec:{bits:["bit","Kibit","Mibit","Gibit","Tibit","Pibit","Eibit","Zibit","Yibit"],bytes:["B","KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"]},jedec:{bits:["bit","Kbit","Mbit","Gbit","Tbit","Pbit","Ebit","Zbit","Ybit"],bytes:["B","KB","MB","GB","TB","PB","EB","ZB","YB"]}},fullform:{iec:["","kibi","mebi","gibi","tebi","pebi","exbi","zebi","yobi"],jedec:["","kilo","mega","giga","tera","peta","exa","zetta","yotta"]}};function g(g){var h=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},B=h.bits,M=void 0!==B&&B,S=h.pad,T=void 0!==S&&S,w=h.base,x=void 0===w?-1:w,E=h.round,j=void 0===E?2:E,N=h.locale,P=void 0===N?r:N,k=h.localeOptions,G=void 0===k?{}:k,K=h.separator,Y=void 0===K?r:K,Z=h.spacer,z=void 0===Z?" ":Z,I=h.symbols,L=void 0===I?{}:I,O=h.standard,q=void 0===O?r:O,A=h.output,C=void 0===A?m:A,D=h.fullform,F=void 0!==D&&D,H=h.fullforms,J=void 0===H?[]:H,Q=h.exponent,R=void 0===Q?-1:Q,U=h.roundingMethod,V=void 0===U?p:U,W=h.precision,X=void 0===W?0:W,$=R,_=Number(g),ii=[],ti=0,oi=r;-1===x&&0===q.length?(x=10,q=u):-1===x&&q.length>0?x=(q=q===a?a:u)===a?2:10:q=10===(x=2===x?2:10)||q===u?u:a;var ei=10===x?1e3:1024,ni=!0===F,ri=_<0,bi=Math[V];if(isNaN(g))throw new TypeError(d);if(i(bi)!==l)throw new TypeError(f);if(ri&&(_=-_),(-1===$||isNaN($))&&($=Math.floor(Math.log(_)/Math.log(ei)))<0&&($=0),$>8&&(X>0&&(X+=8-$),$=8),C===b)return $;if(0===_)ii[0]=0,oi=ii[1]=v.symbol[q][M?o:n][$];else{ti=_/(2===x?Math.pow(2,10*$):Math.pow(1e3,$)),M&&(ti*=8)>=ei&&$<8&&(ti/=ei,$++);var li=Math.pow(10,$>0?j:0);ii[0]=bi(ti*li)/li,ii[0]===ei&&$<8&&-1===R&&(ii[0]=1,$++),oi=ii[1]=10===x&&1===$?M?y:"kB":v.symbol[q][M?o:n][$]}if(ri&&(ii[0]=-ii[0]),X>0&&(ii[0]=ii[0].toPrecision(X)),ii[1]=L[ii[1]]||ii[1],!0===P?ii[0]=ii[0].toLocaleString():P.length>0?ii[0]=ii[0].toLocaleString(P,G):Y.length>0&&(ii[0]=ii[0].toString().replace(c,Y)),T&&!1===Number.isInteger(ii[0])&&j>0){var ai=Y||c,di=ii[0].toString().split(ai),fi=di[1]||r,ui=fi.length,si=j-ui;ii[0]="".concat(di[0]).concat(ai).concat(fi.padEnd(ui+si,"0"))}return ni&&(ii[1]=J[$]?J[$]:v.fullform[q][$]+(M?"bit":e)+(1===ii[0]?r:"s")),C===t?ii:C===s?{value:ii[0],symbol:ii[1],exponent:$,unit:oi}:ii.join(z)}return g.partial=function(i){return function(t){return g(t,i)}},g}));
1305
<0&&($=0),$>//# sourceMappingURL=filesize.min.js.map