Split the server statistics and configuration feature into separate pages.

This commit is contained in:
Matt Scott 2023-02-19 15:04:30 -05:00
parent 55e4f5f829
commit 65bfc53acb
5 changed files with 704 additions and 604 deletions

View File

@ -4,7 +4,8 @@ import traceback
import re
from base64 import b64encode
from ast import literal_eval
from flask import Blueprint, render_template, render_template_string, make_response, url_for, current_app, request, redirect, jsonify, abort, flash, session
from flask import Blueprint, render_template, render_template_string, make_response, url_for, current_app, request, \
redirect, jsonify, abort, flash, session
from flask_login import login_required, current_user
from ..decorators import operator_role_required, admin_role_required, history_access_required
@ -44,6 +45,8 @@ change_type: "addition" or "deletion" or "status" for status change or "unchange
Note: A change in "content", is considered a deletion and recreation of the same record,
holding the new content value.
"""
def get_record_changes(del_rrset, add_rrset):
changeSet = []
delSet = del_rrset['records'] if 'records' in del_rrset else []
@ -82,14 +85,15 @@ def get_record_changes(del_rrset, add_rrset):
exists = True
break
if not exists:
changeSet.append( ( {"disabled":a['disabled'], "content":a['content']}, {"disabled":a['disabled'], "content":a['content']}, "unchanged") )
changeSet.append(({"disabled": a['disabled'], "content": a['content']},
{"disabled": a['disabled'], "content": a['content']}, "unchanged"))
return changeSet
# out_changes is a list of HistoryRecordEntry objects in which we will append the new changes
# a HistoryRecordEntry represents a pair of add_rrset and del_rrset
def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_num, record_name=None, record_type=None):
if history_entry.detail is None:
return
@ -101,7 +105,6 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n
add_rrsets = detail_dict['add_rrsets']
del_rrsets = detail_dict['del_rrsets']
for add_rrset in add_rrsets:
exists = False
for del_rrset in del_rrsets:
@ -114,7 +117,8 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n
if not exists: # this is a new record
if change_num not in out_changes:
out_changes[change_num] = []
out_changes[change_num].append(HistoryRecordEntry(history_entry, [], add_rrset, "+")) # (add_rrset, del_rrset, change_type)
out_changes[change_num].append(
HistoryRecordEntry(history_entry, [], add_rrset, "+")) # (add_rrset, del_rrset, change_type)
for del_rrset in del_rrsets:
exists = False
for add_rrset in add_rrsets:
@ -126,7 +130,6 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n
out_changes[change_num] = []
out_changes[change_num].append(HistoryRecordEntry(history_entry, del_rrset, [], "-"))
# only used for changelog per record
if record_name != None and record_type != None: # then get only the records with the specific (record_name, record_type) tuple
if change_num in out_changes:
@ -134,15 +137,16 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n
else:
return
for hre in changes_i: # for each history record entry in changes_i
if 'type' in hre.add_rrset and hre.add_rrset['name'] == record_name and hre.add_rrset['type'] == record_type:
if 'type' in hre.add_rrset and hre.add_rrset['name'] == record_name and hre.add_rrset[
'type'] == record_type:
continue
elif 'type' in hre.del_rrset and hre.del_rrset['name'] == record_name and hre.del_rrset['type'] == record_type:
elif 'type' in hre.del_rrset and hre.del_rrset['name'] == record_name and hre.del_rrset[
'type'] == record_type:
continue
else:
out_changes[change_num].remove(hre)
# records with same (name,type) are considered as a single HistoryRecordEntry
# history_entry is of type History - used to extract created_by and created_on
# add_rrset is a dictionary of replace
@ -158,7 +162,6 @@ class HistoryRecordEntry:
self.changed_fields = [] # contains a subset of : [ttl, name, type]
self.changeSet = [] # all changes for the records of this add_rrset-del_rrset pair
if change_type == "+": # addition
self.changed_fields.append("name")
self.changed_fields.append("type")
@ -175,8 +178,6 @@ class HistoryRecordEntry:
self.changed_fields.append("ttl")
self.changeSet = get_record_changes(del_rrset, add_rrset)
def toDict(self):
return {
"add_rrset": self.add_rrset,
@ -191,6 +192,7 @@ class HistoryRecordEntry:
def __eq__(self, obj2): # used for removal of objects from a list
return True if obj2.toDict() == self.toDict() else False
@admin_bp.before_request
def before_request():
# Manage session timeout
@ -202,11 +204,10 @@ def before_request():
session.modified = True
@admin_bp.route('/pdns', methods=['GET'])
@admin_bp.route('/server/statistics', methods=['GET'])
@login_required
@operator_role_required
def pdns_stats():
def server_statistics():
if not Setting().get('pdns_api_url') or not Setting().get(
'pdns_api_key') or not Setting().get('pdns_version'):
return redirect(url_for('admin.setting_pdns'))
@ -215,7 +216,6 @@ def pdns_stats():
users = User.query.all()
server = Server(server_id='localhost')
configs = server.get_config()
statistics = server.get_statistic()
history_number = History.query.count()
@ -226,12 +226,33 @@ def pdns_stats():
else:
uptime = 0
return render_template('admin_pdns_stats.html',
return render_template('admin_server_statistics.html',
domains=domains,
users=users,
statistics=statistics,
uptime=uptime,
history_number=history_number)
@admin_bp.route('/server/configuration', methods=['GET'])
@login_required
@operator_role_required
def server_configuration():
if not Setting().get('pdns_api_url') or not Setting().get(
'pdns_api_key') or not Setting().get('pdns_version'):
return redirect(url_for('admin.setting_pdns'))
domains = Domain.query.all()
users = User.query.all()
server = Server(server_id='localhost')
configs = server.get_config()
history_number = History.query.count()
return render_template('admin_server_configuration.html',
domains=domains,
users=users,
configs=configs,
statistics=statistics,
uptime=uptime,
history_number=history_number)
@ -296,6 +317,7 @@ def edit_user(user_username=None):
create=create,
error=result['msg'])
@admin_bp.route('/key/edit/<key_id>', methods=['GET', 'POST'])
@admin_bp.route('/key/edit', methods=['GET', 'POST'])
@login_required
@ -381,6 +403,7 @@ def edit_key(key_id=None):
create=create,
plain_key=plain_key)
@admin_bp.route('/manage-keys', methods=['GET', 'POST'])
@login_required
@operator_role_required
@ -427,6 +450,7 @@ def manage_keys():
'msg': 'Key has been removed.'
}), 200)
@admin_bp.route('/manage-user', methods=['GET', 'POST'])
@login_required
@operator_role_required
@ -800,7 +824,9 @@ class DetailedHistory():
</table>
""",
domaintype=detail_dict['domain_type'],
account=Account.get_name_by_id(self=None, account_id=detail_dict['account_id']) if detail_dict['account_id'] != "0" else "None")
account=Account.get_name_by_id(self=None, account_id=detail_dict[
'account_id']) if detail_dict[
'account_id'] != "0" else "None")
elif 'authenticator' in detail_dict: # this is a user authentication
self.detailed_msg = render_template_string("""
@ -825,9 +851,11 @@ class DetailedHistory():
</tbody>
</table>
""",
background_rgba="68,157,68" if detail_dict['success'] == 1 else "201,48,44",
background_rgba="68,157,68" if detail_dict[
'success'] == 1 else "201,48,44",
username=detail_dict['username'],
auth_result="success" if detail_dict['success'] == 1 else "failure",
auth_result="success" if detail_dict[
'success'] == 1 else "failure",
authenticator=detail_dict['authenticator'],
ip_address=detail_dict['ip_address'])
@ -844,7 +872,8 @@ class DetailedHistory():
</table>
""",
template_name=DetailedHistory.get_key_val(detail_dict, "name"),
description=DetailedHistory.get_key_val(detail_dict, "description"))
description=DetailedHistory.get_key_val(detail_dict,
"description"))
elif 'Change domain' in history.msg and 'access control' in history.msg: # added or removed a user from a domain
users_with_access = DetailedHistory.get_key_val(detail_dict, "user_has_access")
@ -868,9 +897,12 @@ class DetailedHistory():
""",
keyname=DetailedHistory.get_key_val(detail_dict, "key"),
rolename=DetailedHistory.get_key_val(detail_dict, "role"),
description=DetailedHistory.get_key_val(detail_dict, "description"),
linked_domains=DetailedHistory.get_key_val(detail_dict, "domains" if "domains" in detail_dict else "domain_acl"),
linked_accounts=DetailedHistory.get_key_val(detail_dict, "accounts"))
description=DetailedHistory.get_key_val(detail_dict,
"description"),
linked_domains=DetailedHistory.get_key_val(detail_dict,
"domains" if "domains" in detail_dict else "domain_acl"),
linked_accounts=DetailedHistory.get_key_val(detail_dict,
"accounts"))
elif 'Delete API key' in history.msg:
self.detailed_msg = render_template_string("""
@ -883,8 +915,10 @@ class DetailedHistory():
""",
keyname=DetailedHistory.get_key_val(detail_dict, "key"),
rolename=DetailedHistory.get_key_val(detail_dict, "role"),
description=DetailedHistory.get_key_val(detail_dict, "description"),
linked_domains=DetailedHistory.get_key_val(detail_dict, "domains"))
description=DetailedHistory.get_key_val(detail_dict,
"description"),
linked_domains=DetailedHistory.get_key_val(detail_dict,
"domains"))
elif 'Update type for domain' in history.msg:
self.detailed_msg = render_template_string("""
@ -905,8 +939,10 @@ class DetailedHistory():
<tr><td>Domain Master IPs:</td><td>{{ domain_master_ips }}</td></tr>
</table>
""",
domain_type=DetailedHistory.get_key_val(detail_dict, "domain_type"),
domain_master_ips=DetailedHistory.get_key_val(detail_dict, "domain_master_ips"))
domain_type=DetailedHistory.get_key_val(detail_dict,
"domain_type"),
domain_master_ips=DetailedHistory.get_key_val(detail_dict,
"domain_master_ips"))
elif DetailedHistory.get_key_val(detail_dict, 'msg') and DetailedHistory.get_key_val(detail_dict, 'status'):
self.detailed_msg = render_template_string('''
@ -915,7 +951,8 @@ class DetailedHistory():
<tr><td>Message:</td><td>{{ history_msg }}</td></tr>
</table>
''',
history_status=DetailedHistory.get_key_val(detail_dict, 'status'),
history_status=DetailedHistory.get_key_val(detail_dict,
'status'),
history_msg=DetailedHistory.get_key_val(detail_dict, 'msg'))
elif 'Update domain' in history.msg and 'associate account' in history.msg: # When an account gets associated or dissociate with domains
@ -925,8 +962,10 @@ class DetailedHistory():
<tr><td>Dissociate:</td><td>{{ history_dissoc_account }}</td></tr>
</table>
''',
history_assoc_account=DetailedHistory.get_key_val(detail_dict, 'assoc_account'),
history_dissoc_account=DetailedHistory.get_key_val(detail_dict, 'dissoc_account'))
history_assoc_account=DetailedHistory.get_key_val(detail_dict,
'assoc_account'),
history_dissoc_account=DetailedHistory.get_key_val(detail_dict,
'dissoc_account'))
# check for lower key as well for old databases
@staticmethod
@ -952,6 +991,7 @@ def convert_histories(histories):
detailedHistories.append(DetailedHistory(histories[i], None))
return detailedHistories
@admin_bp.route('/history', methods=['GET', 'POST'])
@login_required
@history_access_required
@ -989,7 +1029,6 @@ def history():
'msg': 'Can not remove histories.'
}), 500)
if request.method == 'GET':
doms = accounts = users = ""
if current_user.role.name in ['Administrator', 'Operator']:
@ -997,8 +1036,6 @@ def history():
all_account_names = Account.query.all()
all_user_names = User.query.all()
for d in all_domain_names:
doms += d.name + " "
for acc in all_account_names:
@ -1026,7 +1063,6 @@ def history():
AccountUser.user_id == current_user.id
)).all()
all_user_names = []
for a in all_account_names:
temp = db.session.query(User) \
@ -1051,7 +1087,9 @@ def history():
accounts += a.name + " "
for u in all_user_names:
users += u.username + " "
return render_template('admin_history.html', all_domain_names=doms, all_account_names=accounts, all_usernames=users)
return render_template('admin_history.html', all_domain_names=doms, all_account_names=accounts,
all_usernames=users)
# local_offset is the offset of the utc to the local time
# offset must be int
@ -1059,9 +1097,11 @@ def history():
def from_utc_to_local(local_offset, timeframe):
offset = str(local_offset * (-1))
date_split = str(timeframe).split(".")[0]
date_converted = datetime.datetime.strptime(date_split, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(minutes=int(offset))
date_converted = datetime.datetime.strptime(date_split, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(
minutes=int(offset))
return date_converted
@admin_bp.route('/history_table', methods=['GET', 'POST'])
@login_required
@history_access_required
@ -1115,28 +1155,35 @@ def history_table(): # ajax call data
))
domain_name = request.args.get('domain_name_filter') if request.args.get('domain_name_filter') != None \
and len(request.args.get('domain_name_filter')) != 0 else None
and len(
request.args.get('domain_name_filter')) != 0 else None
account_name = request.args.get('account_name_filter') if request.args.get('account_name_filter') != None \
and len(request.args.get('account_name_filter')) != 0 else None
and len(
request.args.get('account_name_filter')) != 0 else None
user_name = request.args.get('auth_name_filter') if request.args.get('auth_name_filter') != None \
and len(request.args.get('auth_name_filter')) != 0 else None
min_date = request.args.get('min') if request.args.get('min') != None and len( request.args.get('min')) != 0 else None
min_date = request.args.get('min') if request.args.get('min') != None and len(
request.args.get('min')) != 0 else None
if min_date != None: # get 1 day earlier, to check for timezone errors
min_date = str(datetime.datetime.strptime(min_date, '%Y-%m-%d') - datetime.timedelta(days=1))
max_date = request.args.get('max') if request.args.get('max') != None and len( request.args.get('max')) != 0 else None
max_date = request.args.get('max') if request.args.get('max') != None and len(
request.args.get('max')) != 0 else None
if max_date != None: # get 1 day later, to check for timezone errors
max_date = str(datetime.datetime.strptime(max_date, '%Y-%m-%d') + datetime.timedelta(days=1))
tzoffset = request.args.get('tzoffset') if request.args.get('tzoffset') != None and len(request.args.get('tzoffset')) != 0 else None
tzoffset = request.args.get('tzoffset') if request.args.get('tzoffset') != None and len(
request.args.get('tzoffset')) != 0 else None
changed_by = request.args.get('user_name_filter') if request.args.get('user_name_filter') != None \
and len(request.args.get('user_name_filter')) != 0 else None
and len(
request.args.get('user_name_filter')) != 0 else None
"""
Auth methods: LOCAL, Github OAuth, Azure OAuth, SAML, OIDC OAuth, Google OAuth
"""
auth_methods = []
if (request.args.get('auth_local_only_checkbox') is None \
and request.args.get('auth_oauth_only_checkbox') is None \
and request.args.get('auth_saml_only_checkbox') is None and request.args.get('auth_all_checkbox') is None):
and request.args.get('auth_saml_only_checkbox') is None and request.args.get(
'auth_all_checkbox') is None):
auth_methods = []
if request.args.get('auth_all_checkbox') == "on":
auth_methods.append("")
@ -1152,9 +1199,6 @@ def history_table(): # ajax call data
else:
changelog_only = False
# users cannot search for authentication
if user_name != None and current_user.role.name not in ['Administrator', 'Operator']:
histories = []
@ -1165,8 +1209,11 @@ def history_table(): # ajax call data
.filter(
db.and_(
db.or_(
History.msg.like("%domain "+ domain_name) if domain_name != "*" else History.msg.like("%domain%"),
History.msg.like("%domain "+ domain_name + " access control") if domain_name != "*" else History.msg.like("%domain%access control")
History.msg.like("%domain " + domain_name) if domain_name != "*" else History.msg.like(
"%domain%"),
History.msg.like(
"%domain " + domain_name + " access control") if domain_name != "*" else History.msg.like(
"%domain%access control")
),
History.created_on <= max_date if max_date != None else True,
History.created_on >= min_date if min_date != None else True,
@ -1214,14 +1261,19 @@ def history_table(): # ajax call data
)
).order_by(History.created_on.desc()) \
.limit(lim).all()
elif user_name != None and current_user.role.name in [ 'Administrator', 'Operator']: # only admins can see the user login-logouts
elif user_name != None and current_user.role.name in ['Administrator',
'Operator']: # only admins can see the user login-logouts
histories = History.query \
.filter(
db.and_(
db.or_(
History.msg.like("User "+ user_name + " authentication%") if user_name != "*" and user_name != None else History.msg.like("%authentication%"),
History.msg.like("User "+ user_name + " was not authorized%") if user_name != "*" and user_name != None else History.msg.like("User%was not authorized%")
History.msg.like(
"User " + user_name + " authentication%") if user_name != "*" and user_name != None else History.msg.like(
"%authentication%"),
History.msg.like(
"User " + user_name + " was not authorized%") if user_name != "*" and user_name != None else History.msg.like(
"User%was not authorized%")
),
History.created_on <= max_date if max_date != None else True,
History.created_on >= min_date if min_date != None else True,
@ -1236,7 +1288,8 @@ def history_table(): # ajax call data
temp.append(h)
break
histories = temp
elif (changed_by != None or max_date != None) and current_user.role.name in [ 'Administrator', 'Operator'] : # select changed by and date filters only
elif (changed_by != None or max_date != None) and current_user.role.name in ['Administrator',
'Operator']: # select changed by and date filters only
histories = History.query \
.filter(
db.and_(
@ -1246,7 +1299,8 @@ def history_table(): # ajax call data
)
) \
.order_by(History.created_on.desc()).limit(lim).all()
elif (changed_by != None or max_date != None): # special filtering for user because one user does not have access to log-ins logs
elif (
changed_by != None or max_date != None): # special filtering for user because one user does not have access to log-ins logs
histories = base_query \
.filter(
db.and_(
@ -1289,14 +1343,15 @@ def history_table(): # ajax call data
max_date_split = max_date.split()[0]
for i, history_rec in enumerate(detailedHistories):
local_date = str(from_utc_to_local(int(tzoffset), history_rec.history.created_on).date())
if (min_date != None and local_date == min_date_split) or (max_date != None and local_date == max_date_split):
if (min_date != None and local_date == min_date_split) or (
max_date != None and local_date == max_date_split):
detailedHistories[i] = None
# Remove elements previously flagged as None
detailedHistories = [h for h in detailedHistories if h is not None]
return render_template('admin_history_table.html', histories=detailedHistories, len_histories=len(detailedHistories), lim=lim)
return render_template('admin_history_table.html', histories=detailedHistories,
len_histories=len(detailedHistories), lim=lim)
@admin_bp.route('/setting/basic', methods=['GET'])
@ -1542,7 +1597,6 @@ def setting_authentication():
Setting().set('purge', True
if request.form.get('purge') == 'ON' else False)
result = {'status': True, 'msg': 'Saved successfully'}
elif conf_type == 'google':
google_oauth_enabled = True if request.form.get(
@ -2003,6 +2057,7 @@ def global_search():
return render_template('admin_global_search.html', domains=domains, records=records, comments=comments)
def validateURN(value):
NID_PATTERN = re.compile(r'^[0-9a-z][0-9a-z-]{1,31}$', flags=re.IGNORECASE)
NSS_PCHAR = '[a-z0-9-._~]|%[a-f0-9]{2}|[!$&\'()*+,;=]|:|@'

View File

@ -1,127 +0,0 @@
{% extends "base.html" %}
{% set active_page = "admin_console" %}
{% block title %}
<title>
Admin Console - {{ SITE_NAME }}
</title>
{% endblock %}
{% block dashboard_stat %}
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0 text-dark">
PowerDNS
<small>Server Statistics & Configuration</small>
</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item active">PowerDNS Server Statistics & Configuration</li>
</ol>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header">
<h3 class="card-title">PowerDNS Server Statistics</h3>
</div>
<div class="card-body">
<table id="tbl_statistics" class="table table-bordered table-striped">
<thead>
<tr>
<th width="30%">Statistic</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for statistic in statistics %}
<tr class="odd gradeX">
<td>
<a href="https://doc.powerdns.com/authoritative/search.html?q={{ statistic['name'] }}"
target="_blank" class="btn btn-primary">
<i class="fa fa-search"></i>&nbsp;{{ statistic['name'] }}
</a>
</td>
<td>{{ statistic['value'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header">
<h3 class="card-title">PowerDNS Server Configuration</h3>
</div>
<div class="card-body">
<table id="tbl_configuration" class="table table-bordered table-striped">
<thead>
<tr>
<th width="30%">Configuration</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for config in configs %}
<tr class="odd gradeX">
<td>
<a href="https://doc.powerdns.com/authoritative/search.html?q={{ config['name'] }}"
target="_blank" class="btn btn-primary">
<i class="fa-solid fa-search"></i>&nbsp;{{ config['name'] }}
</a>
</td>
<td>
{{ config['value'] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
// set up statistics data table
$("#tbl_statistics").DataTable({
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
});
// set up configuration data table
$("#tbl_configuration").DataTable({
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
});
</script>
{% endblock %}

View File

@ -0,0 +1,84 @@
{% extends "base.html" %}
{% set active_page = "server_configuration" %}
{% block title %}<title>Server Configuration - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0 text-dark">
Server Configuration
</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item active">Server Configuration</li>
</ol>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header">
<h3 class="card-title">Server Configuration</h3>
</div>
<!-- /.card-header -->
<div class="card-body table-responsive">
<table id="tbl_configuration" class="table table-bordered table-striped table-hover table-sm">
<thead>
<tr>
<th>Configuration</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for config in configs %}
<tr class="odd gradeX">
<td>
<a href="https://doc.powerdns.com/authoritative/search.html?q={{ config['name'] }}"
target="_blank" class="btn btn-primary">
<i class="fa-solid fa-search"></i>&nbsp;{{ config['name'] }}
</a>
</td>
<td>
{{ config['value'] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</div>
<!-- /.container-fluid -->
</section>
{% endblock %}
{% block extrascripts %}
<script>
// Initialize DataTables
$("#tbl_configuration").DataTable({
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
});
</script>
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends "base.html" %}
{% set active_page = "server_statistics" %}
{% block title %}<title>Server Statistics - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0 text-dark">
Server Statistics
</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item active">Server Statistics</li>
</ol>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header">
<h3 class="card-title">Server Statistics</h3>
</div>
<!-- /.card-header -->
<div class="card-body table-responsive">
<table id="tbl_statistics" class="table table-bordered table-striped table-hover table-sm">
<thead>
<tr>
<th width="30%">Statistic</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for statistic in statistics %}
<tr class="odd gradeX">
<td>
<a href="https://doc.powerdns.com/authoritative/search.html?q={{ statistic['name'] }}"
target="_blank" class="btn btn-primary">
<i class="fa fa-search"></i>&nbsp;{{ statistic['name'] }}
</a>
</td>
<td>{{ statistic['value'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</div>
<!-- /.container-fluid -->
</section>
{% endblock %}
{% block extrascripts %}
<script>
// Initialize DataTables
$("#tbl_statistics").DataTable({
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
});
</script>
{% endblock %}

View File

@ -115,10 +115,16 @@
{% endif %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<li class="nav-header">Administration</li>
<li class="{{ 'nav-item active' if active_page == 'admin_console' else 'nav-item' }}">
<a href="{{ url_for('admin.pdns_stats') }}" class="nav-link">
<i class="nav-icon fa-solid fa-info-circle"></i>
<p>PowerDNS Info</p>
<li class="{{ 'nav-item active' if active_page == 'server_statistics' else 'nav-item' }}">
<a href="{{ url_for('admin.server_statistics') }}" class="nav-link">
<i class="nav-icon fa-solid fa-chart-simple"></i>
<p>Server Statistics</p>
</a>
</li>
<li class="{{ 'nav-item active' if active_page == 'server_configuration' else 'nav-item' }}">
<a href="{{ url_for('admin.server_configuration') }}" class="nav-link">
<i class="nav-icon fa-solid fa-cog"></i>
<p>Server Configuration</p>
</a>
</li>
<li class="{{ 'nav-item active' if active_page == 'admin_global_search' else 'nav-item' }}">