Files
ruantiblock_openwrt/luci-app-ruantiblock/htdocs/luci-static/resources/view/ruantiblock/abstract-log.js
T
2021-03-26 23:49:38 +03:00

565 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
'require ui';
document.head.append(E('style', {'type': 'text/css'},
`
.log-entry-empty {
}
.log-entry-number {
min-width: 4em !important;
}
.log-entry-time {
min-width: 14em !important;
}
.log-entry-host {
min-width: 10em !important;
}
.log-entry-host-cell {
word-break: break-all !important;
word-wrap: break-word !important;
}
.log-entry-log-level {
max-width: 5em !important;
}
.log-entry-facility{
max-width: 7em !important;
}
.log-entry-message {
min-width: 25em !important;
}
.log-entry-message-cell {
overflow-x: hidden !important;
text-overflow: ellipsis !important;
}
.log-empty {
}
.log-emerg {
background-color: #a93734 !important;
color: #fff;
}
log-emerg .td {
color: #fff !important;
}
log-emerg td {
color: #fff !important;
}
.log-alert {
background-color: #ff7968 !important;
color: #fff;
}
.log-alert .td {
color: #fff !important;
}
.log-alert td {
color: #fff !important;
}
.log-crit {
background-color: #fcc3bf !important;
}
.log-err {
background-color: #ffe9e8 !important;
}
.log-warn {
background-color: #fff7e2 !important;
}
.log-notice {
background-color: #e3ffec !important;
}
.log-info {
}
.log-debug {
background-color: #ebf6ff !important;
}
.log-highlight-item {
background-color: #ffef00;
}
.log-entries-count {
margin: 0 0 5px 5px;
font-weight: bold;
opacity: 0.6;
}
.log-entries-count-level {
display: inline-block !important;
margin: 0 0 0 5px;
padding: 0 4px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
border: 1px solid #ccc;
font-weight: normal;
}
.log-host-dropdown-item {
}
`));
return L.Class.extend({
view: L.view.extend({
viewName: null,
title: null,
logLevels: {
'emerg': E('span', { 'class': 'zonebadge log-emerg' }, E('strong', _('Emergency'))),
'alert': E('span', { 'class': 'zonebadge log-alert' }, E('strong', _('Alert'))),
'crit': E('span', { 'class': 'zonebadge log-crit' }, E('strong', _('Critical'))),
'err': E('span', { 'class': 'zonebadge log-err' }, E('strong', _('Error'))),
'warn': E('span', { 'class': 'zonebadge log-warn' }, E('strong', _('Warning'))),
'notice': E('span', { 'class': 'zonebadge log-notice' }, E('strong', _('Notice'))),
'info': E('span', { 'class': 'zonebadge log-info' }, E('strong', _('Info'))),
'debug': E('span', { 'class': 'zonebadge log-debug' }, E('strong', _('Debug'))),
},
tailValue: 25,
logSortingValue: 'asc',
logLevelsStat: {},
logHosts: {},
logHostsDropdown: null,
logLevelsDropdown: null,
totalLogLines: 0,
htmlEntities: function(str) {
return String(str).replace(
/&/g, '&').replace(
/</g, '&#60;').replace(
/>/g, '&#62;').replace(
/"/g, '&#34;').replace(
/'/g, '&#39;');
},
makelogHostsDropdownItem: function(host) {
return E(
'span',
{ 'class': 'zonebadge log-host-dropdown-item' },
E('strong', host)
);
},
/**
*
* @param {number} tail
* @returns {string}
* Returns the raw content of the log
*
*/
getLogData: function(tail) {
throw new Error('getLogData must be overridden by a subclass');
},
/**
*
* @param {string} logdata
* @param {number} tail
* @returns {Array<number, string|null, string|null, string|null, string|null, string|null>}
* Returns an array of values: [ #, Timestamp, Host, Level, Facility, Message ]
*
*/
parseLogData: function(logdata, tail) {
throw new Error('parseLogData must be overridden by a subclass');
},
setHostFilter: function(cArr) {
let logHostsKeys = Object.keys(this.logHosts);
if(logHostsKeys.length > 0) {
let selectedHosts = this.logHostsDropdown.getValue();
this.logHostsDropdown.addChoices(Object.keys(this.logHosts), this.logHosts);
if(selectedHosts.length === 0 || logHostsKeys.length === selectedHosts.length) {
return cArr;
};
return cArr.filter(e => selectedHosts.includes(e[2]));
};
return cArr;
},
setLevelFilter: function(cArr) {
let logLevelsKeys = Object.keys(this.logLevels);
if(logLevelsKeys.length > 0) {
let selectedLevels = this.logLevelsDropdown.getValue();
if(selectedLevels.length === 0 || logLevelsKeys.length === selectedLevels.length) {
return cArr;
};
return cArr.filter(e => selectedLevels.includes(e[3]));
};
return cArr;
},
setRegexpFilter: function(cArr) {
let fPattern = document.getElementById('logFilter').value;
if(!fPattern) {
return cArr;
};
let fArr = [];
try {
let regExp = new RegExp(`(${fPattern})`, 'giu');
cArr.forEach((e, i) => {
if(e[5] !== null && regExp.test(e[5])) {
e[5] = e[5].replace(regExp, '<span class="log-highlight-item">$1</span>');
fArr.push(e);
};
});
} catch(err) {
if(err.name === 'SyntaxError') {
ui.addNotification(null,
E('p', {}, _('Invalid regular expression') + ': ' + err.message));
return cArr;
} else {
throw err;
};
};
return fArr;
},
makeLogArea: function(logdataArray) {
let lines = `<div class="tr"><div class="td center log-entry-empty">${_('No entries available...')}</div></div>`;
let logTable = E('div', { 'id': 'logTable', 'class': 'table' });
for(let level of Object.keys(this.logLevels)) {
this.logLevelsStat[level] = 0;
};
if(logdataArray.length > 0) {
lines = [];
logdataArray.forEach((e, i) => {
if(e[3] in this.logLevels) {
this.logLevelsStat[e[3]] = this.logLevelsStat[e[3]] + 1;
};
lines.push(
`<div class="tr log-${e[3] || 'empty'}"><div class="td left" data-title="#">${e[0]}</div>` +
((e[1]) ? `<div class="td left" data-title="${_('Timestamp')}">${e[1]}</div>` : '') +
((e[2]) ? `<div class="td left log-entry-host-cell" data-title="${_('Host')}">${e[2]}</div>` : '') +
((e[3]) ? `<div class="td left" data-title="${_('Level')}">${e[3]}</div>` : '') +
((e[4]) ? `<div class="td left" data-title="${_('Facility')}">${e[4]}</div>` : '') +
((e[5]) ? `<div class="td left log-entry-message-cell" data-title="${_('Message')}">${e[5]}</div>` : '') +
`</div>`
);
});
lines = lines.join('');
logTable.append(
E('div', { 'class': 'tr table-titles' }, [
E('div', { 'class': 'th left log-entry-number' }, '#'),
(logdataArray[0][1]) ? E('div', { 'class': 'th left log-entry-time' }, _('Timestamp')) : '',
(logdataArray[0][2]) ? E('div', { 'class': 'th left log-entry-host' }, _('Host')) : '',
(logdataArray[0][3]) ? E('div', { 'class': 'th left log-entry-log-level' }, _('Level')) : '',
(logdataArray[0][4]) ? E('div', { 'class': 'th left log-entry-facility' }, _('Facility')) : '',
(logdataArray[0][5]) ? E('div', { 'class': 'th left log-entry-message' }, _('Message')) : '',
])
);
};
try {
logTable.insertAdjacentHTML('beforeend', lines);
} catch(err) {
if(err.name === 'SyntaxError') {
ui.addNotification(null,
E('p', {}, _('HTML/XML error') + ': ' + err.message), 'error');
};
throw err;
};
let levelsStatString = '';
if((Object.values(this.logLevelsStat).reduce((s,c) => s + c, 0)) > 0) {
Object.entries(this.logLevelsStat).forEach(e => {
if(e[0] in this.logLevels && e[1] > 0) {
levelsStatString += `<span class="log-entries-count-level log-${e[0]}" title="${e[0]}">${e[1]}</span>`;
};
});
};
return E([
E('div', { 'class': 'log-entries-count' },
`${_('Entries')}: ${logdataArray.length} / ${this.totalLogLines}${levelsStatString}`
),
logTable
]);
},
downloadLog: function(ev) {
let formElems = Array.from(document.forms.logForm.elements);
formElems.forEach(e => e.disabled = true);
return this.getLogData(0).then(logdata => {
logdata = logdata || '';
let link = E('a', {
'download': this.viewName + '.log',
'href': URL.createObjectURL(
new Blob([ logdata ], { type: 'text/plain' })),
});
link.click();
URL.revokeObjectURL(link.href);
}).catch(() => {
ui.addNotification(null,
E('p', {}, _('Download error') + ': ' + err.message));
}).finally(() => {
formElems.forEach(e => e.disabled = false);
});
},
load: function() {
// Restoring settings from localStorage
let tailValueLocal = localStorage.getItem(`luci-app-${this.viewName}-tailValue`);
if(tailValueLocal) {
this.tailValue = Number(tailValueLocal);
};
let logSortingLocal = localStorage.getItem(`luci-app-${this.viewName}-logSorting`);
if(logSortingLocal) {
this.logSortingValue = logSortingLocal;
};
return this.getLogData(this.tailValue);
},
render: function(logdata) {
let logWrapper = E('div', {
'id': 'logWrapper',
'style': 'width:100%; min-height:20em; padding: 0 0 0 45px; font-size:0.9em !important'
}, this.makeLogArea(this.parseLogData(logdata, this.tailValue)));
let tailInput = E('input', {
'id': 'tailInput',
'name': 'tailInput',
'type': 'text',
'form': 'logForm',
'class': 'cbi-input-text',
'style': 'width:4em !important; min-width:4em !important',
'maxlength': 5,
});
tailInput.value = (this.tailValue === 0) ? null : this.tailValue;
ui.addValidator(tailInput, 'uinteger', true);
let tailReset = E('input', {
'type': 'button',
'form': 'logForm',
'class': 'cbi-button btn cbi-button-reset',
'value': 'Χ',
'click': ev => {
tailInput.value = null;
logFormSubmitBtn.click();
ev.target.blur();
},
'style': 'max-width:4em !important',
});
let logHostsDropdownElem = '';
let logHostsKeys = Object.keys(this.logHosts);
if(logHostsKeys.length > 0) {
this.logHostsDropdown = new ui.Dropdown(
null,
this.logHosts,
{
id: 'logHostsDropdown',
multiple: true,
select_placeholder: _('All'),
}
);
logHostsDropdownElem = E(
'div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logHostsDropdown',
}, _('Hosts')),
E('div', { 'class': 'cbi-value-field' },
this.logHostsDropdown.render()
),
]
);
};
let logLevelsDropdownElem = '';
let logLevelsKeys = Object.keys(this.logLevels);
if(logLevelsKeys.length > 0) {
this.logLevelsDropdown = new ui.Dropdown(
null,
this.logLevels,
{
id: 'logLevelsDropdown',
sort: logLevelsKeys,
multiple: true,
select_placeholder: _('All'),
}
);
logLevelsDropdownElem = E(
'div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logLevelsDropdown',
}, _('Logging levels')),
E('div', { 'class': 'cbi-value-field' },
this.logLevelsDropdown.render()
),
]
);
};
let logFilter = E('input', {
'id': 'logFilter',
'name': 'logFilter',
'type': 'text',
'form': 'logForm',
'class': 'cbi-input-text',
'placeholder': _('Type an expression...'),
});
let logFormSubmitBtn = E('input', {
'type': 'submit',
'form': 'logForm',
'class': 'cbi-button btn cbi-button-action',
'value': _('Apply'),
'click': ev => ev.target.blur(),
'style': 'margin-right: 1em',
});
let logSorting = E('select', {
'id': 'logSorting',
'name': 'logSorting',
'form': 'logForm',
'class': "cbi-input-select",
}, [
E('option', { 'value': 'asc' }, _('ascending')),
E('option', { 'value': 'desc' }, _('descending')),
]);
logSorting.value = this.logSortingValue;
let logDownloadBtn = E('button', {
'id': 'logDownloadBtn',
'name': 'logDownloadBtn',
'class': 'cbi-button btn',
'click': ui.createHandlerFn(this, this.downloadLog),
}, _('Download log'));
return E([
E('h2', { 'id': 'logTitle', 'class': 'fade-in' }, this.title),
E('div', { 'class': 'cbi-section-descr fade-in' }),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'tailInput',
}, _('Last entries')),
E('div', { 'class': 'cbi-value-field' }, [
tailInput,
tailReset,
]),
]),
logHostsDropdownElem,
logLevelsDropdownElem,
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logFilter',
}, _('Message filter')),
E('div', { 'class': 'cbi-value-field' }, logFilter),
]),
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logSorting',
}, _('Sorting entries')),
E('div', { 'class': 'cbi-value-field' }, logSorting,),
]),
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logFilter',
}, _('Refresh log')),
E('div', { 'class': 'cbi-value-field' }, [
logFormSubmitBtn,
E('form', {
'id': 'logForm',
'name': 'logForm',
'style': 'display:inline-block; margin-top:0.5em',
'submit': ui.createHandlerFn(this, function(ev) {
ev.preventDefault();
let formElems = Array.from(document.forms.logForm.elements);
formElems.forEach(e => e.disabled = true);
logDownloadBtn.disabled = true;
// Saving settings to localStorage
if(this.tailValue != tailInput.value) {
this.tailValue = (/^[0-9]+$/.test(tailInput.value)) ? tailInput.value : 0;
localStorage.setItem(
`luci-app-${this.viewName}-tailValue`, String(this.tailValue));
};
if(this.logSortingValue != logSorting.value) {
this.logSortingValue = logSorting.value;
localStorage.setItem(
`luci-app-${this.viewName}-logSorting`, this.logSortingValue);
};
let tail = (tailInput.value && tailInput.value > 0) ? tailInput.value : 0
return this.getLogData(tail).then(logdata => {
logdata = logdata || '';
logWrapper.innerHTML = '';
logWrapper.append(
this.makeLogArea(
this.setRegexpFilter(
this.setLevelFilter(
this.setHostFilter(
this.parseLogData(logdata, tail)
)
)
)
)
);
}).finally(() => {
formElems.forEach(e => e.disabled = false);
logDownloadBtn.disabled = false;
});
}),
}, E('span', {}, '&#160;')),
]),
]),
])
),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'style': 'position:fixed; z-index:1 !important' }, [
E('button', {
'class': 'btn',
'style': 'position:relative; display:block; margin:0 !important; left:1px; top:1px',
'click': ev => {
document.getElementById('logTitle').scrollIntoView(true);
ev.target.blur();
},
}, '&#8593;'),
E('button', {
'class': 'btn',
'style': 'position:relative; display:block; margin:0 !important; margin-top:1px !important; left:1px; top:1px',
'click': ev => {
logWrapper.scrollIntoView(false);
ev.target.blur();
},
}, '&#8595;'),
]),
logWrapper,
])
),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' },
E('div', { 'class': 'cbi-value' },
E('div', { 'style': 'width:100%; text-align:right !important' }, logDownloadBtn)
),
)
),
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null,
}),
})