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/16 11:11:49 UTC

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

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