// 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: // This log record // Child record #1 // Child record #2 // ... // Child record #N (last child record) // End record (hidden) 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() }${ text.substr(startIndex, search.length).escapeHTML() }${ highlight(text.substr(startIndex + search.length), search)}` ); } var logRecords = new LogRecords(); logRecords.appendRecords(INITIAL_LOG_RECORDS); synchronizeScrollCheckboxes(); observeDebugCheckboxes(); synchronizeAutomaticExpandCheckboxes(); observeSearch(); periodicUpdateLog(); });