This is an automated email from the ASF dual-hosted git repository. heneveld pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/brooklyn-ui.git
commit efb5511e1a56059b2755a411e02ce8dc42785962 Author: Alex Heneveld <[email protected]> AuthorDate: Thu Oct 6 10:42:23 2022 +0100 improve workflow ui for GC'd tasks, plus other minor ui fixes create stub tasks where a workflow references a task; use improved UI for details on workflow replays; show graphic for additional task statuses --- .../components/providers/entity-api.provider.js | 2 +- .../components/task-list/task-list.directive.js | 4 + .../components/workflow/workflow-step.directive.js | 2 +- .../workflow/workflow-step.template.html | 2 +- .../inspect/activities/activities.controller.js | 100 +++++++++++++++++++-- .../inspect/activities/detail/detail.controller.js | 24 ++++- .../inspect/activities/detail/detail.template.html | 10 ++- ui-modules/utils/status/status.js | 8 +- 8 files changed, 133 insertions(+), 19 deletions(-) diff --git a/ui-modules/app-inspector/app/components/providers/entity-api.provider.js b/ui-modules/app-inspector/app/components/providers/entity-api.provider.js index 8ec4e2bd..62d64f76 100644 --- a/ui-modules/app-inspector/app/components/providers/entity-api.provider.js +++ b/ui-modules/app-inspector/app/components/providers/entity-api.provider.js @@ -194,6 +194,6 @@ function EntityApi($http, $q) { return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflows/', {observable: true, ignoreLoadingBar: true}); } function getWorkflow(applicationId, entityId, workflowId) { - return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflow/' + workflowId, {observable: true, ignoreLoadingBar: true}); + return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflows/' + workflowId, {observable: true, ignoreLoadingBar: true}); } } \ No newline at end of file diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.directive.js b/ui-modules/app-inspector/app/components/task-list/task-list.directive.js index 8f9ac4e3..054aa305 100644 --- a/ui-modules/app-inspector/app/components/task-list/task-list.directive.js +++ b/ui-modules/app-inspector/app/components/task-list/task-list.directive.js @@ -206,6 +206,10 @@ export function durationFilter() { } function isTaskWithTag(task, tag) { + if (!task.tags) { + console.log("Task without tags: ", task); + return false; + } return task.tags.indexOf(tag)>=0; } diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js b/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js index e22cf1e2..51e2aef7 100644 --- a/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js +++ b/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js @@ -113,7 +113,7 @@ export function workflowStepDirective() { $scope.isFocusTask = false; if ($scope.task) { - if ($scope.stepContext.taskId === $scope.task.id) { + if (!vm.isNullish($scope.stepContext.taskId) && $scope.stepContext.taskId === $scope.task.id) { $scope.isFocusTask = true; } else if ($scope.isFocusStep) { diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html b/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html index a9d4848b..a0aa233a 100644 --- a/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html +++ b/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html @@ -43,7 +43,7 @@ <div class="workflow-step" id="workflow-step-{{stepIindex}}" ng-class="vm.getWorkflowStepClasses(stepIndex)"> <div class="rhs-icons"> - <div ng-if="isFocusTask" class="workflow-step-pill focus-step"> + <div ng-if="isFocusTask" class="workflow-step-pill focus-step" title="This step instance is for the task currently selected in the activity view."> selected </div> <div ng-click="vm.toggleExpandState()" class="expand-toggle"> diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js index df4888da..8c3110b7 100644 --- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js +++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js @@ -62,17 +62,50 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti onStateChange(); }) + function mergeActivities() { + // merge activitiesRaw records with workflows records, into vm.activities; + // only once activitiesRaw is loaded + if (vm.activitiesRaw) { + const newActivitiesMap = {}; + vm.activitiesRaw.forEach(activity => { + newActivitiesMap[activity.id] = activity; + }); + + // TODO + //(vm.workflows || []) + Object.values(vm.workflows || {}) + .forEach(wf => { + (wf.replays || []).forEach(wft => { + let newActivity = newActivitiesMap[wtf.taskId]; + if (!newActivity) { + // create stub tasks for the replays of workflows + newActivity = makeTaskStubFromWorkflowRecord(wf, wtf); + newActivitiesMap[wtf.taskId] = newActivity; + } + newActivity.workflowId = wtf.workflowId; + newActivity.isWorkflowOldReplay = wtf.workflowId !== wtf.taskId; + }); + }); + newActivitiesMap['extra'] = makeTaskStubMock("Extra workflow", "extra", applicationId, entityId); + + vm.activitiesMap = newActivitiesMap; + vm.activities = Object.values(vm.activitiesMap); + } + } + function onStateChange() { if ($state.current.name === activitiesState.name && !vm.activities) { // only run if we are the active state entityApi.entityActivities(applicationId, entityId).then((response) => { - vm.activities = response.data; + vm.activitiesRaw = response.data; + mergeActivities(); observers.push(response.subscribe((response) => { - vm.activities = response.data; + vm.activitiesRaw = response.data; + mergeActivities(); vm.error = undefined; })); }).catch((error) => { - $log.warn('Error loading activity for '+activityId, error); + $log.warn('Error loading activities for entity '+entityId, error); vm.error = 'Cannot load activities for entity with ID: ' + entityId; }); @@ -87,10 +120,22 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti vm.error = 'Cannot load activities (deep) for entity with ID: ' + entityId; }); + entityApi.getWorkflows(applicationId, entityId).then((response) => { + vm.workflows = response.data; + mergeActivities(); + observers.push(response.subscribe((response) => { + vm.workflows = response.data; + mergeActivities(); + })); + }).catch((error) => { + $log.warn('Error loading workflows for entity '+entityId, error); + }); + + $scope.$on('$destroy', () => { - observers.forEach((observer) => { - observer.unsubscribe(); - }); + observers.forEach((observer) => { + observer.unsubscribe(); + }); }); } } @@ -100,3 +145,46 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti $scope.globalFilters = globalFilters; } } + +export function makeTaskStubFromWorkflowRecord(wf, wft) { + return { + id: wft.taskId, + displayName: wf.name + (wft.reasonForReplay ? " ("+wft.reasonForReplay+")" : ""), + entityId: (wf.entity || {}).id, + isError: wtf.isError===false ? false : true, + currentStatus: vm.isNullish(wtf.isError) ? "Unavailable" : wtf.status, + submitTimeUtc: wft.submittedTimeUtc, + startTimeUtc: wft.startTimeUtc, + endTimeUtc: wft.endTimeUtc, + tags: [ + "WORKFLOW", + { + workflowId: wf.workflowId, + applicationId: wf.applicationId, + entityId: wf.entityId, + }, + ], + }; +}; + +// for testing only +export function makeTaskStubMock(name, id, applicationId, entityId) { + return { + id, + displayName: name, + entityId: entityId, + isError: true, + currentStatus: "Unavailable", + submitTimeUtc: Date.now()-5000, + startTimeUtc: Date.now()-4000, + endTimeUtc: Date.now()-1000, + tags: [ + "WORKFLOW", + { + workflowId: 'extra', + applicationId: applicationId, + entityId: entityId, + }, + ], + }; +} 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 9a9e1e0c..648142b9 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 @@ -19,6 +19,7 @@ import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner'; import template from "./detail.template.html"; import modalTemplate from './kilt.modal.template.html'; +import {makeTaskStubFromWorkflowRecord, makeTaskStubMock} from "../activities.controller"; export const detailState = { name: 'main.inspect.activities.detail', @@ -144,18 +145,33 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou $log.warn('Error loading activity for '+activityId, error); // prefer this simpler error message over the specific ones below vm.errorBasic = true; - vm.error = $sce.trustAsHtml('Cannot load activity with ID: <b>' + _.escape(activityId) + '</b> <br/><br/>' + - 'Task may have completed and been cleared from memory, or may not have been run. Details may be available in logs.'); + vm.error = $sce.trustAsHtml('Cannot load task with ID: <b>' + _.escape(activityId) + '</b> <br/><br/>' + + 'The task is no longer stored in memory. Details may be available in logs.'); // in case it corresponds to a workflow and not a task, try loading as a workflow loadWorkflow(null).then(()=> { + const wft = (wf.mainTasks || []).find(t => t.taskId === activityId); + if (wft) { + vm.model.activity = makeTaskStubFromWorkflowRecord(wf, wft); + vm.model.workflow.tag = findWorkflowTag(vm.model.activity); + } else { + throw "Workflow task "+activityId+" not stored on workflow"; + } + // give a better error - vm.error = $sce.trustAsHtml('Information on workflow <b>' + _.escape(activityId) + '</b> is available but with limitations.<br/><br/>' + - 'The initial task is no longer available, possibly because this workflow has been resumed after a restart.'); + vm.error = $sce.trustAsHtml('Limited information on workflow task <b>' + _.escape(activityId) + '</b>.<br/><br/>' + + (!vm.model.activity.endTimeUtc + ? "The run appears to have been interrupted by a server restart or failover." + : 'The workflow is known but this task is no longer stored in memory.') ); }).catch(error2 => { $log.debug("ID "+activityId+" does not correspond to workflow either", error2); + + // vm.error = $sce.trustAsHtml('Mock data for workflow task <b>' + _.escape(activityId) + '</b>.'); + // + // vm.model.activity = makeTaskStubMock("Extra workflow task", "extra", applicationId, entityId); + // vm.model.workflow.tag = findWorkflowTag(vm.model.activity); }); }); diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html index 254435f4..fd5f5fe0 100644 --- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html +++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html @@ -186,16 +186,18 @@ <div class="workflow-body"> <div ng-if="vm.model.workflow.loading == 'loaded'"> - <div ng-if="vm.model.workflow.data.taskIds.length > 1"> + <div ng-if="vm.model.workflow.data.replays.length > 1"> <div style="float: right; margin-top: -9px;" class="btn-group" uib-dropdown> <button id="replay-button" type="button" class="btn btn-select-dropdown" uib-dropdown-toggle> Select replay <span class="caret"></span> </button> <ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="replay-button"> - <li role="menuitem" ng-repeat="id in vm.model.workflow.data.taskIds" id="workflow-replay-{{ id }}"> - <a href="" ui-sref="main.inspect.activities.detail({activityId: id})" ng-class="{'selected' : vm.model.activityId === id}"> + <li role="menuitem" ng-repeat="replay in vm.model.workflow.data.replays" id="workflow-replay-{{ replay.taskId }}"> + <a href="" ui-sref="main.inspect.activities.detail({activityId: replay.taskId})" ng-class="{'selected' : vm.model.activityId === replay.taskId}"> <i class="fa fa-check check"></i> - <span class="monospace">{{ id }}</span></a> </li> +<!-- <span class="monospace">{{ replay.taskId }}</span>--> + {{ replay.submitTimeUtc | date : 'MMM dd, yyyy @ H:mm:ss' }} - {{ replay.reasonForReplay || '(no reason supplied)' }} + </a> </li> <li role="menuitem"> <a href="" ng-click="vm.showReplayHelp()" ng-class="{'selected' : showReplayHelp}"><i>More information</i></a> </li> diff --git a/ui-modules/utils/status/status.js b/ui-modules/utils/status/status.js index 4677925b..17a21388 100644 --- a/ui-modules/utils/status/status.js +++ b/ui-modules/utils/status/status.js @@ -40,9 +40,13 @@ const STATUS = { ERROR: {name: 'Error', icon: ICONS.ERROR}, UNKNOWN: {name: 'Unknown', icon: ICONS.UNKNOWN}, NO_STATE: {name: '', icon: ICONS.NO_STATE}, + + // for tasks 'In progress': {name: 'In progress', icon: ICONS.STARTING}, 'Completed': {name: 'Completed', icon: ICONS.RUNNING}, - 'Failed': {name: 'Failed', icon: ICONS.ERROR} + 'Failed': {name: 'Failed', icon: ICONS.ERROR}, + 'Unavailable': {name: 'Incomplete', icon: ICONS.ERROR}, + 'Cancelled': {name: 'Cancelled', icon: ICONS.ERROR}, }; const MODULE_NAME = 'brooklyn.components.status'; @@ -77,7 +81,7 @@ export function statusIconDirective() { } export function statusTextDirective() { var directive = { - template: '<div ng-class="statusClass()">{{status.name}}</div>', + template: '<div ng-class="statusClass()">{{status.name || value}}</div>', restrict: 'E', scope: { value: '@'
