sftpgo/templates/webadmin/events.html
Nicola Murino 04ab8e72f6
WebUI: make error messages user dismissible
Fixes #1171

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-10 18:07:23 +01:00

601 lines
No EOL
22 KiB
HTML

<!--
Copyright (C) 2019-2023 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/daterangepicker/daterangepicker.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Search logs</h6>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-3">
<select class="form-control selectpicker" id="idEventType" name="events_type" onchange="onEventChanged(this.value)">
<option value="1" selected>Fs events</option>
<option value="2">Provider events</option>
</select>
</div>
<div class="form-group col-md-3">
<select class="form-control selectpicker" id="idActions" name="actions" title="Actions" multiple>
</select>
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idUsername" name="username" spellcheck="false" placeholder="Username">
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idIp" name="ip" placeholder="IP address">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<select class="form-control selectpicker fs-events" id="idProtocols" name="protocols" title="Protocols" multiple>
<option value="SFTP">SFTP</option>
<option value="SCP">SCP</option>
<option value="SSH">SSH</option>
<option value="FTP">FTP</option>
<option value="DAV">DAV</option>
<option value="HTTP">HTTP</option>
<option value="OIDC">OIDC</option>
<option value="HTTPShare">HTTPShare</option>
<option value="DataRetention">DataRetention</option>
<option value="EventAction">EventAction</option>
</select>
</div>
<div class="form-group col-md-4">
<select class="form-control selectpicker fs-events" id="idStatuses" name="statuses" title="Statuses" multiple>
<option value="1">OK</option>
<option value="2">KO</option>
<option value="3">Quota exceeded</option>
</select>
</div>
<div class="form-group col-md-4">
<div class="input-group">
<input type="text" id="dateTimeRange" class="form-control bg-light border-0" aria-describedby="search-button">
<div class="input-group-append">
<button id="search-button" class="btn btn-primary" type="button" onclick="onSearchClicked()">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<button class="btn btn-secondary mt-1 mb-1 px-5" type="button" onclick="onExportClicked()">Export</button>
</div>
</div>
<div class="table-responsive fs-events">
<table class="table table-hover nowrap" id="dataTableFs" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Time</th>
<th>Action</th>
<th>Path</th>
<th>User</th>
<th>Proto</th>
<th>IP</th>
<th>Info</th>
</tr>
</thead>
</table>
</div>
<div class="table-responsive provider-events">
<table class="table table-hover nowrap" id="dataTableProvider" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Time</th>
<th>Action</th>
<th>Object</th>
<th>User</th>
<th>IP</th>
</tr>
</thead>
</table>
</div>
<div id="paginationContainer" class="m-4 d-none">
<nav aria-label="Pagination">
<ul class="pagination justify-content-end">
<li id="pageItemPrev" class="page-item disabled"><a id="pagePrevious" class="page-link" href="#" onclick="prevClicked()">Previous</a></li>
<li id="pageItemNext" class="page-item disabled"><a id="pageNext" class="page-link" href="#" onclick="nextClicked()">Next</a></li>
</ul>
</nav>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script src="{{.StaticURL}}/vendor/moment/js/moment.min.js"></script>
<script src="{{.StaticURL}}/vendor/daterangepicker/daterangepicker.min.js"></script>
<script type="text/javascript">
let dateFn = $.fn.dataTable.render.datetime();
let isFsDataTableInitialized = false;
let isProviderDataTableInitialized = false;
const pageSize = 20;
const paginationData = new Map();
function fileSizeIEC(a,b,c,d,e){
return (b=Math,c=b.log,d=1024,e=c(a)/c(d)|0,a/b.pow(d,e)).toFixed(1)
+' '+(e?'KMGTPEZY'[--e]+'iB':'Bytes')
}
function resetPagination() {
$('#pageItemPrev').addClass("disabled");
$('#pageItemNext').addClass("disabled");
$('#paginationContainer').addClass("d-none");
paginationData.delete("firstId");
paginationData.delete("firstTs");
paginationData.delete("lastId");
paginationData.delete("lastTs");
paginationData.set("prevClicked",false);
paginationData.set("nextClicked",false);
}
function prevClicked(){
paginationData.set("prevClicked",true);
paginationData.set("nextClicked",false);
doSearch();
}
function nextClicked(){
paginationData.set("prevClicked",false);
paginationData.set("nextClicked",true);
doSearch();
}
function handleResponseData(data) {
let length = data.length;
let isNext = paginationData.get("nextClicked");
let isPrev = paginationData.get("prevClicked");
if (length > pageSize) {
data.pop();
length--;
if (isPrev || isNext){
$('#pageItemPrev').removeClass("disabled");
}
$('#pageItemNext').removeClass("disabled");
} else {
if (isPrev){
$('#pageItemPrev').addClass("disabled");
$('#pageItemNext').removeClass("disabled");
} else if (isNext){
$('#pageItemPrev').removeClass("disabled");
$('#pageItemNext').addClass("disabled");
} else {
$('#pageItemNext').addClass("disabled");
}
}
if (isPrev){
data = data.reverse();
}
if (length > 0){
paginationData.set("lastId",data[0].id);
paginationData.set("lastTs",data[0].timestamp);
paginationData.set("firstId",data[length-1].id);
paginationData.set("firstTs",data[length-1].timestamp);
$('#paginationContainer').removeClass("d-none");
} else {
resetPagination();
}
return data;
}
function onExportClicked() {
paginationData.set("prevClicked",false);
paginationData.set("nextClicked",false);
let exportURL = getSearchURL(true);
let ts = new Date().getTime().toString();
window.open(`${exportURL}&_=${ts}`);
}
function onSearchClicked() {
resetPagination();
doSearch();
}
function doSearch() {
let eventType = $('#idEventType').val();
let table;
if (eventType == 1){
if (!isFsDataTableInitialized){
initFsDatatable();
return;
}
table = $('#dataTableFs').DataTable();
} else {
if (!isProviderDataTableInitialized){
initProviderDatatable();
return;
}
table = $('#dataTableProvider').DataTable();
}
table.clear().draw();
table.ajax.url(getSearchURL(false)).load();
}
function getSearchURL(csvExport) {
let url = "";
let eventType = $('#idEventType').val();
let order = "DESC";
let limit = pageSize + 1;
if (csvExport){
order = "ASC";
}
if (eventType == 1){
url = "{{.FsEventsSearchURL}}?limit="+limit;
let protocols = [];
$('#idProtocols').find('option:selected').each(function(){
protocols.push($(this).val());
});
if (protocols.length > 0){
url+="&protocols="+encodeURIComponent(String(protocols));
}
let statuses = [];
$('#idStatuses').find('option:selected').each(function(){
statuses.push($(this).val());
});
if (statuses.length > 0){
url+="&statuses="+encodeURIComponent(String(statuses));
}
} else {
url = "{{.ProviderEventsSearchURL}}?omit_object_data=true&limit="+limit;
}
let actions = [];
$('#idActions').find('option:selected').each(function(){
actions.push($(this).val());
});
if (actions.length > 0){
url+="&actions="+encodeURIComponent(String(actions));
}
let username = $('#idUsername').val();
if (username){
url+="&username="+encodeURIComponent(username);
}
let ip = $('#idIp').val();
if (ip){
url+="&ip="+encodeURIComponent(ip);
}
let drp = $('#dateTimeRange').data('daterangepicker');
let excludeIds = [];
let start_ts;
if (!csvExport && paginationData.get("prevClicked") && paginationData.has("lastId") && paginationData.has("lastTs")){
order = "ASC";
start_ts = paginationData.get("lastTs");
excludeIds.push(paginationData.get("lastId"));
} else {
start_ts = drp.startDate.valueOf()*1000000;
}
let end_ts;
if (!csvExport && paginationData.get("nextClicked") && paginationData.has("firstId") && paginationData.has("firstTs")){
end_ts = paginationData.get("firstTs");
excludeIds.push(paginationData.get("firstId"));
} else {
end_ts = drp.endDate.valueOf()*1000000;
}
url+="&start_timestamp="+encodeURIComponent(start_ts);
url+="&end_timestamp="+encodeURIComponent(end_ts);
if (excludeIds.length > 0){
url+="&exclude_ids="+encodeURIComponent(String(excludeIds));
}
url+="&order="+order;
if (csvExport){
url+="&csv_export=true";
}
return url;
}
function initProviderDatatable(){
$('#errorMsg').hide();
let tableProvider = $('#dataTableProvider').DataTable({
"ajax": {
"url": getSearchURL(false),
"dataSrc": handleResponseData,
"error": function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "Failed to get provider events";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
},
"deferRender": true,
"processing": true,
"columns": [
{ "data": "id" },
{
"data": "timestamp",
"render": function (data, type, row) {
if (type === 'display') {
return dateFn(data/1000000,type);
}
return data;
}
},
{
"data": "action"
},
{
"data": "object_type",
"render": function (data, type, row) {
if (type === 'display') {
let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true);
return ellipsisFn(`${data}: ${row["object_name"]}`,type);
}
return data;
}
},
{
"data": "username",
"defaultContent": ""
},
{
"data": "ip",
"defaultContent": ""
}
],
"buttons": [],
"lengthChange": false,
"columnDefs": [
{
"targets": [0],
"visible": false,
"searchable": false
},
],
"responsive": true,
"searching": false,
"paging": false,
"info": false,
"ordering": false,
"language": {
"loadingRecords": "",
"emptyTable": "No logs found"
}
});
new $.fn.dataTable.FixedHeader(tableProvider);
isProviderDataTableInitialized = true;
}
function initFsDatatable(){
$('#errorMsg').hide();
let tableFs = $('#dataTableFs').DataTable({
"ajax": {
"url": getSearchURL(false),
"dataSrc": handleResponseData,
"error": function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "Failed to get filesystem events";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
},
"deferRender": true,
"processing": true,
"columns": [
{ "data": "id" },
{
"data": "timestamp",
"render": function (data, type, row) {
if (type === 'display') {
return dateFn(data/1000000,type);
}
return data;
}
},
{
"data": "action"
},
{
"data": "virtual_path",
"render": function (data, type, row) {
if (type === 'display') {
let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true);
if (row["virtual_target_path"]){
return ellipsisFn(`${data} => ${row["virtual_target_path"]}`,type);
}
return ellipsisFn(data,type);
}
return data;
}
},
{
"data": "username",
"defaultContent": ""
},
{
"data": "protocol",
"defaultContent": ""
},
{
"data": "ip",
"defaultContent": ""
},
{
"data": "status",
"render": function (data, type, row) {
if (type === 'display') {
let info = 'OK';
if (data == 2){
info = 'KO';
} else if (data == 3){
info = 'Quota exceeded'
}
if (row["file_size"]){
let humanSize = fileSizeIEC(row["file_size"]);
info+=`. ${humanSize}`
}
if (row["ssh_cmd"]){
info+=`. Command: ${row["ssh_cmd"]}`
}
return info;
}
return data;
}
}
],
"buttons": [],
"lengthChange": false,
"columnDefs": [
{
"targets": [0],
"visible": false,
"searchable": false
},
],
"responsive": true,
"searching": false,
"paging": false,
"info": false,
"ordering": false,
"language": {
"loadingRecords": "",
"emptyTable": "No logs found"
}
});
new $.fn.dataTable.FixedHeader(tableFs);
isFsDataTableInitialized = true;
}
function selectFsEvents(){
let idActions = $('#idActions');
idActions.selectpicker('deselectAll');
idActions.find('option').remove();
idActions.find('li').remove();
idActions.append($('<option>').val('upload').text('Upload'));
idActions.append($('<option>').val('download').text('Download'));
idActions.append($('<option>').val('mkdir').text('Mkdir'));
idActions.append($('<option>').val('rmdir').text('Rmdir'));
idActions.append($('<option>').val('rename').text('Rename'));
idActions.append($('<option>').val('delete').text('Delete'));
idActions.append($('<option>').val('first-upload').text('First upload'));
idActions.append($('<option>').val('first-download').text('First download'));
idActions.append($('<option>').val('ssh_cmd').text('SSH command'));
idActions.selectpicker('refresh');
$('#idUsername').val("");
$('#idIp').val("");
$('.provider-events').hide();
$('.fs-events').show();
onSearchClicked();
}
function selectProviderEvents(){
let idActions = $('#idActions');
idActions.selectpicker('deselectAll');
idActions.find('option').remove();
idActions.find('li').remove();
idActions.append($('<option>').val('add').text('Add'));
idActions.append($('<option>').val('update').text('Update'));
idActions.append($('<option>').val('delete').text('Delete'));
idActions.selectpicker('refresh');
$('#idUsername').val("");
$('#idIp').val("");
$('.fs-events').hide();
$('.provider-events').show();
onSearchClicked();
}
function onEventChanged(val){
switch (val){
case '1':
selectFsEvents();
break;
case '2':
selectProviderEvents();
break;
default:
console.log(`unsupported event type: ${val}`);
}
}
$(document).ready(function () {
$('#dateTimeRange').daterangepicker({
timePicker: true,
timePicker24Hour: true,
opens: 'left',
startDate: moment().add(-1,'hour'),
endDate: moment(),
locale: {
format: 'DD/MM HH:mm'
}
});
$.fn.dataTable.ext.errMode = 'none';
onEventChanged('1');
});
</script>
{{end}}