You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@brooklyn.apache.org by GitBox <gi...@apache.org> on 2021/07/15 13:34:54 UTC

[GitHub] [brooklyn-ui] algairim opened a new pull request #249: WIP Logbook widget

algairim opened a new pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] asfgit closed pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
asfgit closed pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r671174878



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['datetime', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'datetime',   selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
+
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {

Review comment:
       This is addressed in 3cd18d2f7f1e32424040ea1a88654f1c63c88ebe now




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] jathanasiou commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
jathanasiou commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r672136763



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -210,36 +285,30 @@ export function logbook() {
         }
 
         /**
-         * Converts log entries to string.
+         * Extracts timestamp from the log entry.
          *
-         * @param {Array.<Object>} logEntries The log entries to convert.
-         * @returns {String} log entries converted to string.
+         * @param logEntry The log entry.
+         * @returns {number} The extracted date-time.
          */
-        function covertLogEntriesToString(logEntries) {
-            let output = [];
-            const fieldsToShow = getCheckedBoxes($scope.logFields);
-            logEntries.forEach(entry => {
-                let outputLine = [];
-                if (fieldsToShow.includes('datetime') && entry.timestamp)
-                    outputLine.push(entry.timestamp);
-                if (fieldsToShow.includes('taskId') && entry.taskId)
-                    outputLine.push(entry.taskId);
-                if (fieldsToShow.includes('entityIds') && entry.entityIds)
-                    outputLine.push(entry.entityIds);
-                if (fieldsToShow.includes('level') && entry.level)
-                    outputLine.push(entry.level);
-                if (fieldsToShow.includes('bundleId') && entry.bundleId)
-                    outputLine.push(entry.bundleId);
-                if (fieldsToShow.includes('class') && entry.class)
-                    outputLine.push(entry.class);
-                if (fieldsToShow.includes('threadName') && entry.threadName)
-                    outputLine.push(entry.threadName);
-                if (fieldsToShow.includes('message') && entry.message)
-                    outputLine.push(entry.message);
-
-                output.push(outputLine.join(' '));
-            })
-            return output.length > 0 ? output.join('\n') : 'No results';
+        function getLogEntryTimestamp(logEntry) {
+            return Date.parse(logEntry.timestamp.replace(',', '.'))
+        }
+
+        /**
+         * Extracts UTC timestamp from the date.
+         *
+         * @param {Date|number} date The date to get UTC timestamp of.
+         * @returns {number|undefined} The UTC timestamp.
+         */
+        function getUtcTimestamp(date) {
+            const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
+            if (date instanceof Date) {
+                return date.valueOf() - timezoneOffsetMs;
+            } else if (typeof date === 'number') {
+                return date - timezoneOffsetMs;
+            } else {
+                return undefined;

Review comment:
       Return undefined is a needless statement for JS functions.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r672141531



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -210,36 +285,30 @@ export function logbook() {
         }
 
         /**
-         * Converts log entries to string.
+         * Extracts timestamp from the log entry.
          *
-         * @param {Array.<Object>} logEntries The log entries to convert.
-         * @returns {String} log entries converted to string.
+         * @param logEntry The log entry.
+         * @returns {number} The extracted date-time.
          */
-        function covertLogEntriesToString(logEntries) {
-            let output = [];
-            const fieldsToShow = getCheckedBoxes($scope.logFields);
-            logEntries.forEach(entry => {
-                let outputLine = [];
-                if (fieldsToShow.includes('datetime') && entry.timestamp)
-                    outputLine.push(entry.timestamp);
-                if (fieldsToShow.includes('taskId') && entry.taskId)
-                    outputLine.push(entry.taskId);
-                if (fieldsToShow.includes('entityIds') && entry.entityIds)
-                    outputLine.push(entry.entityIds);
-                if (fieldsToShow.includes('level') && entry.level)
-                    outputLine.push(entry.level);
-                if (fieldsToShow.includes('bundleId') && entry.bundleId)
-                    outputLine.push(entry.bundleId);
-                if (fieldsToShow.includes('class') && entry.class)
-                    outputLine.push(entry.class);
-                if (fieldsToShow.includes('threadName') && entry.threadName)
-                    outputLine.push(entry.threadName);
-                if (fieldsToShow.includes('message') && entry.message)
-                    outputLine.push(entry.message);
-
-                output.push(outputLine.join(' '));
-            })
-            return output.length > 0 ? output.join('\n') : 'No results';
+        function getLogEntryTimestamp(logEntry) {
+            return Date.parse(logEntry.timestamp.replace(',', '.'))
+        }
+
+        /**
+         * Extracts UTC timestamp from the date.
+         *
+         * @param {Date|number} date The date to get UTC timestamp of.
+         * @returns {number|undefined} The UTC timestamp.
+         */
+        function getUtcTimestamp(date) {
+            const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
+            if (date instanceof Date) {
+                return date.valueOf() - timezoneOffsetMs;
+            } else if (typeof date === 'number') {
+                return date - timezoneOffsetMs;
+            } else {
+                return undefined;

Review comment:
       I would prefer to keep it explicit.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r671245417



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,235 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['timestamp', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'timestamp',  selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
+
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {
+            return getCheckedBoxes($scope.logFields).reduce((output, fieldKey) => {
+                    if (entry[fieldKey]) {
+                        output.push(entry[fieldKey])
+                    }
+                    return output;
+                }, []).join(' ');
+        }
+
+        /**
+         * Caches the datetime of the first item in the visible area of the query result.
+         */
+        function cacheDatetimeToScrollTo() {
+            let element = Array.from($element.find('pre')).find(item => item.offsetTop > (autoScrollableElement.scrollTop + autoScrollableElement.offsetTop - 1));
+            let firstLogEntryInTheVisibleArea = $scope.logEntries.find(item => item.id === element.id);
+            if (firstLogEntryInTheVisibleArea) {
+                datetimeToScrollTo = firstLogEntryInTheVisibleArea.datetime;
+            }
+        }
+
+        /**
+         * @returns {boolean} true if current query is a tail request, false otherwise.
+         */
+        function isTail() {
+            return $scope.search.latest && !$scope.search.dateTimeTo;
+        }
 
         /**
          * Performs a logbook query.
          */
         function doQuery() {
-            const levels = $scope.allLevels ? ['ALL'] : getCheckedBoxes($scope.logLevels);
+
+            if (!vm.isValidNumber()) {
+                console.error('number of items is invalid', $scope.search.numberOfItems)
+                return;
+            }
+
+            const levels = getCheckedBoxes($scope.search.logLevels);
 
             const params = {
                 levels: levels,
-                tail: $scope.isLatest,
-                searchPhrase: $scope.searchPhrase,
-                numberOfItems: $scope.numberOfItems,
-                dateTimeFrom: $scope.isLatest ? dateTimeToAutoUpdateFrom : $scope.dateTimeFrom,
-                dateTimeTo: $scope.isLatest ? '' : $scope.dateTimeTo,
+                tail: $scope.search.latest,
+                searchPhrase: $scope.search.phrase,
+                numberOfItems: $scope.search.numberOfItems,
+                dateTimeFrom: isTail() && !isNewQueryParameters ? dateTimeToAutoRefreshFrom : $scope.search.dateTimeFrom,
+                dateTimeTo: $scope.search.dateTimeTo,
             }
 
+            cacheDatetimeToScrollTo();
+
             logbookApi.logbookQuery(params, true).then((logEntries) => {
 
-                if ($scope.isLatest && $scope.logEntries.length !== 0) {
-                    if (logEntries.length > 0) {
+                // Assign unique IDs for new log entries.
+                logEntries.forEach(item => item.id = generateLogEntryId());

Review comment:
       Looks like we need proper IDs assigned in the backend data model to make it reliable, confirmed with @jathanasiou 




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] ahgittin commented on pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
ahgittin commented on pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#issuecomment-882439101


   LGTM


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] ahgittin commented on pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
ahgittin commented on pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#issuecomment-882439101


   LGTM


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r671173592



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['datetime', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'datetime',   selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
+
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {
+            let fieldsToShow = getCheckedBoxes($scope.logFields);
+            let outputLine = [];
+            if (fieldsToShow.includes('datetime') && entry.timestamp)
+                outputLine.push(entry.timestamp);
+            if (fieldsToShow.includes('taskId') && entry.taskId)
+                outputLine.push(entry.taskId);
+            if (fieldsToShow.includes('entityIds') && entry.entityIds)
+                outputLine.push(entry.entityIds);
+            if (fieldsToShow.includes('level') && entry.level)
+                outputLine.push(entry.level);
+            if (fieldsToShow.includes('bundleId') && entry.bundleId)
+                outputLine.push(entry.bundleId);
+            if (fieldsToShow.includes('class') && entry.class)
+                outputLine.push(entry.class);
+            if (fieldsToShow.includes('threadName') && entry.threadName)
+                outputLine.push(entry.threadName);
+            if (fieldsToShow.includes('message') && entry.message)
+                outputLine.push(entry.message);
+
+            return outputLine.join(' ');
+        };
+
+        /**
+         * Caches the datetime of the first item in the visible area of the query result.
+         */
+        function cacheDatetimeToScrollTo() {
+            let element = Array.from($element.find('pre')).find(item => item.offsetTop > (autoScrollableElement.scrollTop + autoScrollableElement.offsetTop - 1));

Review comment:
       Same as above, I'm open to ideas on how to avoid parsing the DOM.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r671172091



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['datetime', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'datetime',   selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
+
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {

Review comment:
       Good catch.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] asfgit closed pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
asfgit closed pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r671165588



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));

Review comment:
       Element object is required here to perform a scroll action on entire log section, not just on a single log entry (log line), when needed. I take any suggestion(s) on how to perform scroll action by using data model, instead of searching element in the DOM.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r671172890



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['datetime', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'datetime',   selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
+
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {
+            let fieldsToShow = getCheckedBoxes($scope.logFields);
+            let outputLine = [];
+            if (fieldsToShow.includes('datetime') && entry.timestamp)
+                outputLine.push(entry.timestamp);
+            if (fieldsToShow.includes('taskId') && entry.taskId)
+                outputLine.push(entry.taskId);
+            if (fieldsToShow.includes('entityIds') && entry.entityIds)
+                outputLine.push(entry.entityIds);
+            if (fieldsToShow.includes('level') && entry.level)
+                outputLine.push(entry.level);
+            if (fieldsToShow.includes('bundleId') && entry.bundleId)
+                outputLine.push(entry.bundleId);
+            if (fieldsToShow.includes('class') && entry.class)
+                outputLine.push(entry.class);
+            if (fieldsToShow.includes('threadName') && entry.threadName)
+                outputLine.push(entry.threadName);
+            if (fieldsToShow.includes('message') && entry.message)
+                outputLine.push(entry.message);
+
+            return outputLine.join(' ');
+        };
+
+        /**
+         * Caches the datetime of the first item in the visible area of the query result.
+         */
+        function cacheDatetimeToScrollTo() {
+            let element = Array.from($element.find('pre')).find(item => item.offsetTop > (autoScrollableElement.scrollTop + autoScrollableElement.offsetTop - 1));
+            let firstLogEntryInTheVisibleArea = $scope.logEntries.find(item => item.id === element.id);
+            if (firstLogEntryInTheVisibleArea) {
+                datetimeToScrollTo = firstLogEntryInTheVisibleArea.datetime;
+            }
+        }
+
+        /**
+         * @returns {boolean} true if current query is a tail request, false otherwise.
+         */
+        function isTail() {
+            return $scope.search.latest && !$scope.search.dateTimeTo;
+        }
 
         /**
          * Performs a logbook query.
          */
         function doQuery() {
-            const levels = $scope.allLevels ? ['ALL'] : getCheckedBoxes($scope.logLevels);
+
+            if (!vm.isValidNumber()) {
+                console.error('number of items is invalid', $scope.search.numberOfItems)
+                return;
+            }
+
+            const levels = getCheckedBoxes($scope.search.logLevels);
 
             const params = {
                 levels: levels,
-                tail: $scope.isLatest,
-                searchPhrase: $scope.searchPhrase,
-                numberOfItems: $scope.numberOfItems,
-                dateTimeFrom: $scope.isLatest ? dateTimeToAutoUpdateFrom : $scope.dateTimeFrom,
-                dateTimeTo: $scope.isLatest ? '' : $scope.dateTimeTo,
+                tail: $scope.search.latest,
+                searchPhrase: $scope.search.phrase,
+                numberOfItems: $scope.search.numberOfItems,
+                dateTimeFrom: isTail() && !isNewQueryParameters ? dateTimeToAutoRefreshFrom : $scope.search.dateTimeFrom,
+                dateTimeTo: $scope.search.dateTimeTo,
             }
 
+            cacheDatetimeToScrollTo();
+
             logbookApi.logbookQuery(params, true).then((logEntries) => {
 
-                if ($scope.isLatest && $scope.logEntries.length !== 0) {
-                    if (logEntries.length > 0) {
+                // Assign unique IDs for new log entries.
+                logEntries.forEach(item => item.id = generateLogEntryId());
+
+                if (logEntries.length > 0 && isTail() && $scope.autoRefresh && !isNewQueryParameters) {
 
-                        // Calculate date-time to display up to. Note, calendar does not take into account milliseconds,
-                        // round down to seconds.
-                        let latestDateTimeToDisplay = Math.floor(logEntries.slice(-1)[0].datetime / DEFAULT_NUMBER_OF_ITEMS) * DEFAULT_NUMBER_OF_ITEMS;
+                    // Calculate date-time to display up to. Note, calendar does not take into account milliseconds, round down to seconds.
+                    let dateTimeOfLastLogEntry = Math.floor(logEntries.slice(-1)[0].datetime / DEFAULT_NUMBER_OF_ITEMS) * DEFAULT_NUMBER_OF_ITEMS;
+                    let dateTimeFrom = new Date($scope.search.dateTimeFrom).getTime();
+
+                    if (dateTimeOfLastLogEntry > dateTimeFrom) {
 
                         // Display new log entries.
-                        let newLogEntries = logEntries.filter(entry => entry.datetime <= latestDateTimeToDisplay);
+                        let newLogEntries = logEntries.filter(({datetime}) => datetime <= dateTimeOfLastLogEntry);
                         $scope.logEntries = $scope.logEntries.concat(newLogEntries).slice(-DEFAULT_NUMBER_OF_ITEMS);
 
-                        // Cache next date-time to query tail from.
-                        dateTimeToAutoUpdateFrom = new Date(latestDateTimeToDisplay);
+                        // Optimize auto-refresh: cache next date-time to query tail from, if it is still a tail.
+                        dateTimeToAutoRefreshFrom = dateTimeOfLastLogEntry;
+
                     } else {
-                        // Or re-set the cache.
-                        dateTimeToAutoUpdateFrom = '';
+                        // Or re-set the cached value.
+                        dateTimeToAutoRefreshFrom = '';
                     }
                 } else {
                     $scope.logEntries = logEntries;
                 }
 
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
-                scrollToMostRecentRecords();
+                // Auto-scroll.
+                if ($scope.logEntries.length > 0) {
+                    if ($scope.isAutoScrollDown) {
+                        scrollToMostRecentLogEntry();
+                    } else if (datetimeToScrollTo && datetimeToScrollTo >= $scope.logEntries[0].datetime) {
+                        scrollToLogEntryWithDateTime(datetimeToScrollTo);
+                    }
+                }
+
+                // Re-set marker for search parameters changes after auto-scroll.
+                isNewQueryParameters = false;
+
+                if ($scope.logEntries.length === 0) {

Review comment:
       No, `isNewQueryParameters` must be reset before that, regardless of query result.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#issuecomment-881228848


   Log level colors match with the streams in app-inspector, for consistency.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on pull request #249: WIP Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#issuecomment-881215425


   <img width="1310" alt="Screenshot 2021-07-16 at 07 39 48" src="https://user-images.githubusercontent.com/81319331/125903596-31617587-a379-41b1-97b8-89b79b871a52.png">
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] jathanasiou commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
jathanasiou commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r671138890



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['datetime', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'datetime',   selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
+
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {

Review comment:
       Could be written as 
   
   ```
   vm.covertLogEntryToString = (entry) => getCheckedBoxes(entry)
       .reduce((output, fieldKey) => {
           if (entry[fieldKey]) output.push(entry[fieldKey])
           return output;
       }, []);
   ```

##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));

Review comment:
       `$element` is a jQuery-like selector for HTML elements. In general, relying on DOM is considered inefficient and error prone (eg if element type is changes from <pre> to <div> this breaks).
   
   In general a programmatic/JS scope should be used as the source of truth. In this particular case it seems that we render them based on this logic:
   
   ```
   <pre class="logbook-item" ng-repeat="item in logEntries track by item.id ....
   ```
   
   Meaning that this line could be using `$scope.logEntries` instead of trying to parse the DOM.

##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['datetime', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'datetime',   selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
+
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {
+            let fieldsToShow = getCheckedBoxes($scope.logFields);
+            let outputLine = [];
+            if (fieldsToShow.includes('datetime') && entry.timestamp)
+                outputLine.push(entry.timestamp);
+            if (fieldsToShow.includes('taskId') && entry.taskId)
+                outputLine.push(entry.taskId);
+            if (fieldsToShow.includes('entityIds') && entry.entityIds)
+                outputLine.push(entry.entityIds);
+            if (fieldsToShow.includes('level') && entry.level)
+                outputLine.push(entry.level);
+            if (fieldsToShow.includes('bundleId') && entry.bundleId)
+                outputLine.push(entry.bundleId);
+            if (fieldsToShow.includes('class') && entry.class)
+                outputLine.push(entry.class);
+            if (fieldsToShow.includes('threadName') && entry.threadName)
+                outputLine.push(entry.threadName);
+            if (fieldsToShow.includes('message') && entry.message)
+                outputLine.push(entry.message);
+
+            return outputLine.join(' ');
+        };
+
+        /**
+         * Caches the datetime of the first item in the visible area of the query result.
+         */
+        function cacheDatetimeToScrollTo() {
+            let element = Array.from($element.find('pre')).find(item => item.offsetTop > (autoScrollableElement.scrollTop + autoScrollableElement.offsetTop - 1));

Review comment:
       Same as above, DOM parsing should be avoided when a rich framework like Angular does the state management in programatic context.

##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -39,168 +39,249 @@ export function logbook() {
 
         const DEFAULT_NUMBER_OF_ITEMS = 1000;
 
-        $scope.isAutoScroll = true; // Auto-scroll by default.
-        $scope.isLatest = true; // Indicates whether to query tail (last number of lines) or head (by default).
-        $scope.autoUpdate = false;
-        $scope.waitingResponse = false;
-
-        $scope.logtext = '';
-        $scope.logEntries = [];
-
         let vm = this;
         let refreshFunction = null;
-        let autoScrollableElements = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable'));
-        let dateTimeToAutoUpdateFrom = ''; // TODO: use this date to optimize 'tail' queries to reduce the network traffic.
-
-        // Set up cancellation of auto-scrolling on scrolling up.
-        autoScrollableElements.forEach(item => {
-            if (item.addEventListener) {
-                let wheelHandler = () => {
-                    $scope.$apply(() => {
-                        $scope.isAutoScroll = (item.scrollTop + item.offsetHeight) >= item.scrollHeight;
-                    });
-                }
-                // Chrome, Safari, Opera
-                item.addEventListener("mousewheel", wheelHandler, false);
-                // Firefox
-                item.addEventListener("DOMMouseScroll", wheelHandler, false);
-            }
-        });
-
-        vm.queryTail = () => {
-            let autoUpdate = !$scope.autoUpdate; // Calculate new value.
-
-            if (autoUpdate) {
-                $scope.isAutoScroll = true;
-                resetQueryParameters();
-                doQuery();
+        let autoScrollableElement = Array.from($element.find('pre')).find(item => item.classList.contains('auto-scrollable'));
+        let isNewQueryParameters = true; // Fresh start, new parameters!
+        let dateTimeToAutoRefreshFrom = '';
+        let datetimeToScrollTo = null;
+
+        // Set up cancellation of auto-scrolling down.
+        if (autoScrollableElement.addEventListener) {
+            let wheelHandler = () => {
+                $scope.$apply(() => {
+                    $scope.isAutoScrollDown = (autoScrollableElement.scrollTop + autoScrollableElement.offsetHeight) >= autoScrollableElement.scrollHeight;
+                });
             }
-
-            $scope.autoUpdate = autoUpdate; // Set new value.
+            // Chrome, Safari, Opera
+            autoScrollableElement.addEventListener("mousewheel", wheelHandler, false);
+            // Firefox
+            autoScrollableElement.addEventListener("DOMMouseScroll", wheelHandler, false);
         }
 
-        vm.queryHead = () => {
-            $scope.waitingResponse = true;
-            $scope.autoUpdate = false;
-            $scope.isLatest = false;
-            $scope.logtext = 'Loading...';
-            doQuery();
-        }
-
-        $scope.$watch('allLevels', (value) => {
-            if (!value) {
-                if (getCheckedBoxes($scope.logLevels).length === 0) {
-                    $scope.allLevels = true;
-                } else {
-                    return;
-                }
-            }
-            for (let i = 0; i < $scope.logLevels.length; ++i) {
-                $scope.logLevels[i].selected = false;
-            }
-        });
+        $scope.isAutoScrollDown = true; // Auto-scroll down, by default.
+        $scope.autoRefresh = false;
+        $scope.waitingResponse = false;
+        $scope.logtext = '';
+        $scope.wordwrap = true;
+        $scope.logEntries = [];
+        $scope.minNumberOfItems = 1;
+        $scope.maxNumberOfItems = 10000;
+
+        // Initialize search parameters.
+        $scope.search = {
+            logLevels: [
+                {name: 'Info',  value: 'INFO',  selected: true},
+                {name: 'Warn',  value: 'WARN',  selected: true},
+                {name: 'Error', value: 'ERROR', selected: true},
+                {name: 'Fatal', value: 'FATAL', selected: true},
+                {name: 'Debug', value: 'DEBUG', selected: true},
+            ],
+            latest: true,
+            dateTimeFrom: '',
+            dateTimeTo: '',
+            numberOfItems: DEFAULT_NUMBER_OF_ITEMS,
+            phrase: ''
+        };
+
+        // Define search result filters.
+        $scope.fieldsToShow = ['datetime', 'class', 'message']
+        $scope.logFields = [
+            {name: 'Timestamp',   value: 'datetime',   selected: true},
+            {name: 'Task ID',     value: 'taskId',     selected: false},
+            {name: 'Entity IDs',  value: 'entityIds',  selected: false},
+            {name: 'Log level',   value: 'level',      selected: true},
+            {name: 'Bundle ID',   value: 'bundleId',   selected: false},
+            {name: 'Class',       value: 'class',      selected: true},
+            {name: 'Thread name', value: 'threadName', selected: false},
+            {name: 'Message',     value: 'message',    selected: true},
+        ];
 
-        $scope.$watch('logLevels', (newVal, oldVal) => {
-            let selected = newVal.reduce(function (s, c) {
-                return s + (c.selected ? 1 : 0);
-            }, 0);
-            if (selected === newVal.length || selected === 0) {
-                $scope.allLevels = true;
-            } else if (selected > 0) {
-                $scope.allLevels = false;
+        // Watch for search parameters changes.
+        $scope.$watch('search', () => {
+            // Restart the auto-refresh.
+            if ($scope.autoRefresh) {
+                stopAutoRefresh();
+                vm.singleQuery();
+                startAutoRefresh();
             }
         }, true);
 
-        $scope.$watch('logFields', (newVal, oldVal) => {
-            if ($scope.logEntries !== "") {
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
+        $scope.$watch('search.latest', () => {
+            datetimeToScrollTo = null;
+            if ($scope.search.latest) {
+                scrollToMostRecentLogEntry();
+            } else {
+                scrollToFirstLogEntry();
             }
         }, true);
 
-        $scope.$watch('autoUpdate', ()=> {
-            if ($scope.autoUpdate) {
-                refreshFunction = $interval(doQuery, 1000);
+        // Watch for auto-update events.
+        $scope.$watch('autoRefresh', () => {
+            if ($scope.autoRefresh) {
+                startAutoRefresh();
             } else {
-                cancelAutoUpdate();
+                stopAutoRefresh();
             }
         });
 
-        $scope.$on('$destroy', cancelAutoUpdate);
+        $scope.$on('$destroy', stopAutoRefresh);
+
+        /**
+         * @returns {boolean} True if number of items is a number and within a supported range, false otherwise.
+         */
+        vm.isValidNumber =() => {
+            return $scope.search.numberOfItems >= $scope.minNumberOfItems && $scope.search.numberOfItems <= $scope.maxNumberOfItems;
+        }
+
+        /**
+         * Handles the click event on the log entry.
+         *
+         * @param {Object} logEntry The clicked log entry data.
+         */
+        vm.logEntryOnClick = (logEntry) => {
+            pinLogEntry(logEntry);
+        };
+
+        /**
+         * Starts an auto-query. Performs new query each time search parameters change.
+         */
+        vm.autoQuery = () => {
+            let autoRefresh = !$scope.autoRefresh; // Calculate new value first.
 
-        // Watch the 'isAutoScroll' and auto-scroll down if enabled.
-        $scope.$watch('isAutoScroll', () => {
-            if ($scope.isAutoScroll) {
-                scrollToMostRecentRecords();
+            if (autoRefresh) {
+                $scope.isAutoScrollDown = true;
+                doQuery();
             }
-        });
 
-        // Initialize query parameters, reset them.
-        resetQueryParameters();
-
-        // Initialize the reset of search parameters.
-        $scope.allLevels = true
-        $scope.logLevels = [
-            {"name": "Info", "value": "INFO", "selected": false},
-            {"name": "Warn", "value": "WARN", "selected": false},
-            {"name": "Error", "value": "ERROR", "selected": false},
-            {"name": "Fatal", "value": "FATAL", "selected": false},
-            {"name": "Debug", "value": "DEBUG", "selected": false},
-        ];
-        $scope.searchPhrase = '';
+            $scope.autoRefresh = autoRefresh; // Now, set the new value.
+        };
 
-        // Initialize filters.
-        $scope.fieldsToShow = ['datetime', 'class', 'message']
-        $scope.logFields = [
-            {"name": "Timestamp", "value": "datetime", "selected": true},
-            {"name": "Task ID", "value": "taskId", "selected": false},
-            {"name": "Entity IDs", "value": "entityIds", "selected": false},
-            {"name": "Log level", "value": "level", "selected": true},
-            {"name": "Bundle ID", "value": "bundleId", "selected": false},
-            {"name": "Class", "value": "class", "selected": true},
-            {"name": "Thread name", "value": "threadName", "selected": false},
-            {"name": "Message", "value": "message", "selected": true},
-        ];
+        /**
+         * Performs a single query with search parameters selected.
+         */
+        vm.singleQuery = () => {
+            isNewQueryParameters = true;
+            $scope.waitingResponse = true;
+            $scope.logtext = 'Loading...';
+            $scope.logEntries = [];
+            doQuery();
+        };
+
+        /**
+         * Converts log entry to string.
+         *
+         * @param {Object} entry The log entry to convert.
+         * @returns {String} log entry converted to string.
+         */
+        vm.covertLogEntryToString = (entry) => {
+            let fieldsToShow = getCheckedBoxes($scope.logFields);
+            let outputLine = [];
+            if (fieldsToShow.includes('datetime') && entry.timestamp)
+                outputLine.push(entry.timestamp);
+            if (fieldsToShow.includes('taskId') && entry.taskId)
+                outputLine.push(entry.taskId);
+            if (fieldsToShow.includes('entityIds') && entry.entityIds)
+                outputLine.push(entry.entityIds);
+            if (fieldsToShow.includes('level') && entry.level)
+                outputLine.push(entry.level);
+            if (fieldsToShow.includes('bundleId') && entry.bundleId)
+                outputLine.push(entry.bundleId);
+            if (fieldsToShow.includes('class') && entry.class)
+                outputLine.push(entry.class);
+            if (fieldsToShow.includes('threadName') && entry.threadName)
+                outputLine.push(entry.threadName);
+            if (fieldsToShow.includes('message') && entry.message)
+                outputLine.push(entry.message);
+
+            return outputLine.join(' ');
+        };
+
+        /**
+         * Caches the datetime of the first item in the visible area of the query result.
+         */
+        function cacheDatetimeToScrollTo() {
+            let element = Array.from($element.find('pre')).find(item => item.offsetTop > (autoScrollableElement.scrollTop + autoScrollableElement.offsetTop - 1));
+            let firstLogEntryInTheVisibleArea = $scope.logEntries.find(item => item.id === element.id);
+            if (firstLogEntryInTheVisibleArea) {
+                datetimeToScrollTo = firstLogEntryInTheVisibleArea.datetime;
+            }
+        }
+
+        /**
+         * @returns {boolean} true if current query is a tail request, false otherwise.
+         */
+        function isTail() {
+            return $scope.search.latest && !$scope.search.dateTimeTo;
+        }
 
         /**
          * Performs a logbook query.
          */
         function doQuery() {
-            const levels = $scope.allLevels ? ['ALL'] : getCheckedBoxes($scope.logLevels);
+
+            if (!vm.isValidNumber()) {
+                console.error('number of items is invalid', $scope.search.numberOfItems)
+                return;
+            }
+
+            const levels = getCheckedBoxes($scope.search.logLevels);
 
             const params = {
                 levels: levels,
-                tail: $scope.isLatest,
-                searchPhrase: $scope.searchPhrase,
-                numberOfItems: $scope.numberOfItems,
-                dateTimeFrom: $scope.isLatest ? dateTimeToAutoUpdateFrom : $scope.dateTimeFrom,
-                dateTimeTo: $scope.isLatest ? '' : $scope.dateTimeTo,
+                tail: $scope.search.latest,
+                searchPhrase: $scope.search.phrase,
+                numberOfItems: $scope.search.numberOfItems,
+                dateTimeFrom: isTail() && !isNewQueryParameters ? dateTimeToAutoRefreshFrom : $scope.search.dateTimeFrom,
+                dateTimeTo: $scope.search.dateTimeTo,
             }
 
+            cacheDatetimeToScrollTo();
+
             logbookApi.logbookQuery(params, true).then((logEntries) => {
 
-                if ($scope.isLatest && $scope.logEntries.length !== 0) {
-                    if (logEntries.length > 0) {
+                // Assign unique IDs for new log entries.
+                logEntries.forEach(item => item.id = generateLogEntryId());
+
+                if (logEntries.length > 0 && isTail() && $scope.autoRefresh && !isNewQueryParameters) {
 
-                        // Calculate date-time to display up to. Note, calendar does not take into account milliseconds,
-                        // round down to seconds.
-                        let latestDateTimeToDisplay = Math.floor(logEntries.slice(-1)[0].datetime / DEFAULT_NUMBER_OF_ITEMS) * DEFAULT_NUMBER_OF_ITEMS;
+                    // Calculate date-time to display up to. Note, calendar does not take into account milliseconds, round down to seconds.
+                    let dateTimeOfLastLogEntry = Math.floor(logEntries.slice(-1)[0].datetime / DEFAULT_NUMBER_OF_ITEMS) * DEFAULT_NUMBER_OF_ITEMS;
+                    let dateTimeFrom = new Date($scope.search.dateTimeFrom).getTime();
+
+                    if (dateTimeOfLastLogEntry > dateTimeFrom) {
 
                         // Display new log entries.
-                        let newLogEntries = logEntries.filter(entry => entry.datetime <= latestDateTimeToDisplay);
+                        let newLogEntries = logEntries.filter(({datetime}) => datetime <= dateTimeOfLastLogEntry);
                         $scope.logEntries = $scope.logEntries.concat(newLogEntries).slice(-DEFAULT_NUMBER_OF_ITEMS);
 
-                        // Cache next date-time to query tail from.
-                        dateTimeToAutoUpdateFrom = new Date(latestDateTimeToDisplay);
+                        // Optimize auto-refresh: cache next date-time to query tail from, if it is still a tail.
+                        dateTimeToAutoRefreshFrom = dateTimeOfLastLogEntry;
+
                     } else {
-                        // Or re-set the cache.
-                        dateTimeToAutoUpdateFrom = '';
+                        // Or re-set the cached value.
+                        dateTimeToAutoRefreshFrom = '';
                     }
                 } else {
                     $scope.logEntries = logEntries;
                 }
 
-                $scope.logtext = covertLogEntriesToString($scope.logEntries);
-                scrollToMostRecentRecords();
+                // Auto-scroll.
+                if ($scope.logEntries.length > 0) {
+                    if ($scope.isAutoScrollDown) {
+                        scrollToMostRecentLogEntry();
+                    } else if (datetimeToScrollTo && datetimeToScrollTo >= $scope.logEntries[0].datetime) {
+                        scrollToLogEntryWithDateTime(datetimeToScrollTo);
+                    }
+                }
+
+                // Re-set marker for search parameters changes after auto-scroll.
+                isNewQueryParameters = false;
+
+                if ($scope.logEntries.length === 0) {

Review comment:
       could be an `else { ..}` to the previous if for brevity




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] jathanasiou commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
jathanasiou commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r672136763



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -210,36 +285,30 @@ export function logbook() {
         }
 
         /**
-         * Converts log entries to string.
+         * Extracts timestamp from the log entry.
          *
-         * @param {Array.<Object>} logEntries The log entries to convert.
-         * @returns {String} log entries converted to string.
+         * @param logEntry The log entry.
+         * @returns {number} The extracted date-time.
          */
-        function covertLogEntriesToString(logEntries) {
-            let output = [];
-            const fieldsToShow = getCheckedBoxes($scope.logFields);
-            logEntries.forEach(entry => {
-                let outputLine = [];
-                if (fieldsToShow.includes('datetime') && entry.timestamp)
-                    outputLine.push(entry.timestamp);
-                if (fieldsToShow.includes('taskId') && entry.taskId)
-                    outputLine.push(entry.taskId);
-                if (fieldsToShow.includes('entityIds') && entry.entityIds)
-                    outputLine.push(entry.entityIds);
-                if (fieldsToShow.includes('level') && entry.level)
-                    outputLine.push(entry.level);
-                if (fieldsToShow.includes('bundleId') && entry.bundleId)
-                    outputLine.push(entry.bundleId);
-                if (fieldsToShow.includes('class') && entry.class)
-                    outputLine.push(entry.class);
-                if (fieldsToShow.includes('threadName') && entry.threadName)
-                    outputLine.push(entry.threadName);
-                if (fieldsToShow.includes('message') && entry.message)
-                    outputLine.push(entry.message);
-
-                output.push(outputLine.join(' '));
-            })
-            return output.length > 0 ? output.join('\n') : 'No results';
+        function getLogEntryTimestamp(logEntry) {
+            return Date.parse(logEntry.timestamp.replace(',', '.'))
+        }
+
+        /**
+         * Extracts UTC timestamp from the date.
+         *
+         * @param {Date|number} date The date to get UTC timestamp of.
+         * @returns {number|undefined} The UTC timestamp.
+         */
+        function getUtcTimestamp(date) {
+            const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
+            if (date instanceof Date) {
+                return date.valueOf() - timezoneOffsetMs;
+            } else if (typeof date === 'number') {
+                return date - timezoneOffsetMs;
+            } else {
+                return undefined;

Review comment:
       Return undefined is a needless statement for JS functions.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] jathanasiou commented on pull request #249: WIP Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
jathanasiou commented on pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#issuecomment-881449679


   # Log items consistency
   
   It seems that the way `logEntries` are being retrieved from the server API right now can be inconsistent.
   
   Using `timestamp` values to define a range the entries can lead to overlaps between consecutive queries. Timestamps can be **relative** and due to the nature of async operations such as this, inappropriate data sets may be sent to the client.
   
   
       query-A (from 16:21:0005 to 16:21:0010)
           client request sent at 16:21:0010
           server receives request at 16:21:0012 (or later) --> log entries may have been generated between 0010 and 0012
           client gets response at 16:21:0013 (or later)
       
       query-B (from 16:21:0010 to 16:21:0015)
           // same situation, overlap and transfer delays can cause unexpected behaviour and displayed items
           // the client could receive the same entry twice, or completely miss a log entry
   
   ----
   ## Suggestion: Use absolute reference IDs per entry
   
   The server should be tagging each log entry with a unique UUID (lots of libraries can do that reliably). After the first query, the client can instead use the ID in the last of (ordered) entry results in the most recent query response to request only entries newer than that one.
   
   See also https://github.com/apache/brooklyn-ui/pull/249#pullrequestreview-708378520
   
   # DOM manipulation
   
   Examining elements via `$element` is far from ideal, but somewhat necessary when DOM-related properties (scroll position inside parent) are needed. Once the issue of entry consistency is resolved, it might be easier to evaluate the mechanism for keeping older lines in "focus" via specific scroll positions.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [brooklyn-ui] algairim commented on a change in pull request #249: Logbook widget navigation improvements

Posted by GitBox <gi...@apache.org>.
algairim commented on a change in pull request #249:
URL: https://github.com/apache/brooklyn-ui/pull/249#discussion_r672141531



##########
File path: ui-modules/utils/logbook/logbook.js
##########
@@ -210,36 +285,30 @@ export function logbook() {
         }
 
         /**
-         * Converts log entries to string.
+         * Extracts timestamp from the log entry.
          *
-         * @param {Array.<Object>} logEntries The log entries to convert.
-         * @returns {String} log entries converted to string.
+         * @param logEntry The log entry.
+         * @returns {number} The extracted date-time.
          */
-        function covertLogEntriesToString(logEntries) {
-            let output = [];
-            const fieldsToShow = getCheckedBoxes($scope.logFields);
-            logEntries.forEach(entry => {
-                let outputLine = [];
-                if (fieldsToShow.includes('datetime') && entry.timestamp)
-                    outputLine.push(entry.timestamp);
-                if (fieldsToShow.includes('taskId') && entry.taskId)
-                    outputLine.push(entry.taskId);
-                if (fieldsToShow.includes('entityIds') && entry.entityIds)
-                    outputLine.push(entry.entityIds);
-                if (fieldsToShow.includes('level') && entry.level)
-                    outputLine.push(entry.level);
-                if (fieldsToShow.includes('bundleId') && entry.bundleId)
-                    outputLine.push(entry.bundleId);
-                if (fieldsToShow.includes('class') && entry.class)
-                    outputLine.push(entry.class);
-                if (fieldsToShow.includes('threadName') && entry.threadName)
-                    outputLine.push(entry.threadName);
-                if (fieldsToShow.includes('message') && entry.message)
-                    outputLine.push(entry.message);
-
-                output.push(outputLine.join(' '));
-            })
-            return output.length > 0 ? output.join('\n') : 'No results';
+        function getLogEntryTimestamp(logEntry) {
+            return Date.parse(logEntry.timestamp.replace(',', '.'))
+        }
+
+        /**
+         * Extracts UTC timestamp from the date.
+         *
+         * @param {Date|number} date The date to get UTC timestamp of.
+         * @returns {number|undefined} The UTC timestamp.
+         */
+        function getUtcTimestamp(date) {
+            const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
+            if (date instanceof Date) {
+                return date.valueOf() - timezoneOffsetMs;
+            } else if (typeof date === 'number') {
+                return date - timezoneOffsetMs;
+            } else {
+                return undefined;

Review comment:
       I would prefer to keep it explicit.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@brooklyn.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org