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"> Loading…</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> '; 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> '; 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> '; 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> '; 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}));0&&($=0),$>
|
|
|
1305 |
<0&&($=0),$>//# sourceMappingURL=filesize.min.js.map0&&($=0),$>
|