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