mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2025-06-14 12:06:06 +00:00
History Tab Overhaul & Domain Record Modifications Changelog (#1042)
Co-authored-by: Konstantinos Kouris <85997752+konkourgr@users.noreply.github.com> Co-authored-by: vmarkop <billy.mark.b.m.10@gmail.com> Co-authored-by: KostasMparmparousis <mparmparousis.kostas@gmail.com> Co-authored-by: dimpapac <demispapa@gmail.com>
This commit is contained in:
@ -13,7 +13,12 @@
|
||||
<li class="active">History</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %} {% block content %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||
|
||||
|
||||
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
@ -28,32 +33,134 @@
|
||||
Clear History <i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<table id="tbl_history" class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Changed by</th>
|
||||
<th>Content</th>
|
||||
<th>Time</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for history in histories %}
|
||||
<tr class="odd gradeX">
|
||||
<td>{{ history.created_by }}</td>
|
||||
<td>{{ history.msg }}</td>
|
||||
<td>{{ history.created_on }}</td>
|
||||
<td width="6%">
|
||||
<button type="button" class="btn btn-flat btn-primary history-info-button"
|
||||
value='{{ history.detail }}'>Info <i class="fa fa-info"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="box-body clearfix">
|
||||
<form id="history-search-form" autocomplete="off">
|
||||
<!-- Custom Tabs -->
|
||||
<div class="nav-tabs-custom" id="tabs">
|
||||
<ul class="nav nav-tabs" id="nav_nav_tabs" name="nav_nav_tabs">
|
||||
<li id="activity_tab" class="active"><a href="#tabs-act" data-toggle="tab">Search for All Activity</a></li>
|
||||
<li id="domain_tab"><a href="#tabs-domain" data-toggle="tab">Search By Domain</a></li>
|
||||
<li id="account_tab"><a href="#tabs-account" data-toggle="tab">Search By Account</a></li>
|
||||
{% if current_user.role.name != 'User' %}
|
||||
<li id="user_auth_tab"><a href="#tabs-auth" data-toggle="tab">Search for User Authentication</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane" id="tabs-act">
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-domain">
|
||||
<td><label>Domain Name</label></td>
|
||||
<td>
|
||||
<div class="autocomplete" style="width:250px;">
|
||||
<input type="text" class="form-control" id="domain_name_filter" name="domain_name_filter" placeholder="Enter * to search for any domain" value="">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="position: relative; top:10px;">
|
||||
<td>Record Changelog only  </td>
|
||||
<td>
|
||||
<input type="checkbox" id="domain_changelog_only_checkbox" name="domain_changelog_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-account">
|
||||
<td><label>Account Name</label></td>
|
||||
<td>
|
||||
<div class="autocomplete" style="width:250px;">
|
||||
<input type="text" class="form-control" id="account_name_filter" name="account_name_filter" placeholder="Enter * to search for any account" value="">
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-auth">
|
||||
<td><label>Username</label></td>
|
||||
<td>
|
||||
<div class="autocomplete" style="width:250px;">
|
||||
<input type="text" class="form-control" id="auth_name_filter" name="auth_name_filter" placeholder="Enter * to search for any username" value="">
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div style="position: relative; top:10px;">
|
||||
<td>Authenticator Types:  </td>
|
||||
<td>  All</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_all_checkbox" name="auth_all_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
<td>  LOCAL</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_local_only_checkbox" name="auth_local_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
<td>  OAuth</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_oauth_only_checkbox" name="auth_oauth_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
<td>  SAML</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_saml_only_checkbox" name="auth_saml_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Custom Tabs -->
|
||||
|
||||
<div class="box-body">
|
||||
<table id="Filters-Table">
|
||||
<thead>
|
||||
<th>Filters</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label>Changed by:  </label></td>
|
||||
<td>
|
||||
<div class="autocomplete" style="width:250px;">
|
||||
<input type="text" style=" border:1px solid #d2d6de; width:250px; height: 34px;" id="user_name_filter" name="user_name_filter" value="">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="position: relative; top:10px;">
|
||||
<label>Minimum date:  </label>
|
||||
</td>
|
||||
<td style="position: relative; top:10px;">
|
||||
<input type="text" id="min" name="min" class="datepicker" autocomplete="off" style=" border:1px solid #d2d6de; width:250px; height: 34px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="position: relative; top:20px;">
|
||||
<label>Maximum date:  </label>
|
||||
</td>
|
||||
<td style="position: relative; top:20px;">
|
||||
<input type="text" id="max" name="max" class="datepicker" autocomplete="off" style=" border:1px solid #d2d6de; width:250px; height: 34px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td> </td></tr>
|
||||
<tr><td> </td></tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button type="submit" id="search-submit" name="search-submit" class="btn btn-flat btn-primary button-filter">Search <i class="fa fa-search"></i></button>
|
||||
</td>
|
||||
<td>
|
||||
<!-- -->
|
||||
<button id="clear-filters" name="clear-filters" class="btn btn-flat btn-warning button-clearf">Clear Filters <i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="table_from_ajax"></div>
|
||||
|
||||
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
@ -65,31 +172,302 @@
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
// set up history data table
|
||||
$("#tbl_history").DataTable({
|
||||
"paging": true,
|
||||
"lengthChange": false,
|
||||
"searching": true,
|
||||
"ordering": true,
|
||||
"info": true,
|
||||
"autoWidth": false,
|
||||
"order": [
|
||||
[2, "desc"]
|
||||
],
|
||||
"columnDefs": [{
|
||||
"type": "time",
|
||||
"render": function (data, type, row) {
|
||||
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
/* Don't let user search with a blank main field */
|
||||
var canSearch=true;
|
||||
|
||||
$(document).ready(function () {
|
||||
$.ajax({
|
||||
url: "/admin/history_table",
|
||||
type: "get",
|
||||
success: function(response) {
|
||||
console.log('Submission was successful.');
|
||||
$("#table_from_ajax").html(response);
|
||||
},
|
||||
"targets": 2
|
||||
}]
|
||||
error: function(xhr) {
|
||||
console.log("Sending data: ", data, " failed")
|
||||
}
|
||||
});
|
||||
|
||||
var minDate = $('#min');
|
||||
var maxDate = $('#max');
|
||||
domain_changelog = $('domain_changelog_only_checkbox');
|
||||
|
||||
// Show/hide filters
|
||||
$('#domain_name_filter, #account_name_filter, #auth_name_filter').on('keyup change', function (e) {
|
||||
if ( $('#domain_name_filter').val() == "" && $('#account_name_filter').val() == "" && $('#auth_name_filter').val() == "")
|
||||
canSearch=false;
|
||||
else
|
||||
canSearch=true;
|
||||
});
|
||||
|
||||
// Handle giving later mindate than current max
|
||||
$('#min').on('change', function () {
|
||||
if (minDate.val() > maxDate.val())
|
||||
$('#max').datepicker('setDate', minDate.val() );
|
||||
});
|
||||
|
||||
// Handle giving earlier maxdate than current min
|
||||
$('#max').on('keyup change', function () {
|
||||
if (maxDate.val() < minDate.val())
|
||||
$('#min').datepicker('setDate', maxDate.val() );
|
||||
});
|
||||
|
||||
$(function() {
|
||||
$( ".datepicker" ).datepicker({
|
||||
changeMonth: true,
|
||||
changeYear: true,
|
||||
maxDate: '+0'
|
||||
});
|
||||
$(".datepicker").datepicker("option", "dateFormat", "yy-mm-dd")
|
||||
});
|
||||
});
|
||||
$(document.body).on('click', '.history-info-button', function () {
|
||||
var modal = $("#modal_history_info");
|
||||
var info = $(this).val();
|
||||
$('#modal-code-content').html(json_library.prettyPrint(info));
|
||||
modal.modal('show');
|
||||
|
||||
$('.checkbox,.radio').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
radioClass: 'iradio_square-blue',
|
||||
increaseArea: '20%'
|
||||
});
|
||||
|
||||
//Handle "ALL" Checkbox
|
||||
$('#auth_all_checkbox').on('ifChecked',function() {
|
||||
$('#auth_local_only_checkbox').iCheck('check');
|
||||
$('#auth_oauth_only_checkbox').iCheck('check');
|
||||
$('#auth_saml_only_checkbox').iCheck('check');
|
||||
});
|
||||
|
||||
$('#auth_all_checkbox').on('ifUnchecked',function() {
|
||||
//check if all were checked
|
||||
if($('#auth_local_only_checkbox').is(':checked') && $('#auth_oauth_only_checkbox').is(':checked') && $('#auth_saml_only_checkbox').is(':checked'))
|
||||
{
|
||||
$('#auth_local_only_checkbox').iCheck('uncheck');
|
||||
$('#auth_oauth_only_checkbox').iCheck('uncheck');
|
||||
$('#auth_saml_only_checkbox').iCheck('uncheck');
|
||||
}
|
||||
});
|
||||
|
||||
//Handle other auth checkboxes
|
||||
$('#auth_local_only_checkbox').on('ifChecked',function() {
|
||||
//check if all others were checked
|
||||
if($('#auth_oauth_only_checkbox').is(':checked') && $('#auth_saml_only_checkbox').is(':checked'))
|
||||
$('#auth_all_checkbox').iCheck('check');
|
||||
});
|
||||
$('#auth_local_only_checkbox').on('ifUnchecked',function() {
|
||||
$('#auth_all_checkbox').iCheck('uncheck');
|
||||
});
|
||||
|
||||
$('#auth_oauth_only_checkbox').on('ifChecked',function() {
|
||||
if($('#auth_local_only_checkbox').is(':checked') && $('#auth_saml_only_checkbox').is(':checked'))
|
||||
$('#auth_all_checkbox').iCheck('check');
|
||||
});
|
||||
$('#auth_oauth_only_checkbox').on('ifUnchecked',function() {
|
||||
$('#auth_all_checkbox').iCheck('uncheck');
|
||||
});
|
||||
|
||||
$('#auth_saml_only_checkbox').on('ifChecked',function() {
|
||||
if($('#auth_local_only_checkbox').is(':checked') && $('#auth_oauth_only_checkbox').is(':checked'))
|
||||
$('#auth_all_checkbox').iCheck('check');
|
||||
});
|
||||
$('#auth_saml_only_checkbox').on('ifUnchecked',function() {
|
||||
$('#auth_all_checkbox').iCheck('uncheck');
|
||||
});
|
||||
|
||||
$(document.body).on("click", ".button-clearf", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
$('#user_name_filter').val('');
|
||||
$('#min').val('');
|
||||
$('#max').val('');
|
||||
$('#domain_name_filter').val('');
|
||||
$('#account_name_filter').val('');
|
||||
$('#auth_name_filter').val('');
|
||||
$('#auth_all_checkbox').iCheck('check');
|
||||
$('#domain_changelog_only_checkbox').iCheck('uncheck');
|
||||
});
|
||||
|
||||
var all_doms = "{{all_domain_names}}".split(" ");
|
||||
var all_accounts = "{{all_account_names}}".split(" ");
|
||||
var all_usernames = "{{all_usernames}}".split(" ");
|
||||
all_doms.pop(); // remove last element which is " "
|
||||
all_accounts.pop();
|
||||
all_usernames.pop();
|
||||
|
||||
function autocomplete(inp, arr) {
|
||||
/*the autocomplete function takes two arguments,
|
||||
the text field element and an array of possible autocompleted values:*/
|
||||
var currentFocus;
|
||||
/*execute a function when someone writes in the text field:*/
|
||||
inp.addEventListener("input", function(e) {
|
||||
var a, b, i, val = this.value;
|
||||
/*close any already open lists of autocompleted values*/
|
||||
closeAllLists();
|
||||
if (!val) { return false;}
|
||||
currentFocus = -1;
|
||||
/*create a DIV element that will contain the items (values):*/
|
||||
a = document.createElement("DIV");
|
||||
a.setAttribute("id", this.id + "autocomplete-list");
|
||||
a.setAttribute("class", "autocomplete-items");
|
||||
/*append the DIV element as a child of the autocomplete container:*/
|
||||
this.parentNode.appendChild(a);
|
||||
/*for each item in the array...*/
|
||||
for (i = 0; i < arr.length; i++) {
|
||||
/*check if the item starts with the same letters as the text field value:*/
|
||||
if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
|
||||
/*create a DIV element for each matching element:*/
|
||||
b = document.createElement("DIV");
|
||||
/*make the matching letters bold:*/
|
||||
b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>";
|
||||
b.innerHTML += arr[i].substr(val.length);
|
||||
/*insert a input field that will hold the current array item's value:*/
|
||||
b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";
|
||||
/*execute a function when someone clicks on the item value (DIV element):*/
|
||||
b.addEventListener("click", function(e) {
|
||||
/*insert the value for the autocomplete text field:*/
|
||||
inp.value = this.getElementsByTagName("input")[0].value;
|
||||
/*close the list of autocompleted values,
|
||||
(or any other open lists of autocompleted values:*/
|
||||
closeAllLists();
|
||||
});
|
||||
a.appendChild(b);
|
||||
}
|
||||
}
|
||||
});
|
||||
/*execute a function presses a key on the keyboard:*/
|
||||
inp.addEventListener("keydown", function(e) {
|
||||
var x = document.getElementById(this.id + "autocomplete-list");
|
||||
if (x) x = x.getElementsByTagName("div");
|
||||
if (e.keyCode == 40) {
|
||||
/*If the arrow DOWN key is pressed,
|
||||
increase the currentFocus variable:*/
|
||||
currentFocus++;
|
||||
/*and and make the current item more visible:*/
|
||||
addActive(x);
|
||||
} else if (e.keyCode == 38) { //up
|
||||
/*If the arrow UP key is pressed,
|
||||
decrease the currentFocus variable:*/
|
||||
currentFocus--;
|
||||
/*and and make the current item more visible:*/
|
||||
addActive(x);
|
||||
} else if (e.keyCode == 13) {
|
||||
/*If the ENTER key is pressed, prevent the form from being submitted,*/
|
||||
e.preventDefault();
|
||||
if (currentFocus > -1) {
|
||||
/*and simulate a click on the "active" item:*/
|
||||
if (x) x[currentFocus].click();
|
||||
}
|
||||
}
|
||||
});
|
||||
function addActive(x) {
|
||||
/*a function to classify an item as "active":*/
|
||||
if (!x) return false;
|
||||
/*start by removing the "active" class on all items:*/
|
||||
removeActive(x);
|
||||
if (currentFocus >= x.length) currentFocus = 0;
|
||||
if (currentFocus < 0) currentFocus = (x.length - 1);
|
||||
/*add class "autocomplete-active":*/
|
||||
x[currentFocus].classList.add("autocomplete-active");
|
||||
}
|
||||
function removeActive(x) {
|
||||
/*a function to remove the "active" class from all autocomplete items:*/
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
x[i].classList.remove("autocomplete-active");
|
||||
}
|
||||
}
|
||||
function closeAllLists(elmnt) {
|
||||
/*close all autocomplete lists in the document,
|
||||
except the one passed as an argument:*/
|
||||
var x = document.getElementsByClassName("autocomplete-items");
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
if (elmnt != x[i] && elmnt != inp) {
|
||||
x[i].parentNode.removeChild(x[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*execute a function when someone clicks in the document:*/
|
||||
document.addEventListener("click", function (e) {
|
||||
closeAllLists(e.target);
|
||||
});
|
||||
}
|
||||
|
||||
/*initiate the autocomplete function on the "myInput" element, and pass along the countries array as possible autocomplete values:*/
|
||||
autocomplete(document.getElementById("domain_name_filter"), all_doms);
|
||||
autocomplete(document.getElementById("account_name_filter"), all_accounts);
|
||||
autocomplete(document.getElementById("auth_name_filter"), all_usernames);
|
||||
autocomplete(document.getElementById("user_name_filter"), all_usernames);
|
||||
|
||||
|
||||
// prevent multiple filter field at the same time
|
||||
$('#domain_tab').click(function() {
|
||||
$('#account_name_filter').val('');
|
||||
$('#auth_name_filter').val('');
|
||||
$('#user_name_filter').removeAttr('disabled');
|
||||
canSearch=false;
|
||||
main_field="Domain Name"
|
||||
});
|
||||
|
||||
$('#account_tab').click(function() {
|
||||
$('#domain_name_filter').val('');
|
||||
$('#auth_name_filter').val('');
|
||||
$('#user_name_filter').removeAttr('disabled');
|
||||
canSearch=false;
|
||||
main_field="Account Name"
|
||||
});
|
||||
|
||||
$('#user_auth_tab').click( function() {
|
||||
$('#domain_name_filter').val('');
|
||||
$('#account_name_filter').val('');
|
||||
$('#user_name_filter').val('');
|
||||
$('#user_name_filter').attr('disabled','disabled');
|
||||
canSearch=false;
|
||||
main_field="Username"
|
||||
});
|
||||
|
||||
$('#activity_tab').click( function() {
|
||||
$('#domain_name_filter').val('');
|
||||
$('#account_name_filter').val('');
|
||||
$('#auth_name_filter').val('');
|
||||
$('#user_name_filter').removeAttr('disabled');
|
||||
$('#search-submit').removeAttr('disabled','disabled');
|
||||
canSearch=true;
|
||||
main_field=""
|
||||
});
|
||||
|
||||
// if search submit is pressed, and max date not initialized
|
||||
// then initialize it
|
||||
$('#search-submit').on('click', function() {
|
||||
if ($('#max').val() === "" || $('#max').val() === undefined)
|
||||
$('#max').datepicker('setDate', new Date());
|
||||
});
|
||||
|
||||
$("#history-search-form").submit(function(e){ // ajax call to load results on submition
|
||||
e.preventDefault(); // prevent page reloading
|
||||
|
||||
if(!canSearch)
|
||||
{
|
||||
var modal = $("#modal_error");
|
||||
modal.find('.modal-body p').text("Please fill out the " + main_field + " field.");
|
||||
modal.modal('show');
|
||||
}
|
||||
else
|
||||
{
|
||||
var form = $(this);
|
||||
var tzoffset = (new Date()).getTimezoneOffset();
|
||||
$.ajax({
|
||||
url: "/admin/history_table",
|
||||
type: "get",
|
||||
data: form.serialize() + "&tzoffset=" + tzoffset,
|
||||
success: function(response) {
|
||||
console.log('Submission was successful.');
|
||||
$("#table_from_ajax").html(response);
|
||||
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.log("Sending data: ", data, " failed")
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modals %}
|
||||
@ -127,7 +505,7 @@
|
||||
<h4 class="modal-title">History Details</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre><code id="modal-code-content"></code></pre>
|
||||
<div id="modal-info-content"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-right" data-dismiss="modal">Close</button>
|
||||
@ -138,4 +516,4 @@
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
<!-- /.modal -->
|
||||
{% endblock %}
|
||||
{% endblock %}
|
104
powerdnsadmin/templates/admin_history_table.html
Normal file
104
powerdnsadmin/templates/admin_history_table.html
Normal file
@ -0,0 +1,104 @@
|
||||
|
||||
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||
|
||||
|
||||
{% if len_histories >= lim %}
|
||||
<p style="color: rgb(224, 3, 3);"><b>Limit of loaded history records has been reached! Only {{lim}} history records are shown. </b></p>
|
||||
{% endif %}
|
||||
|
||||
<div class="box-body"></div>
|
||||
<table id="tbl_history" class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Changed by</th>
|
||||
<th>Content</th>
|
||||
<th>Time</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for history in histories %}
|
||||
<tr class="odd gradeX">
|
||||
<td>{{ history.history.created_by }}</td>
|
||||
<td>{{ history.history.msg }}</td>
|
||||
<td>{{ history.history.created_on }}</td>
|
||||
|
||||
<td width="6%">
|
||||
<button type="button" class="btn btn-flat btn-primary history-info-button"
|
||||
{% if history.detailed_msg == "" and history.change_set == None %}
|
||||
style="visibility: hidden;"
|
||||
{% endif%}
|
||||
value='{{ history.detailed_msg }}
|
||||
{% if history.change_set != None %}
|
||||
<div class="content">
|
||||
<div id="change_index_definition"></div>
|
||||
{% call applied_change_macro.applied_change_template(history.change_set) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endif %}
|
||||
'>Info <i class="fa fa-info"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
|
||||
var table;
|
||||
$(document).ready(function () {
|
||||
|
||||
|
||||
table = $('#tbl_history').DataTable({
|
||||
"order": [
|
||||
[2, "desc"]
|
||||
],
|
||||
"searching": true,
|
||||
"columnDefs": [{
|
||||
"type": "time",
|
||||
"render": function (data, type, row) {
|
||||
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
"targets": 2
|
||||
}],
|
||||
"info": true,
|
||||
"autoWidth": false,
|
||||
orderCellsTop: true,
|
||||
fixedHeader: true
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$(document.body).on('click', '.history-info-button', function () {
|
||||
var modal = $("#modal_history_info");
|
||||
var info = $(this).val();
|
||||
$('#modal-info-content').html(info);
|
||||
modal.modal('show');
|
||||
});
|
||||
|
||||
|
||||
|
||||
$(document.body).on("click", ".button-filter", function (e) {
|
||||
e.stopPropagation();
|
||||
var nextRow = $("#filter-table")
|
||||
if (nextRow.css("visibility") == "visible")
|
||||
nextRow.css("visibility", "collapse")
|
||||
else
|
||||
nextRow.css("visibility", "visible")
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
@ -51,7 +51,7 @@
|
||||
<div class="nav-tabs-custom" id="tabs">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#tabs-general" data-toggle="tab">General</a></li>
|
||||
<li class="active"><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li>
|
||||
<li><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li>
|
||||
<li><a href="#tabs-google" data-toggle="tab">Google OAuth</a></li>
|
||||
<li><a href="#tabs-github" data-toggle="tab">Github OAuth</a></li>
|
||||
<li><a href="#tabs-azure" data-toggle="tab">Microsoft OAuth</a></li>
|
||||
@ -76,7 +76,7 @@
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane active" id="tabs-ldap">
|
||||
<div class="tab-pane" id="tabs-ldap">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{% if error %}
|
||||
@ -693,12 +693,12 @@
|
||||
<script>
|
||||
|
||||
$(function() {
|
||||
$('#tabs').tabs({
|
||||
// add url anchor tags
|
||||
activate: function(event, ui) {
|
||||
window.location.hash = ui.newPanel.attr('id');
|
||||
}
|
||||
});
|
||||
// $('#tabs').tabs({
|
||||
// // add url anchor tags
|
||||
// activate: function(event, ui) {
|
||||
// window.location.hash = ui.newPanel.attr('id');
|
||||
// }
|
||||
// });
|
||||
// re-set active tab (ui)
|
||||
var activeTabIdx = $('#tabs').tabs('option','active');
|
||||
$('#tabs li:eq('+activeTabIdx+')').tab('show')
|
||||
|
133
powerdnsadmin/templates/applied_change_macro.html
Normal file
133
powerdnsadmin/templates/applied_change_macro.html
Normal file
@ -0,0 +1,133 @@
|
||||
{% macro applied_change_template(change_set) -%}
|
||||
{{ caller() }}
|
||||
{% for hist_rec_entry in change_set %}
|
||||
<table id="tbl_records" class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3">
|
||||
{% if hist_rec_entry.change_type == "+" %}
|
||||
<span
|
||||
style="background-color: lightgreen">{{hist_rec_entry.add_rrest['name']}}
|
||||
{{hist_rec_entry.add_rrest['type']}}</span>
|
||||
{% elif hist_rec_entry.change_type == "-" %}
|
||||
<s
|
||||
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||
{{hist_rec_entry.del_rrest['name']}}
|
||||
{{hist_rec_entry.del_rrest['type']}}
|
||||
</s>
|
||||
{% else %}
|
||||
{{hist_rec_entry.add_rrest['name']}}
|
||||
{{hist_rec_entry.add_rrest['type']}}
|
||||
{% endif %}
|
||||
|
||||
, TTL:
|
||||
{% if "ttl" in hist_rec_entry.changed_fields %}
|
||||
<s
|
||||
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||
{{hist_rec_entry.del_rrest['ttl']}}</s>
|
||||
<span
|
||||
style="background-color: lightgreen">{{hist_rec_entry.add_rrest['ttl']}}</span>
|
||||
{% else %}
|
||||
{{hist_rec_entry.add_rrest['ttl']}}
|
||||
{% endif %}
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
<th style="width: 150px;">Status</th>
|
||||
<th style="width: 400px;">Data</th>
|
||||
<th style="width: 400px;">Comment</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for changes in hist_rec_entry.changeSet %}
|
||||
<tr>
|
||||
{% if changes[2] == "unchanged" %}
|
||||
<td>{{ "Activated" if changes[0]['disabled'] ==
|
||||
False else
|
||||
"Disabled"}} </td>
|
||||
{% elif changes[2] == "addition" %}
|
||||
<td>
|
||||
<span style="background-color: lightgreen">
|
||||
{{ "Activated" if changes[1]['disabled'] ==
|
||||
False else
|
||||
"Disabled"}}
|
||||
</span>
|
||||
</td>
|
||||
{% elif changes[2] == "status" %}
|
||||
<td>
|
||||
<s
|
||||
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||
{{ "Activated" if changes[0]['disabled'] ==
|
||||
False else
|
||||
"Disabled"}}</s>
|
||||
<span style="background-color: lightgreen">{{
|
||||
"Activated" if changes[1]['disabled'] ==
|
||||
False else
|
||||
"Disabled"}}</span>
|
||||
</td>
|
||||
{% elif changes[2] == "deletion" %}
|
||||
<td>
|
||||
<s
|
||||
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||
{{ "Activated" if changes[0]['disabled'] ==
|
||||
False else
|
||||
"Disabled"}}</s>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for changes in hist_rec_entry.changeSet %}
|
||||
<tr>
|
||||
{% if changes[2] == "unchanged" %}
|
||||
<td>
|
||||
{{changes[0]['content']}}
|
||||
</td>
|
||||
{% elif changes[2] == "addition" %}
|
||||
<td>
|
||||
<span style="background-color: lightgreen">
|
||||
{{changes[1]['content']}}
|
||||
</span>
|
||||
</td>
|
||||
{% elif changes[2] == "deletion" %}
|
||||
<td>
|
||||
<s
|
||||
style="text-decoration-color: rgba(194, 10, 10, 0.6); text-decoration-thickness: 2px;">
|
||||
{{changes[0]['content']}}
|
||||
</s>
|
||||
</td>
|
||||
{% elif changes[2] == "status" %}
|
||||
<td>
|
||||
{{changes[0]['content']}}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
{% for comments in hist_rec_entry.add_rrest['comments'] %}
|
||||
{{comments['content'] }}
|
||||
<br/>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
@ -6,6 +6,16 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}">
|
||||
<!-- Get jquery UI -->
|
||||
{% if OFFLINE_MODE %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/jquery-ui-smooth-datepicker/jquery-ui.js') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/jquery-ui-smooth-datepicker/jquery-ui.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/jquery-ui-smooth-datepicker/jquery-ui.structure.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/jquery-ui-smooth-datepicker/jquery-ui.theme.css') }}">
|
||||
{% else %}
|
||||
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css" type="text/css"/>
|
||||
{% endif %}
|
||||
<!-- Get Google Fonts we like -->
|
||||
{% if OFFLINE_MODE %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/source_sans_pro.css') }}">
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% set active_page = "dashboard" %}
|
||||
{% block title %}<title>Dashboard - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
@ -16,6 +17,8 @@
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
@ -107,13 +110,25 @@
|
||||
<tbody>
|
||||
{% for history in histories %}
|
||||
<tr class="odd">
|
||||
<td>{{ history.created_by }}</td>
|
||||
<td>{{ history.msg }}</td>
|
||||
<td>{{ history.created_on }}</td>
|
||||
<td>{{ history.history.created_by }}</td>
|
||||
<td>{{ history.history.msg }}</td>
|
||||
<td>{{ history.history.created_on }}</td>
|
||||
<td width="6%">
|
||||
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail }}'>
|
||||
Info <i class="fa fa-info"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-flat btn-primary history-info-button"
|
||||
{% if history.detailed_msg == "" and history.change_set == None %}
|
||||
style="visibility: hidden;"
|
||||
{% endif%}
|
||||
value='{{ history.detailed_msg }}
|
||||
{% if history.change_set != None %}
|
||||
<div class="content">
|
||||
<div id="change_index_definition"></div>
|
||||
{% call applied_change_macro.applied_change_template(history.change_set) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endif %}
|
||||
'>
|
||||
Info <i class="fa fa-info"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -226,11 +241,10 @@
|
||||
]
|
||||
});
|
||||
|
||||
$(document.body).on('click', '.history-info-button', function()
|
||||
{
|
||||
$(document.body).on('click', '.history-info-button', function () {
|
||||
var modal = $("#modal_history_info");
|
||||
var info = $(this).val();
|
||||
$('#modal-code-content').html(json_library.prettyPrint(info));
|
||||
$('#modal-info-content').html(info);
|
||||
modal.modal('show');
|
||||
});
|
||||
|
||||
@ -297,7 +311,7 @@
|
||||
<h4 class="modal-title">History Details</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre><code id="modal-code-content"></code></pre>
|
||||
<div id="modal-info-content"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-right"
|
||||
|
@ -40,12 +40,20 @@
|
||||
<button type="button" class="btn btn-flat btn-danger" onclick="window.location.href='{{ url_for('domain.setting', domain_name=domain.name) }}'">
|
||||
Admin <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain.changelog', domain_name=domain.name) }}'">
|
||||
Changelog <i class="fa fa-history" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
{% else %}
|
||||
<td width="6%">
|
||||
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain.domain', domain_name=domain.name) }}'">
|
||||
Manage <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
{% if allow_user_view_history %}
|
||||
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain.changelog', domain_name=domain.name) }}'">
|
||||
Changelog <i class="fa fa-history" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
@ -33,6 +33,12 @@
|
||||
Update from Master <i class="fa fa-download"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||
<button type="button" style="position: relative; margin-left: 20px" class="btn btn-flat btn-primary button_changelog" id="{{ domain.name }}">
|
||||
Changelog <i class="fa fa-history" aria-hidden="true"></i>
|
||||
</i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<table id="tbl_records" class="table table-bordered table-striped">
|
||||
@ -46,6 +52,9 @@
|
||||
<th>Comment</th>
|
||||
<th>Edit</th>
|
||||
<th>Delete</th>
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||
<th >Changelog</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -91,6 +100,13 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||
<td width="6%">
|
||||
<button type="button" onclick="show_record_changelog('{{record.name}}','{{record.type}}',event)" class="btn btn-flat btn-primary">
|
||||
<i class="fa fa-history" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
<!-- hidden column that we can sort on -->
|
||||
<td>1</td>
|
||||
</tr>
|
||||
@ -144,14 +160,33 @@
|
||||
// hidden column so that we can add new records on top
|
||||
// regardless of whatever sorting is done. See orderFixed
|
||||
visible: false,
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||
targets: [ 9 ]
|
||||
{% else %}
|
||||
targets: [ 8 ]
|
||||
{% endif %}
|
||||
},
|
||||
{
|
||||
className: "length-break",
|
||||
targets: [ 4, 5 ]
|
||||
}
|
||||
],
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||
"orderFixed": [[9, 'asc']]
|
||||
{% else %}
|
||||
"orderFixed": [[8, 'asc']]
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
|
||||
function show_record_changelog(record_name, record_type, e) {
|
||||
e.stopPropagation();
|
||||
window.location.href = "/domain/{{domain.name}}/changelog/" + record_name + ".-" + record_type;
|
||||
}
|
||||
// handle changelog button
|
||||
$(document.body).on("click", ".button_changelog", function(e) {
|
||||
e.stopPropagation();
|
||||
window.location.href = "/domain/{{domain.name}}/changelog";
|
||||
});
|
||||
|
||||
// handle delete button
|
||||
@ -243,7 +278,11 @@
|
||||
|
||||
// add new row
|
||||
var default_type = records_allow_edit[0]
|
||||
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', window.ttl_options[0][0], '', '', '', '', '0']);
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', window.ttl_options[0][0], '', '', '', '', '', '0']);
|
||||
{% else %}
|
||||
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', window.ttl_options[0][0], '', '', '', '', '0']);
|
||||
{% endif %}
|
||||
editRow($("#tbl_records").DataTable(), nRow);
|
||||
document.getElementById("edit-row-focus").focus();
|
||||
nEditing = nRow;
|
||||
|
112
powerdnsadmin/templates/domain_changelog.html
Normal file
112
powerdnsadmin/templates/domain_changelog.html
Normal file
@ -0,0 +1,112 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}<title>{{ domain.name | pretty_domain_name }} - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Domain changelog: <b>{{ domain.name | pretty_domain_name }}</b>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li>Domain</li>
|
||||
<li class="active">{{ domain.name | pretty_domain_name }}</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-body">
|
||||
<button type="button" class="btn btn-flat btn-primary pull-left button_show_records"
|
||||
id="{{ domain.name }}">
|
||||
Manage <i class="fa fa-arrow-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
|
||||
<table id="tbl_changelog" class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Changed on</th>
|
||||
<th>Changed by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for applied_change in allHistoryChanges %}
|
||||
|
||||
<tr class="odd row_record" id="{{ domain.name }}">
|
||||
<td id="changed_on" class="changed_on">
|
||||
{{ allHistoryChanges[applied_change][0].history_entry.created_on }}
|
||||
</td>
|
||||
<td>
|
||||
{{allHistoryChanges[applied_change][0].history_entry.created_by }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Nested Table -->
|
||||
<tr style='visibility:collapse'>
|
||||
<td colspan="2">
|
||||
<div class="content">
|
||||
{% call applied_change_macro.applied_change_template(allHistoryChanges[applied_change]) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- end nested table -->
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
// handle "show records" button
|
||||
$(document.body).on("click", ".button_show_records", function (e) {
|
||||
e.stopPropagation();
|
||||
window.location.href = "/domain/{{domain.name}}";
|
||||
});
|
||||
|
||||
var coll = document.getElementsByClassName("collapsible");
|
||||
var i;
|
||||
|
||||
for (i = 0; i < coll.length; i++) {
|
||||
coll[i].addEventListener("click", function () {
|
||||
this.classList.toggle("active");
|
||||
var content = this.nextElementSibling;
|
||||
if (content.style.maxHeight) {
|
||||
content.style.maxHeight = null;
|
||||
} else {
|
||||
content.style.maxHeight = content.scrollHeight + "px";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// handle click on history record
|
||||
$(document.body).on("click", ".row_record", function (e) {
|
||||
e.stopPropagation();
|
||||
var nextRow = $(this).next('tr')
|
||||
if (nextRow.css("visibility") == "visible")
|
||||
nextRow.css("visibility", "collapse")
|
||||
else
|
||||
nextRow.css("visibility", "visible")
|
||||
|
||||
});
|
||||
|
||||
|
||||
var els = document.getElementsByClassName("changed_on");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
// els[i].innerHTML = moment.utc(els[i].innerHTML).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
els[i].innerHTML = moment.utc(els[i].innerHTML,'YYYY-MM-DD HH:mm:ss').local().format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
@ -123,12 +123,12 @@
|
||||
<!-- TODO: add password and password confirmation comparison check -->
|
||||
<script>
|
||||
$(function () {
|
||||
$('#tabs').tabs({
|
||||
// add url anchor tags
|
||||
activate: function (event, ui) {
|
||||
window.location.hash = ui.newPanel.attr('id');
|
||||
}
|
||||
});
|
||||
// $('#tabs').tabs({
|
||||
// // add url anchor tags
|
||||
// activate: function (event, ui) {
|
||||
// window.location.hash = ui.newPanel.attr('id');
|
||||
// }
|
||||
// });
|
||||
// re-set active tab (ui)
|
||||
var activeTabIdx = $('#tabs').tabs('option', 'active');
|
||||
$('#tabs li:eq(' + activeTabIdx + ')').tab('show')
|
||||
|
Reference in New Issue
Block a user