Работа с датами. Вперед-назад, проверка ограничений.
[squid-reports.git] / web / squid.js
1 urlbase="./api.php"
2
3 graph_colors = [
4 "salmon", "lightcoral", "crimson", "red", "darkred", "orangered",
5 "gold", "orange", "yellow", "darkkhaki", "lime", "green", "greenyellow",
6 "springgreen", "olive", "cyan", "turquoise", "teal", "lightskyblue",
7 "dodgerblue", "royalblue", "blue", "navy", "violet", "fuchsia", 
8 "darkviolet", "purple", "deeppink", "gray", "darkslategray", "sandybrown",
9 "goldenrod", "chocolate", "saddlebrown", "maroon", "rosybrown", "sienna", 
10 "brown", "dimgray", "mediumvioletred", "indigo", "orchid", "mediumpurple", 
11 "steelblue", "powderblue", "lightseagreen", "cadetblue", "aquamarine", 
12 "olivedrab", "chartreuse", "darkolivegreen", "tomato", "firebrick" 
13 ]
14
15 currentState=""
16
17 templates = {}
18 columns = {}
19
20 cats = {}
21 reps = {}
22
23 data = []
24
25 current_filter = {}
26
27 dictionaries = {}
28
29 current_rep = ''
30
31 online_traffic = null
32 online_connections = null
33 online_hosts = []
34
35 timer = null
36
37 assigned_colors = []
38
39 current_time = null
40 time_labels = []
41
42 function toDate(d) {
43   var curr_day = d.getDate();
44   if (curr_day<10) { curr_day = '0' + curr_day; }
45   var curr_month = d.getMonth() + 1;
46   if (curr_month<10) { curr_month = '0' + curr_month; }
47   var curr_year = d.getFullYear();
48   return curr_year + "-" + curr_month + "-" + curr_day
49 }
50
51 function Today() {
52   var dToday = new Date();
53   dToday.setHours(0,0,0,0)
54   return dToday
55 }
56
57 today = toDate(Today());
58
59 date_from = today
60 date_to = today
61
62 function UrlParams(params) {
63
64   var out = [];
65
66   for (var key in params) {
67     if (params.hasOwnProperty(key)) {
68       out.push(key + '=' + encodeURIComponent(params[key]));
69     }
70   }
71
72   return out.join('&');
73
74 }
75
76 function Macro(template, values) {
77   return templates[template].replace(/\$(\w+)\;/g,function (s,name) {
78       return values[name]
79     })
80 }
81
82 function ColumnMacro(column, values, macro = "column"){
83   var columnrec = columns[column]
84   var templatestr
85   if (columnrec) {
86     if (columnrec.template_name) {
87       macro = columnrec.template_name
88     }
89     templatestr = columnrec.template
90   }
91   if (templatestr) {
92     return templatestr.replace(/\$(\w+)\;/g,function (s,name) {
93       return values[name]
94     })
95   } else {
96     return Macro(macro,{VALUE:values[column]})
97   }
98 }
99
100 function HeaderMacro(column,macro = "header-column"){
101   var columnrec = columns[column]
102   if (columnrec) {
103     var alias = columns[column].alias;
104   } else {
105     var alias = ''
106   }
107   if (!(alias)) { alias = column }
108   return Macro(macro,{VALUE:alias})
109 }
110
111 function GetApi(onfinish,method,params) {
112
113   var req = new XMLHttpRequest();
114
115   req.onreadystatechange = function () {
116     if (this.readyState != 4) return; 
117     if (this.status != 200) {
118       setTimeout(OnLoad,30000);
119       return;
120     }
121     res = JSON.parse(this.responseText);
122     onfinish(res);
123   };
124
125   var url  = urlbase+"?method="+method;
126
127   if (params) {
128   
129     url = url + '&' + UrlParams(params)
130   
131   }
132
133   req.open("GET", url, true);
134   req.withCredentials = true;
135   req.send();
136
137 }
138
139 function UpdatePageProps(props) {
140
141   var logo = document.getElementById("brand");
142   logo.innerText = props["site-header"];
143
144   refresh = props["online_refresh"]
145   online_history = props["online_history"]
146
147   cats = props["cats"]
148
149   for (var i in res["columns"]) { 
150     columns[res["columns"][i]["name"]] = {alias:res["columns"][i]["alias"],template:res["columns"][i]["template"],template_name:res["columns"][i]["template_name"]}
151   }
152
153   for (var i in res["templates"]) { 
154     var mnemo = res["templates"][i]["mnemo"]
155     var body = res["templates"][i]["body"]
156     templates[mnemo] = body
157   }
158
159   dictionaries = res["dictionaries"]
160  
161   menuInnerHTML = ""
162   for (var cat in cats) {
163     category = cats[cat]
164     innerHTML = ""
165     for (rep in category["reps"]) {
166       report = category["reps"][rep]
167       reps[report["mnemo"]] = report
168       reptext = Macro("menuitem",{ MNEMO:report["mnemo"], NAME:report["name"], DESCR:report["description"] })
169       innerHTML = innerHTML + reptext
170     }
171     grouptxt = Macro("menugroup",{ MNEMO:category["mnemo"], NAME:category["name"], DESCR:category["description"], MENUITEM: innerHTML})
172     menuInnerHTML = menuInnerHTML + grouptxt
173   }
174   menuInnerHTML = Macro("menuonline",{}) + menuInnerHTML
175   menuHTML = Macro("menu", { MENUGROUP: menuInnerHTML} )
176   var Left = document.getElementById("menu");
177   Left.innerHTML = menuHTML
178
179   Online();
180   
181 }
182
183 function SetDates() {
184   var inp = document.getElementById("date-from")
185   inp.value = date_from
186   inp.max = date_to
187
188   var inp = document.getElementById("date-to")
189   inp.value = date_to
190   inp.min = date_from
191
192   var inp = document.getElementById("report-button")
193   if (current_rep) {
194     inp.disabled = false
195   } else {
196     inp.disabled = true
197   }
198 }
199
200 function OnLoad() {
201
202   GetApi(UpdatePageProps,"get-base-config",null)
203   SetDates();
204
205 }
206
207 function ShowHide(id) {
208   var content = document.getElementById(id);
209   if (content.style.display === "block") {
210     content.style.display = "none";
211   } else {
212     content.style.display = "block";
213   }
214 }
215
216 function MergeTR(tr,th, macro = "column") {
217   var str = ""
218   for (i in th) {
219     if (!(th[i].startsWith('_'))) {
220       str = str + ColumnMacro(th[i],tr,macro) 
221     }
222   }
223   return str
224 }
225
226 function MergeTH(th, macro = "header-column") {
227   var str = ""
228   for (i in th) {
229     if (!(th[i].startsWith('_'))) {
230       str = str + HeaderMacro(th[i])  
231     }
232   }
233   return str
234 }
235
236 function ProduceRep(res) {
237
238   current_filter = res["filter"]
239   if (!current_filter) { current_filter = {} }
240   dictionary = res["dictionary"]
241
242   header_template = res["header"]
243   has_total = res["has_total"]
244
245   innerHTML = ""
246   data = res["data"]
247   ii=0
248
249   if (has_total == "1") {
250     total = data.pop()
251   }
252   
253   for (i in data) {
254     row_data = data[i]
255     table_row = Macro("table-row",{DATA:MergeTR(row_data,dictionary)})
256     innerHTML = innerHTML + table_row
257   }
258
259   headerHTML = Macro("header-row",{DATA:MergeTH(dictionary)})
260   
261   if (has_total == "1") {
262     totalHTML = Macro("total-row",{DATA:MergeTR(total,dictionary)})
263     reportHTML = Macro("report-table-total",{HEADER:headerHTML,LINES:innerHTML,TOTAL:totalHTML})
264   } else {
265     reportHTML = Macro("report-table",{HEADER:headerHTML,LINES:innerHTML})
266   }
267   var body = document.getElementById("report-body") 
268   body.innerHTML = reportHTML;
269
270   if (reps[current_rep].graph_x) {
271     DisplayGraph(true)
272     var config = PrepareGraphDataset(data,reps[current_rep].graph_x,reps[current_rep].graph_y,reps[current_rep].graph_series)
273     config.options.responsive = true
274     
275     config.options.scales = {
276       xAxes: [{
277         scaleLabel: {
278           display: true,
279         }
280       }],
281       yAxes: [{
282         stacked: true,
283         scaleLabel: {
284           display: true,
285         },
286         ticks: {
287           suggestedMin: 0,    // minimum will be 0, unless there is a lower value.
288         }
289       }]
290     }
291     DrawGraph(config)
292   } else {
293     DisplayGraph(false)
294   }
295
296 }
297
298 function DrawGraph(config) {
299   var ctx = document.getElementById('canvas').getContext('2d')
300   var div = document.getElementById("report-graph");
301   canvas.width = div.style.width;
302   canvas.height = div.style.height;
303   window.Graph = new Chart(ctx, config);
304 }
305
306 function AssignColor(key) {
307   if (assigned_colors[key]) {
308     return assigned_colors[key]
309   }
310   var rand_color 
311   if (graph_colors.length) { 
312     var idx = Math.floor(Math.random() * graph_colors.length)
313     rand_color = graph_colors[idx]
314     graph_colors.splice(idx,1)
315   } else {
316     rand_color = "darkgray"
317   }
318   assigned_colors[key] = rand_color
319   return rand_color
320 }
321
322 function PrepareGraphDataset(data,graph_x,graph_y,graph_series) {
323   
324   var xvals = []
325   var series = []
326
327   var values = []
328
329   for (i in data) {
330     rec = data[i]
331     for (key in rec) {
332       if (key == graph_x) {
333         if (!xvals.includes(Number(rec[key]))) {
334           xvals.push(Number(rec[key]))
335         }
336       }
337       if (key == graph_series) {
338         if (!series.includes(rec[key])) {
339           series.push(rec[key])
340         }
341       }
342     }
343   }
344   
345   xvals.sort(function(a,b) { return a-b; })
346   series.sort()
347
348   for (var i in series) {
349     values[i] = {}
350     values[i].data = []
351     values[i].label = series[i]
352     values[i].fill = true
353     values[i].borderColor = AssignColor(values[i].label)
354     values[i].backgroundColor = AssignColor(values[i].label)
355     for (var j in xvals) {
356       values[i].data[j] = 0
357     } 
358   }
359
360   for (var k in data) {
361     rec = data[k]
362     xval = null
363     yval = null
364     dataset = null
365     for (key in rec) {
366       if (key == graph_x) {
367         xval = Number(rec[key])
368       }
369       if (key == graph_y) {
370         yval = Number(rec[key])
371       }
372       if (key == graph_series) {
373         dataset = rec[key]
374       }
375     }
376     var j = xvals.indexOf(xval)
377     var i = series.indexOf(dataset)
378     values[i].data[j] = yval 
379   }
380
381   return  {
382       type: 'line',
383       data: {
384         labels: xvals,
385         datasets: values,
386       },
387       options: {
388       }
389     }
390
391   
392 }
393
394 function AliasByName(dict,name) {
395   for (i in dict) {
396     if (dict[i].name == name) { return dict[i].alias }
397   }
398   return name
399 }
400
401 function AddTraffic(label,b) {
402   var rec
403   for (i in online_traffic) {
404     if (online_traffic[i].label == label) {
405       rec = online_traffic[i]
406       break
407     }
408   }
409   if (!rec) {
410     rec = { label: label, borderColor: AssignColor(label), backgroundColor: AssignColor(label), data: new Array(online_history).fill(0) }
411     online_traffic.push(rec)
412   }
413   rec.data[rec.data.length-1] += b
414 }
415
416 function ProduceOnline(res) {
417
418   if (!online_traffic) {
419     current_time = null
420     online_traffic = []
421     time_labels = []
422     cur = Date.now()
423     for (i = 1; i<=online_history; i++) {
424       cur = cur - refresh
425       time_labels.unshift(cur)
426     }
427     config = {
428       type: 'line',
429       data: {
430         labels: time_labels,
431         datasets: online_traffic,
432       },
433       options: {
434         animation: {
435           duration: 0
436         },
437         responsive: true,
438         scales: {
439           xAxes: [{
440             type: 'time',
441             scaleLabel: {
442               display: true,
443             }
444           }],
445           yAxes: [{
446             stacked: true,
447             ticks: {
448                 suggestedMin: 0,    // minimum will be 0, unless there is a lower value.
449             },
450             scaleLabel: {
451               display: true,
452               labelString: "Скорость, Кбит/c"
453             }
454           }]
455         }
456       }
457     }
458     DrawGraph(config) 
459   }
460
461   var delta
462
463   if (current_time) {
464     delta = (Date.now() - current_time)/1000
465   } else {
466     delta = 1
467   }
468   
469   current_time = Date.now()
470
471   time_labels.shift()
472   time_labels.push(current_time)
473   
474   for (i = online_traffic.length-1; i>=0; i--) {
475     online_traffic[i].data.shift()
476     online_traffic[i].data.push(0)
477   }
478   
479   dictionary = res["dictionary"]
480
481   header_template = res["header"]
482
483   innerHTML = ""
484   data = res["data"]
485   ii=0
486
487   var new_online_connections = []
488
489   dictionary.unshift("useralias")
490   for (i in data) {
491     row_data = data[i]
492     user = row_data["_user"]
493     username = AliasByName(dictionaries['user_id'],user)
494     row_data["useralias"] = username
495     
496     host = row_data["host"]
497     hostname = AliasByName(dictionaries['host_id'],host)
498     row_data["host"] = hostname
499     
500     table_row = Macro("table-row",{DATA:MergeTR(row_data,dictionary)})
501     innerHTML = innerHTML + table_row
502
503     var bytes = Number(row_data['bytes'])
504     
505     var idx = row_data["_ip"]+':'+row_data["_port"]  
506     var last_bytes 
507     
508     if (online_connections) {
509       last_bytes = online_connections[idx]
510       if (!last_bytes) {
511         last_bytes = 0
512       }
513     } else {
514       last_bytes = bytes
515     }
516     
517     new_online_connections[idx] = bytes
518     
519     AddTraffic(username,8*(bytes-last_bytes)/(1024*delta))
520   }
521   
522   online_connections = new_online_connections
523
524   for (i = online_traffic.length-1; i>=0; i--) {
525     if (Math.max.apply(null,online_traffic[i].data) == 0) {
526       online_traffic.splice(i,1)
527     }
528   }
529
530   headerHTML = Macro("header-row",{DATA:MergeTH(dictionary)})
531   
532   reportHTML = Macro("report-table",{HEADER:headerHTML,LINES:innerHTML})
533
534   var body = document.getElementById("report-body") 
535   body.innerHTML = reportHTML;
536
537   timer = setTimeout(Online,refresh)
538  
539   window.Graph.update()
540
541 }
542
543 function CancelRefresh() {
544   if (timer) {
545     clearTimeout(timer)
546     timer = null
547   }
548 }
549
550 function ShowRep(id) {
551   CancelRefresh()
552   current_filter = {}
553   var header = document.getElementById("report-name") 
554   header.innerText = reps[id]["name"] 
555   var body = document.getElementById("report-body") 
556   body.innerText = "Отчет загружается..." 
557   current_rep = id
558   UpdateDates()
559   Rerun()
560 }
561
562 function ShowFilteredRep(id,filter) {
563   CancelRefresh()
564   for (i in filter) {
565     current_filter[i] = filter[i]
566   }
567   var header = document.getElementById("report-name") 
568   header.innerText = reps[id]["name"] 
569   var body = document.getElementById("report-body") 
570   body.innerText = "Отчет загружается..." 
571   current_rep = id
572   UpdateDates()
573   Rerun()
574 }
575
576 function FilterSelect(name,dict,value) {
577   var str = '<select onchange="SetFilter(\''+name+'\',this);">'
578   for (key in dict) {
579     if (dict[key].id==value) {
580       str = str + '<option selected value="'+dict[key].id+'">'+dict[key].name+"</option>"
581     } else {
582       str = str + '<option value="'+dict[key].id+'">'+dict[key].name+"</option>"
583     }
584   }
585   str = str + "</select>" + Macro("clear-filter",{NAME:name})
586   return str
587 }
588
589 function Filter() {
590   var str = ""
591   for (key in current_filter) {
592     var dict = dictionaries[key]
593     if(dict) {
594       str = str + FilterSelect(key,dict,current_filter[key])
595     } else {
596       str = str + Macro("filter-display",{NAME:HeaderMacro(key),VALUE:current_filter[key]}) + Macro("clear-filter",{NAME:key})
597     }
598   }
599   return str
600 }
601
602 function RefreshFilterPane() {
603   var filter = document.getElementById("report-dates")
604   if (current_rep) {
605     filter.style.display = "block";
606   } else {
607     filter.style.display = "none";
608   } 
609   var filter = document.getElementById("filter") 
610   if (Object.keys(current_filter).length === 0) {
611     filter.style.display = "none";
612     filter.innerHTML = ''
613   } else {
614     filter.style.display = "inline-block";
615     filter.innerHTML = Filter()
616   }
617 }
618
619 function Rerun() {
620   online_traffic = null
621   var parameters = JSON.parse(JSON.stringify(current_filter))
622   parameters["mnemo"] = current_rep
623   parameters["date_from"] = date_from
624   parameters["date_to"] = date_to
625   GetApi(ProduceRep,"report",parameters);
626   var filter = document.getElementById("filter") 
627   RefreshFilterPane();
628 }
629
630 function Online() {
631   CancelRefresh()
632   RefreshFilterPane()
633   var header = document.getElementById("report-name") 
634   header.innerText = "Активные соединения" 
635   GetApi(ProduceOnline,"online",{});
636   DisplayGraph(true)
637 }
638
639 function UpdateDates() {
640   var inp = document.getElementById("date-from")
641   date_from = inp.value
642   var inp = document.getElementById("date-to")
643   date_to = inp.value
644
645   SetDates();
646
647 }
648
649 function DateLeft() {
650   UpdateDates()
651   var dFrom = new Date(date_from)
652   var dTo = new Date(date_to)
653   var delta = Math.round(((dTo.getTime() - dFrom.getTime()))/(86400*1000))
654   dTo.setDate(dTo.getDate() - (1 + delta))
655   dFrom.setDate(dFrom.getDate() - (1 + delta))
656   date_from = toDate(dFrom)
657   date_to = toDate(dTo)
658   SetDates()
659 }
660
661 function DateRight() {
662   dToday = new Date();
663   UpdateDates()
664   var dFrom = new Date(date_from)
665   var dTo = new Date(date_to)
666   var delta = Math.round(((dTo.getTime() - dFrom.getTime()))/(86400*1000))
667   dFrom.setDate(dFrom.getDate() + 1 + delta)
668   dTo.setDate(dTo.getDate() + 1 + delta)
669   if (dTo>Today()) {
670     dTo = Today()
671   }
672   if (dFrom>Today()) {
673     dFrom = Today()
674   }
675   date_from = toDate(dFrom)
676   date_to = toDate(dTo)
677   SetDates()
678 }
679
680 function SetFilter(name,select) {
681   current_filter[name] = select.value;
682 }
683
684 function ClearFilter(name) {
685   delete current_filter[name];
686   RefreshFilterPane();
687 }
688
689 function DisplayGraph(on) {
690   var selector = document.getElementById("page-selector") 
691   if (on) {
692     selector.style.display="block"
693   } else {
694     selector.style.display="none"
695     Display('report-body')
696   }
697 }
698
699 function Display(id) {
700   var elements = document.getElementsByClassName('report-block');
701   for (i=0; i<elements.length; i++) {
702     if (elements[i].id == id) {
703       elements[i].style.display = "block"
704     } else {
705       elements[i].style.display = "none"
706     }
707   }
708 }