This closes #197
diff --git a/docs/customizations.md b/docs/customizations.md
index 77599a1..f1d4809 100644
--- a/docs/customizations.md
+++ b/docs/customizations.md
@@ -13,7 +13,7 @@
* Composer - Custom Config Widgets: special widgets to use for config keys can be specified in a registered type's
definition as a map tag, for example for the demo widget `suggestion-dropout` included we might have
- '{ ui-composer-hints: { config-widgets: [ {
+ `{ ui-composer-hints: { config-widgets: [ {
key: start.timeout, suggestion-values: [ 30s, 2m, 5m, 30m, 2h, { value: forever, description: 'No timeout' ],
widget: suggestion-dropdown, label-collapsed: fail after, label-expanded: Fail if not successful within } ] } }`
(as shown in the accompanying `vanillia-with-custom-widget.bom`);
diff --git a/ui-modules/app-inspector/app/components/stream/stream.directive.js b/ui-modules/app-inspector/app/components/stream/stream.directive.js
index 8db9b86..2196885 100644
--- a/ui-modules/app-inspector/app/components/stream/stream.directive.js
+++ b/ui-modules/app-inspector/app/components/stream/stream.directive.js
@@ -43,19 +43,56 @@
$scope.autoUpdate = $scope.autoUpdate !== false;
$scope.tail = $scope.tail !== false;
- let pre = $element.find('pre');
+ // Content filtering features
+ $scope.filteredStream = [];
+ $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.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;
- pre.on('mousewheel', () => {
- $scope.$apply(() => {
- $scope.tail = pre[0].scrollTop + pre[0].offsetHeight >= pre[0].scrollHeight;
- });
+ // 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', () => {
- $scope.$applyAsync(() => {
- pre[0].scrollTop = pre[0].scrollHeight;
- });
+ if ($scope.tail) {
+ $scope.$applyAsync(() => {
+ autoScrollableElement.forEach(item => item.scrollTop = item.scrollHeight);
+ });
+ }
});
$scope.$watch('autoUpdate', ()=> {
@@ -67,9 +104,28 @@
});
$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 = response.data;
+
+ // 3. Filter the content where relevant and display it.
+ updateFilteredContent();
+
}).catch((error)=> {
if (error.data) {
$scope.error = error.data.message;
@@ -77,18 +133,146 @@
}).finally(() => {
if ($scope.tail) {
$scope.$applyAsync(() => {
- pre[0].scrollTop = pre[0].scrollHeight;
+ 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;
+ updateFilteredContent();
+ }
+
+ /**
+ * @returns {boolean} True if logging filter should be displayed, and false otherwise.
+ */
+ function isFilterContent() {
+ return isCliXmlSupported() && $scope.cliXml !== true;
+ }
+
+ /**
+ * @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);
+ });
+ }
+
+ /**
+ * Filters stream content as per selected filters if filtering is enabled, e.g. display/hide warnings or errors.
+ */
+ function updateFilteredContent() {
+
+ if (!isFilterContent()) {
+ return;
+ }
+
+ // Format new CLI XML content
+ if (isCliXmlSupported()) {
+ formatCliXmlContent();
+ }
+ }
+
updateStream();
}
}
diff --git a/ui-modules/app-inspector/app/components/stream/stream.less b/ui-modules/app-inspector/app/components/stream/stream.less
index 8f43f73..f505b92 100644
--- a/ui-modules/app-inspector/app/components/stream/stream.less
+++ b/ui-modules/app-inspector/app/components/stream/stream.less
@@ -19,22 +19,42 @@
stream {
display: block;
position: relative;
+ @warning-log-color: @brand-warning;
+ @error-log-color: @brand-danger;
+ @debug-log-color: @brand-info;
+ @trace-log-color: @gray-lighter;
pre {
min-height: 4em;
background: @gray;
color: @gray-lighter;
.monospace();
- padding: 0.8em;
- padding-bottom: 1.2em;
+ padding: 0.8em 0.8em 1.2em;
+
+ .log-section {
+ all: revert;
+ margin: 0;
+ .monospace();
+ &.log-warning {
+ color: @warning-log-color;
+ }
+ &.log-error {
+ color: @error-log-color;
+ }
+ &.log-debug {
+ color: @debug-log-color;
+ }
+ &.log-trace {
+ color: @trace-log-color;
+ }
+ }
}
.log-actions {
position: absolute;
top: 0;
right: 0;
- padding: 1em;
- padding-top: 0.75em;
+ padding: 0.75em 1em 1em;
}
.log-action {
@@ -49,25 +69,50 @@
&:hover .fa {
color: lighten(desaturate(@base-color, 40%), 10%);
}
- &.active .fa {
- color: @base-color
+ &.active {
+ .fa {
+ color: @base-color;
+ }
+ .fa-exclamation-triangle {
+ color: @error-log-color;
+ }
+ .fa-bolt {
+ color: @warning-log-color;
+ }
+ .fa-bug {
+ color: @debug-log-color;
+ }
+ .fa-bullhorn {
+ color: @trace-log-color;
+ }
+ .fa-question {
+ color: @trace-log-color;
+ }
}
- &.active:hover .fa {
- color: desaturate(@base-color, 20%);
+ &.active:hover {
+ .fa {
+ color: desaturate(@base-color, 20%);
+ }
+ .fa-exclamation-triangle {
+ color: desaturate(@error-log-color, 20%);
+ }
+ .fa-bolt {
+ color: desaturate(@warning-log-color, 20%);
+ }
+ .fa-bug {
+ color: desaturate(@debug-log-color, 20%);
+ }
+ .fa-bullhorn {
+ color: desaturate(@trace-log-color, 20%);
+ }
+ .fa-question {
+ color: desaturate(@trace-log-color, 20%);
+ }
}
}
.fa-arrow-circle-o-down {
margin-top: 1px;
}
-
- .log-autoupdate {
- .fa {
- transform: rotate(0);
- }
- &.active .fa {
- transform: rotate(180deg) translate(0,1px);
- }
- }
}
diff --git a/ui-modules/app-inspector/app/components/stream/stream.template.html b/ui-modules/app-inspector/app/components/stream/stream.template.html
index 0b2e1e1..0322874 100644
--- a/ui-modules/app-inspector/app/components/stream/stream.template.html
+++ b/ui-modules/app-inspector/app/components/stream/stream.template.html
@@ -19,14 +19,38 @@
<loading-state error="error" ng-if="stream === undefined"></loading-state>
<div ng-show="stream.length >= 0">
- <pre ng-show="stream.length > 0">{{stream}}</pre>
+ <pre ng-show="stream.length > 0 && !isFilterContent()" class="auto-scrollable">{{stream}}</pre>
+ <pre ng-show="stream.length > 0 && isFilterContent()" class="auto-scrollable"><pre ng-repeat="item in filteredStream track by item.id" ng-show="isDisplayFormattedItem(item)" class="log-section" ng-class="getFormattedItemLogLevel(item)">{{item.text}}</pre><p ng-show="!isDisplayTrace && !isDisplayDebug && !isDisplayWarning && !isDisplayError && !isDisplayOther" class="text-center"><i>(select filter)</i></p></pre>
<pre ng-show="stream.length == 0" class="text-center"><i>(no content)</i></pre>
-
<div class="log-actions">
- <span class="log-autoupdate log-action" ng-click="autoUpdate = !autoUpdate" ng-class="{'active': autoUpdate}"
- uib-tooltip="{{ autoUpdate ? 'Disable' : 'Enable' }} auto-update" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
- <i class="fa fa-refresh"></i></span>
- <span class="log-tail log-action" ng-click="tail = !tail" ng-class="{'active': tail}"
+
+ <!-- Content filtering -->
+ <span class="log-action" ng-click="isDisplayOther = !isDisplayOther" ng-class="{'active': isDisplayOther}" ng-show="isFilterContent()"
+ uib-tooltip="{{ isDisplayOther ? 'Hide' : 'Show' }} other ({{otherLogLines}} lines)" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
+ <i class="fa fa-question"></i></span>
+ <span class="log-action" ng-click="isDisplayTrace = !isDisplayTrace" ng-class="{'active': isDisplayTrace}" ng-show="isFilterContent()"
+ uib-tooltip="{{ isDisplayTrace ? 'Hide' : 'Show' }} trace ({{traceLogLines}} lines)" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
+ <i class="fa fa-bullhorn"></i></span>
+ <span class="log-action" ng-click="isDisplayDebug = !isDisplayDebug" ng-class="{'active': isDisplayDebug}" ng-show="isFilterContent()"
+ uib-tooltip="{{ isDisplayDebug ? 'Hide' : 'Show' }} debug ({{debugLogLines}} lines)" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
+ <i class="fa fa-bug"></i></span>
+ <span class="log-action" ng-click="isDisplayWarning = !isDisplayWarning" ng-class="{'active': isDisplayWarning}" ng-show="isFilterContent()"
+ uib-tooltip="{{ isDisplayWarning ? 'Hide' : 'Show' }} warnings ({{warningLogLines}} lines)" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
+ <i class="fa fa-bolt"></i></span>
+ <span class="log-action" ng-click="isDisplayError = !isDisplayError" ng-class="{'active': isDisplayError}" ng-show="isFilterContent()"
+ uib-tooltip="{{ isDisplayError ? 'Hide' : 'Show' }} errors ({{errorLogLines}} lines)" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
+ <i class="fa fa-exclamation-triangle"></i></span>
+
+ <!-- CLI XML output mode -->
+ <span class="log-action" ng-click="toggleCliXml()" ng-class="{'active': cliXml}" ng-show="isCliXmlSupported()"
+ uib-tooltip="Display {{ cliXml ? 'formatted output' : 'CLI XML' }}" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
+ <i class="fa fa-code"></i></span>
+
+ <!-- Auto-update and auto-scroll -->
+ <span class="log-action" ng-click="autoUpdate = !autoUpdate" ng-class="{'active': autoUpdate}"
+ uib-tooltip="{{ autoUpdate ? 'Disable' : 'Enable' }} auto-update" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
+ <i class="fa fa-refresh" ng-class="autoUpdate ? 'fa-spin' : ''"></i></span>
+ <span class="log-tail log-action" ng-click="tail = !tail" ng-class="{'active': tail}"
uib-tooltip="{{ tail ? 'Disable' : 'Enable' }} auto-scroll" tooltip-placement="top" tooltip-popup-delay="500" tooltip-append-to-body="true">
<i class="fa fa-arrow-circle-o-down "></i></span>
</div>
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
index f089495..f8f7e2b 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
@@ -116,6 +116,14 @@
otherMap[name] = s;
}
}
+
+ // Do not display streams that are not initialized
+ for (let name in knownMap) {
+ if (knownMap[name] === null || knownMap[name] === undefined) {
+ delete knownMap[name];
+ }
+ }
+
$scope.streamsById = Object.assign({}, knownMap, otherMap);
return Object.keys($scope.streamsById);
};