ui-modules/app-inspector/app/components/stream/stream.directive.js (193 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import angular from "angular"; import template from './stream.template.html'; const MODULE_NAME = 'inspector.stream'; angular.module(MODULE_NAME, []) .directive('stream', streamDirective); export default MODULE_NAME; export function streamDirective() { return { template: template, restrict: 'E', scope: { autoUpdate: '=?', tail: '=?', activityId: '@', streamType: '@', }, controller: ['$scope', '$interval', '$element', 'activityApi', 'brSnackbar', controller] }; function controller($scope, $interval, $element, activityApi, brSnackbar) { $scope.autoUpdate = $scope.autoUpdate !== false; $scope.tail = $scope.tail !== false; // Content filtering features $scope.filteredStream = []; $scope.formattedStream = ''; $scope.streamProcessedUpTo = 0; $scope.otherLogLines = 0; $scope.errorLogLines = 0; $scope.debugLogLines = 0; $scope.traceLogLines = 0; $scope.warningLogLines = 0; $scope.isDisplayOther = $scope.isDisplayOther !== false; $scope.isDisplayError = $scope.isDisplayError !== false; $scope.isDisplayDebug = $scope.isDisplayDebug !== false; $scope.isDisplayTrace = $scope.isDisplayTrace !== false; $scope.isDisplayWarning = $scope.isDisplayWarning !== false; $scope.isFilterContent = isFilterContent; $scope.isFormatContent = isFormatContent; $scope.isDisplayFormattedItem = isDisplayFormattedItem; $scope.getFormattedItemLogLevel = getFormattedItemLogLevel; // CLI XML features $scope.cliXml = false; $scope.cliXmlIdentified = false; $scope.toggleCliXml = toggleCliXml; $scope.isCliXmlSupported = isCliXmlSupported; $scope.cliXmlVerificationRequired = isWinRmStream(); // CLI XML verification is required only when stream is WinRM let autoScrollableElement = Array.from($element.find('pre')).filter(item => item.classList.contains('auto-scrollable')); let refreshFunction; // Set up cancellation of auto-scrolling on scrolling up. autoScrollableElement.forEach(item => { if (item.addEventListener) { let wheelHandler = () => { $scope.$apply(() => { $scope.tail = (item.scrollTop + item.offsetHeight) >= item.scrollHeight; }); } // IE9, Chrome, Safari, Opera item.addEventListener("mousewheel", wheelHandler, false); // Firefox item.addEventListener("DOMMouseScroll", wheelHandler, false); } }); // Watch the 'tail' and auto-scroll down if auto-scroll is enabled. $scope.$watch('tail', () => { if ($scope.tail) { $scope.$applyAsync(() => { autoScrollableElement.forEach(item => item.scrollTop = item.scrollHeight); }); } }); $scope.$watch('autoUpdate', ()=> { if ($scope.autoUpdate) { refreshFunction = $interval(updateStream, 1000); } else { cancelUpdate(); } }); $scope.$on('$destroy', cancelUpdate); /** * Updates the stream data. */ function updateStream() { activityApi.activityStream($scope.activityId, $scope.streamType).then((response)=> { // 1. Try to identify CLI XML output. const CLI_XML_HEADER_SIZE = 100; // estimated headers size in WinRM that can contain indication of CLI XML output if ($scope.cliXmlVerificationRequired && typeof response.data === 'string' && response.data.length >= CLI_XML_HEADER_SIZE) { let header = response.data.slice(0, CLI_XML_HEADER_SIZE); if (header.includes('#< CLIXML') || header.includes('xmlns="http://schemas.microsoft.com/powershell')) { $scope.cliXmlIdentified = true; } $scope.cliXmlVerificationRequired = false; // perform verification once, if conditions match } // 2. Update the stream data holder in this directive. $scope.stream = typeof response.data === 'object' ? JSON.stringify(response.data, null, 2) : response.data; // Check if to drop filters, because of `ng-repeat` performance limits. if ($scope.cliXmlIdentified && !$scope.cliXml && !$scope.filteredStream.length && $scope.stream.length > 99999) { $scope.noFilters = true; brSnackbar.create('Stream content is too big, showing output without filters.'); } // 3. Format the content where relevant. updateFormattedContent(); }).catch((error)=> { if (error.data) { $scope.error = error.data.message; } }).finally(() => { if ($scope.tail) { $scope.$applyAsync(() => { autoScrollableElement.forEach(item => item.scrollTop = item.scrollHeight); }); } }) } /** * Cancels the auto-update of the streamed content. */ function cancelUpdate() { if (refreshFunction) { $interval.cancel(refreshFunction); } } /** * @returns {boolean} True if CLI XML is supported, and false otherwise. CLI XML is expected in WinRM stream only. */ function isCliXmlSupported() { return isWinRmStream() && $scope.cliXmlIdentified === true; } /** * @returns {boolean} True if stream type is WinRM, and false otherwise. */ function isWinRmStream() { return $scope.streamType === 'winrm'; } /** * Switches content format to CLI XML and back. */ function toggleCliXml() { $scope.cliXml = !$scope.cliXml; updateFormattedContent(); } /** * @returns {boolean} True if stream formatting should be performed, and false otherwise. */ function isFormatContent() { return isCliXmlSupported() && $scope.cliXml !== true; } /** * @returns {boolean} True if logging filter should be displayed, and false otherwise. */ function isFilterContent() { return isFormatContent() && !$scope.noFilters; } /** * @returns {string} Returns class name of the formatted item log level. */ function getFormattedItemLogLevel(formattedItem) { if (formattedItem.isWarning) { return 'log-warning'; } else if (formattedItem.isError) { return 'log-error'; } if (formattedItem.isDebug) { return 'log-debug'; } return 'log-trace'; } /** * @returns {boolean} True if formatted item should be displayed, and false otherwise. */ function isDisplayFormattedItem(formattedItem) { return formattedItem.isWarning && $scope.isDisplayWarning || formattedItem.isDebug && $scope.isDisplayDebug || formattedItem.isError && $scope.isDisplayError || formattedItem.isTrace && $scope.isDisplayTrace || formattedItem.isOther && $scope.isDisplayOther; } /** * Formats CLI XML output and displays it in 'filtered-stream-content' field. */ function formatCliXmlContent() { // Slice at index of last closing tag ending wth the new line let streamTags = $scope.stream.match(/<\/(.*?)>\n/g); let lastClosingTagIndex = $scope.stream.lastIndexOf(streamTags[streamTags.length-1]); let newCliXmlData = $scope.stream.slice($scope.streamProcessedUpTo, lastClosingTagIndex); if (!newCliXmlData) { return; } $scope.streamProcessedUpTo += newCliXmlData.length; newCliXmlData.split(/\n/g).forEach(item => { let formattedItem = { id: ($scope.filteredStream.length), // ng-repeat requires unique items, array length fits the bill text: item, isOther: false, isError: false, isDebug: false, isTrace: false, isWarning: false }; if (/<s s="warning">/i.test(item)) { $scope.warningLogLines++; formattedItem.isWarning = true; } else if (/<s s="debug">/i.test(item)) { $scope.debugLogLines++; formattedItem.isDebug = true; } else if (/<s s="verbose">/i.test(item)) { $scope.traceLogLines++; formattedItem.isTrace = true; } else if (/<s s="error">/i.test(item)) { $scope.errorLogLines++; formattedItem.isError = true; } else { $scope.otherLogLines++; formattedItem.isOther = true; } // Remove CLI XML string tags for know log levels if (!formattedItem.isOther) { formattedItem.text = item.replace(/<s s="(.*?)">|\t/gi, ''); } // Remove CLI XML markers, newlines and replace tabs with spaces formattedItem.text = formattedItem.text.replace(/<\/s>|_x000[a-z0-9]_|\n/gi, '').replace(/\t/g,' '); // Push update item and let ng-repeat update the content $scope.filteredStream.push(formattedItem); }); if ($scope.noFilters) { $scope.formattedStream = $scope.filteredStream.map((i) => i.text).join('\n') } } /** * Filters stream content as per selected filters if filtering is enabled, e.g. display/hide warnings or errors. */ function updateFormattedContent() { if (!isFormatContent()) { return; } // Format new CLI XML content if (isCliXmlSupported()) { formatCliXmlContent(); } } updateStream(); } }