Files
server/opt/psa/admin/htdocs/modules/panel-migrator/scripts/view-log.js
2026-01-07 20:52:11 +01:00

731 lines
27 KiB
JavaScript

// Copyright 1999-2025. WebPros International GmbH. All rights reserved.
Jsw.onReady(function () {
const appState = PanelMigrator.initAppState();
const severityLevel = {
INFO: 1,
ERROR: 2,
WARNING: 3,
DEBUG: 4,
EXCEPTION: 5,
MIGRATOR_EXECUTION: 6,
};
const severityProperties = {};
severityProperties[severityLevel.INFO] = {
title: 'Info',
rowCssClass: 'plesk-migrator-log-row-severity-info',
severityCellCssClass: 'plesk-migrator-log-cell-severity-info',
};
severityProperties[severityLevel.ERROR] = {
title: 'Error',
rowCssClass: 'plesk-migrator-log-row-severity-error',
severityCellCssClass: 'plesk-migrator-log-cell-severity-error',
};
severityProperties[severityLevel.WARNING] = {
title: 'Warning',
rowCssClass: 'plesk-migrator-log-row-severity-warning',
severityCellCssClass: 'plesk-migrator-log-cell-severity-warning',
};
severityProperties[severityLevel.DEBUG] = {
title: 'Debug',
rowCssClass: 'plesk-migrator-log-row-severity-debug',
severityCellCssClass: 'plesk-migrator-log-cell-severity-debug',
};
severityProperties[severityLevel.EXCEPTION] = {
title: 'Exception',
rowCssClass: 'plesk-migrator-log-row-severity-debug',
severityCellCssClass: 'plesk-migrator-log-cell-severity-debug',
};
severityProperties[severityLevel.MIGRATOR_EXECUTION] = {
title: 'Migrator execution',
rowCssClass: 'plesk-migrator-log-row-severity-migrator-execution',
severityCellCssClass: 'plesk-migrator-log-cell-severity-migrator-execution',
};
const LogRecords = Class.create({
initialize() {
this._records = {}; // recordId => LogRecord
this._expandedRecords = {};
this._rootRecords = [];
this._inProgressRecords = {};
this._htmlLogTable = $('migration-log-table');
this._htmlLogTableBody = this._htmlLogTable.select('tbody').first();
this._showDebug = false;
this._highlightText = null;
this._filterText = null;
},
appendRecords(recordsData) {
for (let i = 0; i < recordsData.length; i++) {
const recordData = recordsData[i];
this.appendRecord(recordData);
}
},
appendRecord(recordData) {
if (this._records[recordData.recordId]) {
const existingRecord = this._records[recordData.recordId];
this.updateRecordExpandability(existingRecord);
if (recordData.finished) {
existingRecord.finishRecord();
delete this._inProgressRecords[existingRecord.getRecordId()];
}
existingRecord.updateRecordChildErrorMark(recordData.hasChildErrors, recordData.hasChildWarnings);
return;
}
let parentRecord = null;
const rootParentRecordId = 2147483647;
if (recordData.parentRecordId != rootParentRecordId) {
parentRecord = this._records[recordData.parentRecordId];
}
const record = new LogRecord(this, recordData, parentRecord);
this._records[record.getRecordId()] = record;
if (!record.isFinished()) {
this._inProgressRecords[record.getRecordId()] = record;
}
if (parentRecord) {
parentRecord.appendChildRecord(record);
parentRecord.getHtmlEndRow().insert({ before: record.getHtmlRow() });
parentRecord.getHtmlEndRow().insert({ before: record.getHtmlEndRow() });
this.updateRecordExpandability(parentRecord);
} else {
this._htmlLogTableBody.insert(record.getHtmlRow());
this._htmlLogTableBody.insert(record.getHtmlEndRow());
this._rootRecords.push(record);
}
record.setCollapsed();
this.updateRecordExpandability(record);
record.highlightText(this._highlightText);
this.updateRecordVisibility(record);
},
expandRecord(record) {
this.expandRecordsByIds([record.getRecordId()]);
},
expandRecordsByIds(recordIds) {
const self = this;
const collapsedRecordIds = [];
for (let i = 0; i < recordIds.length; i++) {
const recordId = recordIds[i];
if (self._expandedRecords[recordId] != 1) {
collapsedRecordIds.push(recordId);
}
}
if (collapsedRecordIds.length == 0) {
return;
}
const requestFunctions = [];
// divide list of records into parts, to handle long lists of expanded records:
// when there are too many records in a single AJAX request, backend could
// fail to respond (because of memory limit on response, or max execution time of PHP or CGI).
const maxPartSize = 50;
const collapsedRecordIdsParts = divideIntoParts(collapsedRecordIds, maxPartSize);
collapsedRecordIdsParts.forEach(function (collapsedRecordIdsPart) {
requestFunctions.push(function (onSuccess) {
new Ajax.Request(URL_GET_LOG_RECORDS, {
// perform POST request as list of parent IDs could be long and exceed URL length limit
// (the problem is more actual for IIS which has more strict limits)
method: 'post',
parameters: {
'parent-ids': collapsedRecordIdsPart.join(),
},
onSuccess(response) {
const recordsData = response.responseText.evalJSON();
self.appendRecords(recordsData);
for (let i = 0; i < collapsedRecordIdsPart.length; i++) {
const recordId = collapsedRecordIdsPart[i];
self._records[recordId].setExpanded();
self._expandedRecords[recordId] = 1;
for (let j = 0; j < self._records[recordId].getChildRecords().length; j++) {
const childRecord = self._records[recordId].getChildRecords()[j];
self.updateRecordVisibility(childRecord);
}
}
},
onComplete() {
onSuccess();
},
});
});
});
callAsyncFunctionsChain(requestFunctions);
},
expandRecordsByIdsRaw(recordIds) {
const self = this;
for (let i = 0; i < recordIds.length; i++) {
const recordId = recordIds[i];
if (self._expandedRecords[recordId] == 1) {
continue;
}
if (!self._records[recordId].isExpandable()) {
continue;
}
self._records[recordId].setExpanded();
self._expandedRecords[recordId] = 1;
for (let j = 0; j < self._records[recordId].getChildRecords().length; j++) {
const childRecord = self._records[recordId].getChildRecords()[j];
self.updateRecordVisibility(childRecord);
}
}
},
collapseRecord(record) {
for (let i = 0; i < record.getChildRecords().length; i++) {
const childRecord = record.getChildRecords()[i];
for (let j = 0; j < childRecord.getChildRecords().length; j++) {
const childChildRecord = childRecord.getChildRecords()[j];
this._removeRecord(childChildRecord);
}
childRecord.removeAllChildRecords();
childRecord.hide();
childRecord.setCollapsed();
delete this._expandedRecords[childRecord.getRecordId()];
}
record.setCollapsed();
delete this._expandedRecords[record.getRecordId()];
},
_removeRecord(record) {
for (let i = 0; i < record.getChildRecords().length; i++) {
const childRecord = record.getChildRecords()[i];
this._removeRecord(childRecord);
}
delete this._records[record.getRecordId()];
record.getHtmlRow().remove();
record.getHtmlEndRow().remove();
},
toggleDebug() {
const self = this;
this._showDebug = !this._showDebug;
this._foreachRecord(function (record) {
self.updateRecordVisibility(record);
});
this._foreachRecord(function (record) {
self.updateRecordExpandability(record);
});
},
getExpandedRecordIds() {
return Object.keys(this._expandedRecords);
},
getInProgressRecords() {
return this._inProgressRecords;
},
getRootRecords() {
return this._rootRecords;
},
setHighlightText(searchText) {
const self = this;
this._highlightText = searchText;
this._foreachRecord(function (record) {
record.highlightText(searchText);
});
this._foreachRecord(function (record) {
self.updateRecordExpandability(record);
});
},
setFilterText(searchText) {
const self = this;
this._filterText = searchText;
this._foreachRecord(function (record) {
self.updateRecordVisibility(record);
});
this._foreachRecord(function (record) {
self.updateRecordExpandability(record);
});
},
updateRecordVisibility(record) {
if (this._isFiltered(record)) {
record.hide();
return;
}
if (record.getParentRecord() && !record.getParentRecord().isExpanded()) {
record.hide();
return;
}
record.show();
},
updateRecordExpandability(record) {
for (let i = 0; i < record.getChildRecords().length; i++) {
const childRecord = record.getChildRecords()[i];
if (!this._isFiltered(childRecord)) {
record.setExpandable(true);
return;
}
}
record.setExpandable(false);
},
_foreachRecord(callback) {
const self = this;
Object.keys(this._records).forEach(function (key, index) {
const record = self._records[key];
callback(record);
});
},
_isFiltered(record) {
return (
(record.isDebug() && !this._showDebug) ||
(
this._filterText &&
!(record.getMessage().toLowerCase()
.indexOf(this._filterText.toLowerCase()) > -1)
)
);
},
});
var LogRecord = Class.create({
initialize(logRecords, logRecordData, parentRecord) {
this._logRecords = logRecords;
this._childRecords = [];
this._recordId = logRecordData.recordId;
this._severity = logRecordData.severity;
this._time = logRecordData.time;
this._message = logRecordData.message;
this._finished = logRecordData.finished;
this._parentRecord = parentRecord;
this._isExpandable = false;
this._initializeHtml();
this.updateRecordChildErrorMark(logRecordData.hasChildErrors, logRecordData.hasChildWarnings);
},
_initializeHtml() {
const messageCellProperties = {
style: `white-space: pre-wrap; word-wrap: break-word; padding-left: ${this.getLevel() * 50}px`,
class: 'messageText',
};
const severityDescriptor = severityProperties[this._severity];
const rowProperties = { class: `${severityDescriptor.rowCssClass} logItemRow`, style: 'display: none' };
const severityCellProperties = {
class: severityDescriptor.severityCellCssClass,
style: 'white-space: nowrap',
};
this._htmlRow = new Element('tr', rowProperties);
this._htmlTimeCol = new Element('td', { class: 'logRecordDate' }).update(this._time.escapeHTML());
this._htmlErrorIcon = new Element(
'img', {
src: migratorImage('log/error.png'),
style: 'width: 16px; height: 16px; margin-left: 8px; display: none;',
},
);
this._htmlWarningIcon = new Element(
'img', {
src: migratorImage('log/warning.png'),
style: 'width: 16px; height: 16px; margin-left: 8px; display: none;',
},
);
this._htmlSeverityCol = new Element('td', severityCellProperties).update(
severityDescriptor.title.escapeHTML(),
)
.insert(this._htmlErrorIcon)
.insert(this._htmlWarningIcon);
this._htmlMessageCol = new Element('td', messageCellProperties);
const htmlMessageColContainer = new Element('div', { class: 'plesk-migrator-log-container-message-column' });
this._htmlWorkingContainer = new Element('div', { class: 'plesk-migrator-log-working-container' });
this._htmlExpandContainer = new Element('div', { class: 'plesk-migrator-log-expand-container' });
this._htmlMessageContainer = new Element('div', { class: 'plesk-migrator-log-message-container' });
this._htmlMessageContainer.update(this._message.escapeHTML());
htmlMessageColContainer.insert(this._htmlWorkingContainer);
htmlMessageColContainer.insert(this._htmlExpandContainer);
htmlMessageColContainer.insert(this._htmlMessageContainer);
this._htmlMessageCol.insert(htmlMessageColContainer);
this._htmlExpandIcon = new Element('img', {
src: migratorImage('log-expand-icon.gif'),
class: 'plesk-migrator-expand-image',
tabindex: 0,
alt: 'Expand',
role: 'button',
'aria-expanded': this._expanded ? 'true' : 'false',
style: 'display: none',
});
this._htmlCollapseIcon = new Element('img', {
src: migratorImage('log-collapse-icon.gif'),
class: 'plesk-migrator-expand-image',
tabindex: 0,
alt: 'Collapse',
style: 'display: none',
role: 'button',
'aria-expanded': this._expanded ? 'true' : 'false',
});
if (!this._finished) {
this._htmlWorkingIcon = new Element(
'img', {
src: migratorImage('log-record-working.gif'),
style: 'width: 16px; height: 16px;',
},
);
this._htmlWorkingContainer.insert(this._htmlWorkingIcon);
} else {
this._htmlWorkingIcon = null;
}
this._htmlExpandContainer.insert(this._htmlExpandIcon);
this._htmlExpandContainer.insert(this._htmlCollapseIcon);
[this._htmlExpandIcon, this._htmlCollapseIcon].forEach(icon => {
icon.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
icon.click();
}
});
});
this._htmlRow.insert(this._htmlTimeCol);
this._htmlRow.insert(this._htmlSeverityCol);
this._htmlRow.insert(this._htmlMessageCol);
this._expanded = false;
this._endHtmlRow = new Element('tr', { style: 'display: none' });
},
appendChildRecord(record) {
this._childRecords.push(record);
},
getChildRecords() {
return this._childRecords;
},
removeAllChildRecords() {
this._childRecords = [];
},
getHtmlRow() {
return this._htmlRow;
},
// Get fake hidden HTML row, which marks place right after the last child record.
// So, DOM tree will look like:
// <tr>This log record</tr>
// <tr>Child record #1</tr>
// <tr>Child record #2</tr>
// ...
// <tr>Child record #N (last child record)</tr>
// <tr>End record (hidden)</tr>
getHtmlEndRow() {
return this._endHtmlRow;
},
getRecordId() {
return this._recordId;
},
getMessage() {
return this._message;
},
getParentRecord() {
return this._parentRecord;
},
getLevel() {
let i = 0;
let parent = this.getParentRecord();
while (parent) {
i++;
parent = parent.getParentRecord();
}
return i;
},
hide() {
this.getHtmlRow().hide();
},
show() {
this.getHtmlRow().show();
},
setExpandable(isExpandable) {
this._isExpandable = isExpandable;
if (isExpandable) {
if (this._expanded) {
this._htmlCollapseIcon.show();
this._htmlExpandIcon.hide();
} else {
this._htmlCollapseIcon.hide();
this._htmlExpandIcon.show();
}
} else {
this._htmlCollapseIcon.hide();
this._htmlExpandIcon.hide();
}
},
isExpandable() {
return this._isExpandable;
},
setExpanded() {
const record = this;
this._htmlCollapseIcon.show();
this._htmlExpandIcon.hide();
this._htmlExpandContainer.stopObserving('click');
this._htmlExpandContainer.observe(
'click', function () {
record._logRecords.collapseRecord(record);
},
);
this._expanded = true;
},
setCollapsed() {
const record = this;
this._htmlCollapseIcon.hide();
this._htmlExpandIcon.show();
this._htmlExpandContainer.stopObserving('click');
this._htmlExpandContainer.observe(
'click', function () {
record._logRecords.expandRecord(record);
},
);
this._expanded = false;
},
finishRecord() {
this._finished = true;
if (this._htmlWorkingIcon) {
this._htmlWorkingIcon.remove();
this._htmlWorkingIcon = null;
}
},
updateRecordChildErrorMark(hasChildErrors, hasChildWarnings) {
if (hasChildErrors) {
this._htmlWarningIcon.hide();
this._htmlErrorIcon.show();
} else if (hasChildWarnings) {
this._htmlWarningIcon.show();
}
},
isFinished() {
return this._finished;
},
isExpanded() {
return this._expanded;
},
isDebug() {
return this._severity == severityLevel.DEBUG || this._severity == severityLevel.EXCEPTION;
},
highlightText(searchText) {
this._htmlMessageContainer.update(highlight(this._message, searchText));
},
});
function divideIntoParts(list, maxPartSize) {
const parts = [];
for (let i = 0; i < list.length; i += maxPartSize) {
parts.push(list.slice(i, i + maxPartSize));
}
return parts;
}
// Call chain of asynchronous functions, one-by-one: the first function is called, when it completes its
// async execution, the second function is called, when it completes its async execution, the third function
// is called, and so on.
// "functions" argument is a list, each item is a function, which accepts single argument - callback,
// which must be called by the function once it completes its async execution.
function callAsyncFunctionsChain(functions) {
if (functions.length == 0) {
return;
}
let functionId = 0;
function callNextRequestFunction() {
if (functionId >= functions.length) {
return;
}
functions[functionId](function () {
functionId++;
setTimeout(function () {
callNextRequestFunction();
}, 0);
});
}
callNextRequestFunction();
}
// Update log periodically with AJAX
function periodicUpdateLog() {
if (!appState.mounted) {
return;
}
const rootParentRecordId = 2147483647;
let lastRootRecordId = rootParentRecordId;
const rootRecords = logRecords.getRootRecords();
if (rootRecords.length > 0) {
lastRootRecordId = rootRecords[rootRecords.length - 1].getRecordId();
}
const recordIdPairs = [
`${rootParentRecordId}.${lastRootRecordId}`,
];
const inProgressRecs = logRecords.getInProgressRecords();
for (const recId in inProgressRecs) {
if (inProgressRecs.hasOwnProperty(recId)) {
const inProgressRecord = inProgressRecs[recId];
const childRecords = inProgressRecord.getChildRecords();
let lastChildRecordId = rootParentRecordId;
if (childRecords.length > 0) {
lastChildRecordId = childRecords[childRecords.length - 1].getRecordId();
}
recordIdPairs.push(`${inProgressRecord.getRecordId()}.${lastChildRecordId}`);
}
}
let url = URL_GET_LOG_UPDATES;
if ($('automaticExpandBottom').checked) {
url += '/get-all-new-records/true';
}
new Ajax.Request(url, {
// perform POST request as list of parent IDs could be long and exceed URL length limit
// (the problem is more actual for IIS which has more strict limits)
method: 'post',
parameters: {
'in-progress-record-ids': recordIdPairs.join(','),
},
onSuccess(response) {
if (!appState.mounted) {
return;
}
const recordsData = response.responseText.evalJSON();
if (recordsData.length > 0) {
logRecords.appendRecords(recordsData);
if ($('automaticExpandBottom').checked) {
const recordIds = [];
recordsData.forEach(function (recordData) {
recordIds.push(recordData.recordId);
});
logRecords.expandRecordsByIdsRaw(recordIds);
}
if ($('automaticScrollBottom').checked) {
scrollToBottom();
}
}
},
onComplete() {
setTimeout(function () {
periodicUpdateLog();
}, 2000);
},
});
}
function scrollToBottom() {
window.scrollTo(0, document.body.scrollHeight);
}
function synchronizeScrollCheckboxes() {
$('automaticScrollTop').observe('change', function (e) {
$('automaticScrollBottom').checked = $('automaticScrollTop').checked;
});
$('automaticScrollBottom').observe('change', function (e) {
$('automaticScrollTop').checked = $('automaticScrollBottom').checked;
});
}
function observeDebugCheckboxes() {
$('showDebugTop').observe('change', function (e) {
$('showDebugBottom').checked = $('showDebugTop').checked;
logRecords.toggleDebug();
});
$('showDebugBottom').observe('change', function (e) {
$('showDebugTop').checked = $('showDebugBottom').checked;
logRecords.toggleDebug();
});
}
function synchronizeAutomaticExpandCheckboxes() {
$('automaticExpandTop').observe('change', function (e) {
$('automaticExpandBottom').checked = $('automaticExpandTop').checked;
});
$('automaticExpandBottom').observe('change', function (e) {
$('automaticExpandTop').checked = $('automaticExpandBottom').checked;
});
}
function observeSearch() {
function startSearch(searchText) {
if (!searchText) {
return;
}
new Ajax.Request(URL_SEARCH_LOG_RECORDS, {
parameters: {
'search-text': searchText,
},
onSuccess(response) {
const parentRecordIds = response.responseText.evalJSON();
logRecords.expandRecordsByIds(parentRecordIds);
},
});
}
$('buttonSearch').observe('click', function () {
const searchText = $('searchText').value;
logRecords.setHighlightText(searchText);
logRecords.setFilterText('');
startSearch(searchText);
});
$('buttonFilter').observe('click', function () {
const searchText = $('searchText').value;
logRecords.setHighlightText(searchText);
logRecords.setFilterText(searchText);
startSearch(searchText);
});
}
function highlight(text, search) {
if (!search) {
return text.escapeHTML();
}
const startIndex = text.toLowerCase().indexOf(search.toLowerCase());
if (startIndex === -1) {
return text.escapeHTML();
}
return (
`${text.substr(0, startIndex).escapeHTML()
}<span class="plesk-migrator-log-search-highlight">${
text.substr(startIndex, search.length).escapeHTML()
}</span>${
highlight(text.substr(startIndex + search.length), search)}`
);
}
var logRecords = new LogRecords();
logRecords.appendRecords(INITIAL_LOG_RECORDS);
synchronizeScrollCheckboxes();
observeDebugCheckboxes();
synchronizeAutomaticExpandCheckboxes();
observeSearch();
periodicUpdateLog();
});