%PDF- %PDF-
| Direktori : /proc/self/root/backups/router/usr/local/opnsense/mvc/app/views/OPNsense/Unbound/ |
| Current File : //proc/self/root/backups/router/usr/local/opnsense/mvc/app/views/OPNsense/Unbound/overview.volt |
{#
# Copyright (c) 2022 Deciso B.V.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#}
{% set theme_name = ui_theme|default('opnsense') %}
<script src="{{ cache_safe('/ui/js/chart.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/chartjs-plugin-streaming.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/chartjs-plugin-colorschemes.js') }}"></script>
<script src="{{ cache_safe('/ui/js/moment-with-locales.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/chartjs-adapter-moment.js') }}"></script>
<link rel="stylesheet" type="text/css" href="{{ cache_safe(theme_file_or_default('/css/chart.css', theme_name)) }}" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="{{ cache_safe(theme_file_or_default('/css/dns-overview.css', theme_name)) }}" rel="stylesheet"/>
<script>
$(document).ready(function() {
$('#info').hide();
function set_alpha(color, opacity) {
const op = Math.round(Math.min(Math.max(opacity || 1, 0), 1) * 255);
return color + op.toString(16).toUpperCase();
}
function create_chart(target, stepsize, feed_data, logarithmic) {
const ctx = target[0].getContext('2d');
const config = {
type: 'line',
data: {
datasets: [{
label: 'Total queries',
data: feed_data,
borderWidth: 1,
parsing: {
yAxisKey: 'y.total'
}
}, {
label: 'Blocked',
data: feed_data,
borderWidth: 1,
parsing: {
yAxisKey: 'y.blocked'
}
}]
},
options: {
maintainAspectRatio: false,
responsive: true,
aspectRatio: 1,
elements: {
line: {
fill: false,
cubicInterpolationMode: 'monotone',
clip: 0,
},
point: {
radius: 0
}
},
layout: {
padding: {
left: 40,
right: 50,
bottom: 20
}
},
scales: {
x: {
type: 'time',
time: {
tooltipFormat:'HH:mm',
unit:'minute',
stepSize: stepsize,
minUnit: 'minute',
displayFormats: {
minute: 'HH:mm'
}
}
},
y: {
ticks: {
callback: function (value, index, values) {
/* workaround for chart.js v3: no proper handling for
* logarithmic scale when 0 values are supplied.
*/
if (index === 0) {
return '0';
}
/* Don't show decimal values on the y-axis */
if (Math.floor(value) == value) {
return value;
}
},
autoSkip: true,
autoSkipPadding: 10
},
type: logarithmic ? 'logarithmic' : 'linear',
min: logarithmic ? 0.1 : 0,
}
},
plugins: {
tooltip: {
mode: 'nearest',
intersect: false,
callbacks: {
label: function(context) {
let val = context.parsed.y == 0.1 ? 0 : context.parsed.y
return context.dataset.label + ': ' + val.toLocaleString();
}
}
},
legend: {
display: true
},
colorschemes: {
scheme: 'brewer.DarkTwo8'
}
}
}
};
return new Chart(ctx, config);
}
function create_client_chart(target, stepsize, feed_data, logarithmic) {
const ctx = target[0].getContext('2d');
const config = {
type: 'bubble',
data: {
datasets: feed_data
},
options: {
maintainAspectRatio: false,
responsive: true,
aspectRatio: 1,
layout: {
padding: {
left: 40,
right: 50,
bottom: 20
}
},
scales: {
x: {
type: 'time',
time: {
tooltipFormat:'HH:mm',
unit:'minute',
stepSize: stepsize,
minUnit: 'minute',
displayFormats: {
minute: 'HH:mm'
}
}
},
y: {
ticks: {
callback: function (value, index, values) {
if (index === 0) {
return '0';
}
/* Don't show decimal values on the y-axis */
if (Math.floor(value) == value) {
return value;
}
},
autoSkip: true,
autoSkipPadding: 10
},
type: logarithmic ? 'logarithmic' : 'linear',
min: logarithmic ? 0.1 : 0,
}
},
onClick: function(e, a) {
let element = a[0];
if (typeof element == 'undefined') return;
let dataset = this.config._config.data.datasets[element.datasetIndex];
let label = dataset.label;
let data = dataset.data[element.index];
let timestamp = data.x
let ip = data.z;
g_clientFilter = ip;
g_timeFilter = timestamp;
g_labelFilter = label;
$('.nav-tabs a[href="#query-details"]').tab('show');
},
plugins: {
tooltip: {
mode: 'nearest',
intersect: false,
filter: function(context) {
return context.parsed.y != 0.1;
},
callbacks: {
label: function(context) {
/* Logarithmic scaling workaround continuation: replace the earlier
* supplied 0.1 values (if any) with 0 in the tooltip menu
*/
if (context) {
if (context.parsed.y == 0.1) {
return null;
}
let label = context.dataset.label
let val = context.parsed.y == 0.1 ? 0 : context.parsed.y
return label + ' (' + val.toLocaleString() + ')';
}
},
title: function(context) {
if (context[0]) {
/* Default bubble chart has no tooltip title, add the formatted time */
return context[0].formattedValue.split(',')[0].replace(/[{()}]/g, '');;
}
},
afterBody: function(context) {
return 'Click to view details';
}
}
},
legend: {
display: false
},
colorschemes: {
scheme: 'brewer.DarkTwo8'
}
}
}
};
return new Chart(ctx, config);
}
function formatQueryData(data, logarithmic) {
let formatted = [];
Object.keys(data).forEach((key, index) => {
/* workaround for logarithmic scale, see https://github.com/chartjs/Chart.js/issues/9629 */
if (logarithmic) {
Object.keys(data[key]).forEach((k, i) => {
if (data[key][k] == 0) {
data[key][k] = 0.1
}
})
}
formatted.push({
x: key * 1000,
y: data[key]
});
});
/* Add a redundant data step to end the chart time axis properly */
if (formatted.length > 0) {
let lastVal = formatted[formatted.length - 1];
let interval = $("#timeperiod").val() == 1 ? 60 : 600;
let y_val = logarithmic ? 0.1 : 0
formatted.push({
x: lastVal.x + (interval * 1000),
y: {
"total": y_val,
"blocked": y_val
}
});
}
return formatted;
}
function formatClientData(data, logarithmic) {
let uniqueClients = [...new Set(
Object.values(data)
.map(Object.keys)
.reduce((a, b) => a.concat(b), [])
)]
let formatted = [];
// split into different datasets
for (let i = 0; i < uniqueClients.length; i++) {
let tmp = []
let backup_val = logarithmic ? 0.1 : null;
let label = uniqueClients[i];
let hasHostname = false;
Object.keys(data).forEach((key, index) => {
if (!hasHostname) {
if (data[key].hasOwnProperty(uniqueClients[i]) && data[key][uniqueClients[i]]['hostname'] != '') {
label = data[key][uniqueClients[i]]['hostname'];
hasHostname = true;
}
}
/* Similarly with the query line chart, the bubble chart cannot handle null values on a log scale */
tmp.push({
x: key * 1000,
y: data[key].hasOwnProperty(uniqueClients[i]) ? data[key][uniqueClients[i]]['count'] : backup_val,
r: data[key].hasOwnProperty(uniqueClients[i]) ? 4 : 0,
z: data[key].hasOwnProperty(uniqueClients[i]) ? uniqueClients[i] : null // meta-data; not presented, but is necessary for drill-down
})
});
/* Add a redundant data step to end the chart time axis properly */
if (tmp.length > 0) {
let lastVal = tmp[tmp.length - 1];
let interval = $("#timeperiod-clients").val() == 1 ? 60 : 600;
tmp.push({
x: lastVal.x + (interval * 1000),
y: backup_val,
r: 0
});
}
let colors = Chart.colorschemes.brewer.DarkTwo8.length;
let colorIdx = i - parseInt(i / colors) * colors;
let bgColor = Chart.colorschemes.brewer.DarkTwo8[colorIdx];
formatted.push({
label: label,
data: tmp,
borderWidth: 1,
backgroundColor: set_alpha(bgColor, 0.5),
borderColor: bgColor,
hoverRadius: 10
})
}
return formatted;
}
function updateQueryChart(use_log=false) {
let def = new $.Deferred();
ajaxGet('/api/unbound/overview/rolling/' + $("#timeperiod").val(), {}, function(data, status) {
let formatted = formatQueryData(data, use_log);
g_queryChart.config.options.scales.x.time.stepSize = $("#timeperiod").val() == 1 ? 5 : 60;
g_queryChart.config.data.datasets.forEach(function(dataset) {
dataset.data = formatted;
});
if (use_log) {
g_queryChart.config.options.scales.y.type = 'logarithmic';
g_queryChart.config.options.scales.y.min = 0.1;
} else {
g_queryChart.config.options.scales.y.type = 'linear';
g_queryChart.config.options.scales.y.min = 0;
}
g_queryChart.update();
def.resolve();
});
return def;
}
function updateClientChart(use_log=false) {
let def = new $.Deferred();
ajaxGet('/api/unbound/overview/rolling/' + $("#timeperiod-clients").val() + '/1', {}, function(data, status) {
let formatted = formatClientData(data, use_log);
g_clientChart.config.options.scales.x.time.stepSize = $("#timeperiod-clients").val() == 1 ? 5 : 60;
g_clientChart.config.data.datasets = formatted;
if (use_log) {
g_clientChart.config.options.scales.y.type = 'logarithmic';
g_clientChart.config.options.scales.y.min = 0.1;
} else {
g_clientChart.config.options.scales.y.type = 'linear';
g_clientChart.config.options.scales.y.min = 0;
}
g_clientChart.update();
def.resolve();
});
return def;
}
function createTopList(id, data, type, reverse_domains) {
ajaxGet('/api/unbound/overview/isBlockListEnabled', {}, function(bl_enabled, status) {
/* reverse_domains refers to the domains for which the opposite action should take place,
* e.g. if a domain is presented that has been blocked N amount of times, but has been
* whitelisted at a later point in time, the action should be to block it, not whitelist it.
*/
for (let i = 0; i < 10; i++) {
let class_type = type == "pass" ? "block-domain" : "whitelist-domain";
let icon_type = type == "pass" ? "fa fa-ban text-danger" : "fa fa-pencil text-info";
let domain = Object.keys(data)[i];
let statObj = Object.values(data)[i];
if (typeof domain == 'undefined' || typeof statObj == 'undefined') {
$('#' + id).append(
'<li class="list-group-item list-group-item-border list-item-domain top-item">' +
(i + 1) + '. ' +
'<span class="counter">0 (0.0%)' +
'</span></li>'
)
continue;
}
let stripped = domain.replace(/\.$/, "");
if (reverse_domains.has(stripped)) {
icon_type = type == "pass" ? "fa fa-pencil text-info" : "fa fa-ban text-danger";
class_type = type == "pass" ? "whitelist-domain" : "block-domain";
}
let icon = '<button type="button" class="'+ class_type + '" data-value="'+ domain +'" ' +
'data-toggle="tooltip" style="margin-left: 10px;"><i class="' + icon_type + '"></i></button>'
if (bl_enabled.enabled == 0) {
icon = '';
}
let bl = statObj.hasOwnProperty('blocklist') ? '(' + statObj.blocklist + ')' : '';
$('#' + id).append(
'<li class="list-group-item list-group-item-border list-item-domain top-item">' +
'<p class="group-p">' + (i + 1) + '. ' + domain + ' ' + bl + '  </p>' +
'<span class="counter">'+ statObj.total +' (' + statObj.pcnt +'%)' +
icon +
'</span></li>'
)
}
reset_tooltips();
});
}
function create_or_update_totals() {
ajaxGet('/api/unbound/overview/totals/10', {}, function(data, status) {
$('.top-item').remove();
$('#totalCounter').html(data.total);
$('#blockedCounter').html(data.blocked.total + " (" + data.blocked.pcnt + "%)");
$('#sizeCounter').html(data.blocklist_size);
$('#resolvedCounter').html(data.resolved.total + " (" + data.resolved.pcnt + "%)");
createTopList('top', data.top, 'pass', new Set(data.blocklisted_domains));
createTopList('top-blocked', data.top_blocked, 'block', new Set(data.whitelisted_domains));
$('#top li:nth-child(even)').addClass('odd-bg');
$('#top-blocked li:nth-child(even)').addClass('odd-bg');
$('#bannersub').html("Starting from " + (new Date(data.start_time * 1000)).toLocaleString());
});
}
function reset_tooltips() {
$(".block-domain").attr('title', "{{ lang._('Block Domain') }}").tooltip({container: 'body', trigger: 'hover'});
$(".whitelist-domain").attr('title', "{{ lang._('Whitelist Domain') }}").tooltip({container: 'body', trigger: 'hover'});
}
g_queryChart = null;
g_clientChart = null;
g_clientFilter = null;
g_timeFilter = null;
g_labelFilter = null;
/* Initial page load */
function do_startup() {
let def = new $.Deferred();
ajaxGet('/api/unbound/overview/isEnabled', {}, function(is_enabled, status) {
if (is_enabled.enabled == 0) {
def.reject();
return;
}
def.resolve();
if (window.localStorage) {
if (window.localStorage.getItem("api.unbound.overview.timeperiod") !== null) {
$("#timeperiod").val(window.localStorage.getItem("api.unbound.overview.timeperiod"));
}
if (window.localStorage.getItem("api.unbound.overview.timeperiodclients") !== null) {
$("#timeperiod-clients").val(window.localStorage.getItem("api.unbound.overview.timeperiodclients"));
}
if (window.localStorage.getItem("api.unbound.overview.logqchart") !== null) {
$("#toggle-log-qchart").prop('checked', window.localStorage.getItem("api.unbound.overview.logqchart") == 'true');
}
if (window.localStorage.getItem("api.unbound.overview.logcchart") !== null) {
$("#toggle-log-cchart").prop('checked', window.localStorage.getItem("api.unbound.overview.logcchart") == 'true');
}
}
$('#timeperiod').selectpicker('refresh');
$('#timeperiod-clients').selectpicker('refresh');
g_queryChart = create_chart($("#rollingChart"), 60, [], false);
g_clientChart = create_client_chart($("#rollingChartClient"), 60, [], false);
updateQueryChart($("#toggle-log-qchart")[0].checked);
updateClientChart($("#toggle-log-cchart")[0].checked);
create_or_update_totals();
});
return def;
}
$("#timeperiod").change(function() {
if (window.localStorage) {
window.localStorage.setItem("api.unbound.overview.timeperiod", $(this).val());
}
updateQueryChart($("#toggle-log-qchart")[0].checked);
});
$("#timeperiod-clients").change(function() {
if (window.localStorage) {
window.localStorage.setItem("api.unbound.overview.timeperiodclients", $(this).val());
}
updateClientChart($("#toggle-log-cchart")[0].checked);
});
$("#toggle-log-qchart").change(function() {
if (window.localStorage) {
window.localStorage.setItem("api.unbound.overview.logqchart", this.checked);
}
updateQueryChart(this.checked);
})
$("#toggle-log-cchart").change(function() {
if (window.localStorage) {
window.localStorage.setItem("api.unbound.overview.logcchart", this.checked);
}
updateClientChart(this.checked);
})
let blocklist_cb = function() {
$(this).remove("i").html('<i class="fa fa-spinner fa-spin"></i>');
let btn = $(this);
ajaxCall('/api/unbound/settings/updateBlocklist', {
'domain': $(this).data('value'),
'type': 'blocklists'
}, function(data, status) {
btn.addClass('whitelist-domain').removeClass('block-domain').remove("i").html('<i class="fa fa-pencil text-info"></i>');
btn.off('click').on('click', whitelist_cb);
// find all possible other elements containing this domain and update their classes
let elements = $("button[data-value='" + btn.data('value') + "']");
$.each(elements, function (key, value) {
let elem = $(value);
if(elem.hasClass("block-domain")) {
elem.addClass('whitelist-domain').removeClass('block-domain').remove("i").html('<i class="fa fa-pencil text-info"></i>');
// remove event binding and bind the whitelist_cb
elem.off('click').on('click', whitelist_cb);
}
});
reset_tooltips();
});
};
let whitelist_cb = function() {
$(this).remove("i").html('<i class="fa fa-spinner fa-spin"></i>');
let btn = $(this);
ajaxCall('/api/unbound/settings/updateBlocklist', {
'domain': $(this).data('value'),
'type': 'whitelists'
}, function(data, status) {
btn.addClass('block-domain').removeClass('whitelist-domain').remove("i").html('<i class="fa fa-ban text-danger"></i>');
btn.off('click').on('click', blocklist_cb);
// find all possible other elements containing this domain and update their classes
let elements = $("button[data-value='" + btn.data('value') + "']");
$.each(elements, function (key, value) {
let elem = $(value);
if(elem.hasClass("whitelist-domain")) {
elem.addClass('block-domain').removeClass('whitelist-domain').remove("i").html('<i class="fa fa-ban text-danger"></i>');
// remove event binding and bind the blocklist_cb
elem.off('click').on('click', blocklist_cb);
}
});
reset_tooltips();
});
}
$(document).on('click', '.block-domain', blocklist_cb);
$(document).on('click', '.whitelist-domain', whitelist_cb);
do_startup().done(function() {
$('.wrapper').show();
}).fail(function() {
$('.wrapper').hide();
$('#info').show();
});
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
if (e.target.id == 'query_details_tab') {
$("#grid-queries").bootgrid('destroy');
ajaxGet('/api/unbound/overview/isBlockListEnabled', {}, function(bl_enabled, status) {
/* Map the command type (block/whitelist) to the current state of the assigned action as determined by the controller,
* except for cases where they are manually overridden in the Blocklist page (Block/Whitelist Domains).
*/
let whitelisted_domains = null;
let blocklisted_domains = null;
let grid_queries = $("#grid-queries").UIBootgrid({
search:'/api/unbound/overview/searchQueries/',
options: {
rowSelect: false,
multiSelect: false,
selection: false,
useRequestHandlerOnGet: true,
requestHandler: function(request) {
if (g_clientFilter != null && g_timeFilter != null) {
let timestamp = g_timeFilter / 1000;
let interval = $("#timeperiod-clients").val() == 1 ? 60 : 600;
request['client'] = g_clientFilter;
request['timeStart'] = timestamp;
request['timeEnd'] = timestamp + interval;
}
return request;
},
responseHandler: function (response) {
whitelisted_domains = new Set(response.whitelisted_domains);
blocklisted_domains = new Set(response.blocklisted_domains);
return response;
},
formatters: {
"timeformatter": function (column, row) {
return moment.unix(row.time).local().format('YYYY-MM-DD HH:mm:ss');
},
"resolveformatter": function (column, row) {
return row.resolve_time_ms + 'ms';
},
"domain": function (column, row) {
return row.domain;
},
"commands": function (column, row) {
if (bl_enabled.enabled == 0) {
return '';
}
// strip off trailing dot
let domain = row.domain.replace(/\.$/, "");
let btn = '';
let block = '<button type="button" class="btn-secondary block-domain" data-value=' + row.domain + ' data-toggle="tooltip"><i class="fa fa-ban text-danger"></i></button> ';
let pass = '<button type="button" class="btn-secondary whitelist-domain" data-value=' + row.domain + ' data-toggle="tooltip"><i class="fa fa-pencil text-info"></i></button>';
if (row.action == 'Pass') {
btn = block;
} else if (row.action == 'Block') {
btn = pass;
}
if (whitelisted_domains.has(domain)) {
btn = block;
}
if (blocklisted_domains.has(domain)) {
btn = pass;
}
return btn;
},
},
statusMapping: {
0: "query-success",
1: "query-info",
2: "query-warning",
3: "query-danger",
4: "query-error"
}
}
}).on("loaded.rs.jquery.bootgrid", function (e) {
if (g_clientFilter != null && g_timeFilter != null && !$('#searchFilter').length) {
// Add a badge to signify we're in a drill-down
let label = (typeof g_labelFilter != 'undefined') ? g_labelFilter : g_clientFilter;
$('div.actionBar').prepend($('<div id="searchFilter"></div>'));
let timeStart = moment.unix(g_timeFilter / 1000).local().format('MM-DD HH:mm');
let interval = $("#timeperiod-clients").val() == 1 ? 60 : 600;
let timeEnd = moment.unix((g_timeFilter / 1000) + interval).local().format('MM-DD HH:mm');
$('#searchFilter').append('<span class="tag badge badge-pill badge-secondary">' +
label + ' (' + timeStart + ' - ' + timeEnd + ')' +
'<a id="removeFilter"><i class="fa fa-times" aria-hidden="true"></i></span></a>');
$('#removeFilter').click(function(e) {
// Reset filters set by a client drill-down
g_clientFilter = null;
g_timeFilter = null;
g_labelFilter = null;
$('#searchFilter').remove();
$('#grid-queries').bootgrid('reload');
})
}
if (bl_enabled.enabled == 0) {
$(".hide-col").css("display", "none");
} else {
$(".hide-col").css('display', '');
}
$(".domain-content").tooltip({placement: "auto left"});
reset_tooltips();
grid_queries.find(".block-domain").on('click', blocklist_cb);
grid_queries.find(".whitelist-domain").on('click', whitelist_cb);
});
})
}
if (e.target.id == 'query_overview_tab') {
// Reset filters set by a client drill-down
g_clientFilter = null;
g_timeFilter = null;
g_labelFilter = null;
create_or_update_totals();
updateQueryChart($("#toggle-log-qchart")[0].checked);
updateClientChart($("#toggle-log-cchart")[0].checked);
}
});
});
</script>
<div id="info" class="alert alert-warning" role="alert">
{{ lang._('Local gathering of statistics is not enabled. Enable it in Reporting Settings page.') }}
<br />
<a href="/reporting_settings.php">{{ lang._('Go to the Reporting configuration') }}</a>
</div>
<div class="wrapper">
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs" style="border-bottom: none">
<li class="active"><a data-toggle="tab" href="#query-overview" id="query_overview_tab">{{ lang._('Overview') }}</a></li>
<li><a data-toggle="tab" href="#query-details" id="query_details_tab">{{ lang._('Details') }}</a></li>
</ul>
<div class="tab-content content-box">
<div id="query-overview" class="tab-pane fade in active">
<div class="content-box" style="margin-bottom: 10px;">
<div id="counters" class="container-fluid">
<div class="col-md-12">
<h3 id="bannersub"></h3>
</div>
<div class="row" style="margin-bottom: 20px; margin-top: 20px;">
<div class="banner col-xs-3 justify-content-center">
<div class="stats-element">
<div class="stats-icon">
<i class="large-icon fa fa-cogs text-success" aria-hidden="true"></i>
</div>
<div class="stats-text">
<h2 id="totalCounter" class="stats-counter-text"></h2>
<p class="stats-inner-text">{{ lang._('Total')}}</p>
</div>
</div>
</div>
<div class="banner col-xs-3 justify-content-center">
<div class="stats-element">
<div class="stats-icon">
<i class="large-icon fa fa-arrows-v text-info" aria-hidden="true"></i>
</div>
<div class="stats-text">
<h2 id="resolvedCounter" class="stats-counter-text"></h2>
<p class="stats-inner-text">{{ lang._('Resolved') }}</p>
</div>
</div>
</div>
<div class="banner col-xs-3 justify-content-center">
<div class="stats-element">
<div class="stats-icon">
<i class="large-icon fa fa-hand-paper-o text-danger" aria-hidden="true"></i>
</div>
<div class="stats-text">
<h2 id="blockedCounter" class="stats-counter-text"></h2>
<p class="stats-inner-text pull-right">{{ lang._('Blocked') }}</p>
</div>
</div>
</div>
<div class="banner col-xs-3 justify-content-center">
<div class="stats-element">
<div class="stats-icon">
<i class="large-icon fa fa-list text-primary" aria-hidden="true"></i>
</div>
<div class="stats-text">
<h2 id="sizeCounter" class="stats-counter-text"></h2>
<p class="stats-inner-text">{{ lang._('Size of blocklist') }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="content-box" style="margin-bottom: 10px;">
<div id="graph" class="container-fluid">
<div class="row justify-content-center" style="display: flex; flex-wrap: wrap;">
<div class="col-md-4"></div>
<div class="col-md-4 text-center" style="padding: 10px;">
<span id="qGraphTitle" style="padding: 5px;"><b>{{ lang._('Queries over the last ') }}</b></span>
<select class="selectpicker" id="timeperiod" data-width="auto">
<option value="24">{{ lang._('24 Hours') }}</option>
<option value="12">{{ lang._('12 Hours') }}</option>
<option value="1">{{ lang._('1 Hour') }}</option>
</select>
</div>
<div class="col-md-2"></div>
<div class="col-md-2">
<div class="vertical-center">
<label class="h-100" style="margin-right: 5px;">{{ lang._('Logarithmic') }}</label>
<input id="toggle-log-qchart" type="checkbox"></input>
</div>
</div>
</div>
<div class="row">
<div class="col-2"></div>
<div class="col-8">
<div class="chart-container">
<canvas id="rollingChart"></canvas>
</div>
</div>
<div class="col-2"></div>
</div>
</div>
</div>
<div class="content-box" style="margin-bottom: 10px;">
<div id="graph" class="container-fluid">
<div class="row justify-content-center" style="display: flex; flex-wrap: wrap;">
<div class="col-md-4"></div>
<div class="col-md-4 text-center" style="padding: 10px;">
<span id="cGraphTitle" style="padding: 5px;"><b>{{ lang._('Top 10 client activity over the last ') }}</b></span>
<select class="selectpicker" id="timeperiod-clients" data-width="auto">
<option value="24">{{ lang._('24 Hours') }}</option>
<option value="12">{{ lang._('12 Hours') }}</option>
<option value="1">{{ lang._('1 Hour') }}</option>
</select>
</div>
<div class="col-md-2"></div>
<div class="col-md-2">
<div class="vertical-center">
<label class="h-100" style="margin-right: 5px;">Logarithmic</label>
<input id="toggle-log-cchart" type="checkbox"></input>
</div>
</div>
</div>
<div class="row">
<div class="col-2"></div>
<div class="col-8">
<div class="chart-container">
<canvas id="rollingChartClient"></canvas>
</div>
</div>
<div class="col-2"></div>
</div>
</div>
</div>
<div class="content-box">
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<div class="top-list">
<ul class="list-group list-group-wrapper" id="top">
<li class="list-group-item list-group-item-border list-item-domain">
<b>{{ lang._('Top passed domains') }}</b>
</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="top-list">
<ul class="list-group list-group-wrapper" id="top-blocked">
<li class="list-group-item list-group-item-border list-item-domain">
<b>{{ lang._('Top blocked domains') }}</b>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="query-details" class="tab-pane fade in">
<table id="grid-queries" class="table table-condensed">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="status" data-type="numeric" data-visible="false" data-formatter="statusformatter">{{ lang._('status') }}</th>
<th data-column-id="time" data-type="string" data-formatter="timeformatter">{{ lang._('Time') }}</th>
<th data-column-id="client" data-type="string">{{ lang._('Client') }}</th>
<th data-column-id="family" data-width="6em" data-visible="false" data-type="string">{{ lang._('Family') }}</th>
<th data-column-id="type" data-width="6em" data-type="string">{{ lang._('Type') }}</th>
<th data-column-id="domain" data-formatter="domain" data-type="string">{{ lang._('Domain') }}</th>
<th data-column-id="action" data-width="6em" data-type="string">{{ lang._('Action') }}</th>
<th data-column-id="source" data-type="string">{{ lang._('Source') }}</th>
<th data-column-id="rcode" data-type="string">{{ lang._('Return Code') }}</th>
<th data-column-id="resolve_time_ms" data-type="string" data-formatter="resolveformatter">{{ lang._('Resolve time') }}</th>
<th data-column-id="ttl" data-width="6em" data-type="string">{{ lang._('TTL') }}</th>
<th data-column-id="blocklist" data-type="string">{{ lang._('Blocklist') }}</th>
<th data-header-css-class="hide-col" data-css-class="hide-col" data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Command') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</table>
</div>
</div>
</div>