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 0714fb85b637c070e5daac8b869f82d2c9d4ee8a Author: Alex Heneveld <[email protected]> AuthorDate: Tue Oct 4 23:03:40 2022 +0100 workflow UI polish, subworkflows, bug-fixes --- .../components/workflow/workflow-step.directive.js | 1 + .../workflow/workflow-step.template.html | 34 ++++++--- .../workflow/workflow-steps.directive.js | 2 +- .../app/components/workflow/workflow-steps.less | 7 ++ .../inspect/activities/detail/detail.controller.js | 70 ++++++++++++++----- .../main/inspect/activities/detail/detail.less | 31 +++++++-- .../inspect/activities/detail/detail.template.html | 81 ++++++++++++++++------ 7 files changed, 174 insertions(+), 52 deletions(-) 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 9a322297..e22cf1e2 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 @@ -61,6 +61,7 @@ export function workflowStepDirective() { vm.yaml = (data) => jsyaml.dump(data); vm.yamlOrPrimitive = (data) => typeof data === "string" ? data : vm.yaml(data); vm.nonEmpty = (data) => data && (data.length || Object.keys(data).length); + vm.isNullish = _.isNil; $scope.json = null; $scope.jsonMode = null; 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 b3ac63c9..a9d4848b 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 @@ -97,15 +97,15 @@ </span> <span ng-if="isFocusTask"> - in this task ({{ stepContext.taskId }}). + as the activity currently being viewed (<span class="monospace">{{ stepContext.taskId }}</span>). </span> <span ng-if="!isFocusTask"> - in <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId })">Task {{ stepContext.taskId }}</a>. + in <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId })">task <span class="monospace">{{ stepContext.taskId }}</span></a>. </span> </div> <div ng-if="isFocusStep && !isFocusTask" class="space-above"> - <b>The currently selected task ({{ task.id }}) is for a previous invocation of this step.</b> + <b>The activity currently being viewed (<span class="monospace">{{ task.id }}</span>) is for a previous run of this step.</b> </div> <div class="more-space-above"> @@ -126,10 +126,11 @@ <div class="A"><span ng-if="isCurrent">CURRENT</span><span ng-if="!isCurrent">LAST</span> EXECUTION</div> <div class="B"> <span ng-if="isFocusTask"> - Task {{ stepContext.taskId }} + task <span class="monospace">{{ stepContext.taskId }}</span> </span> <span ng-if="!isFocusTask"> - <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId })">Task {{ stepContext.taskId }}</a> + <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId })" + >task <span class="monospace">{{ stepContext.taskId }}</span></a> </span> </div> </div> @@ -138,19 +139,34 @@ <span ng-if="osi.previousTaskId"> Step {{ osi.previous[0]+1 }} (<a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: osi.previousTaskId })" - >Task {{ osi.previousTaskId }}</a>) + >task <span class="monospace">{{ osi.previousTaskId }}</span></a>) </span> <span ng-if="!osi.previousTaskId">(workflow start)</span> </div></div> + <div class="data-row nested" ng-if="!isCurrent"><div class="A">Followed by</div> <div class="B"> <span ng-if="osi.nextTaskId"> Step {{ osi.next[0]+1 }} (<a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: osi.nextTaskId })" - >Task {{ osi.nextTaskId }}</a>) + >task <span class="monospace">{{ osi.nextTaskId }}</span></a>) </span> <span ng-if="!osi.nextTaskId">(workflow end)</span> </div></div> + <div class="data-row nested with-buttons" ng-if="stepContext.subWorkflows && stepContext.subWorkflows.length"><div class="A" style="margin-top: 2px;">Sub-workflows</div> <div class="B"> + <div class="btn-group" uib-dropdown> + <button id="workflow-button" type="button" class="btn btn-select-dropdown workflow-button-small" uib-dropdown-toggle> + {{ stepContext.subWorkflows.length }} nested workflow{{ stepContext.subWorkflows.length>1 ? 's' : '' }} <span class="caret"></span> + </button> + <ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="workflow-button"> + <li role="menuitem" ng-repeat="sub in stepContext.subWorkflows" id="sub-workflow-{{ sub.workflowId }}"> + <a href="" ui-sref="main.inspect.activities.detail({applicationId: sub.applicationId, entityId: sub.entityId, activityId: sub.workflowId})"> + <i class="fa fa-check check"></i> + <span class="monospace">{{ sub.workflowId }}</span></a> </li> + </ul> + </div> + </div></div> + <div class="data-row nested" ng-if="osi.workflowScratch"><div class="A">Workflow Vars</div> <div class="B multiline-code">{{ vm.yaml(osi.workflowScratch) }}</div></div> <div class="data-row nested" ng-if="stepContext.input"><div class="A">Input</div> <div class="B multiline-code">{{ vm.yaml(stepContext.input) }}</div></div> <div class="data-row nested" ng-if="!isCurrent && stepContext.output"><div class="A">Output</div> <div class="B multiline-code">{{ vm.yaml(stepContext.output) }}</div></div> @@ -165,10 +181,10 @@ <div class="more-space-above" ng-if="vm.nonEmpty(stepContext) || vm.nonEmpty(step) || vm.nonEmpty(osi)"> <div class="btn-group right" uib-dropdown> - <button id="single-button" type="button" class="btn btn-select-dropdown pull-right" uib-dropdown-toggle> + <button id="extra-data-button" type="button" class="btn btn-select-dropdown pull-right" uib-dropdown-toggle> View data <span class="caret"></span> </button> - <ul class="dropdown-menu pull-right" uib-dropdown-menu role="menu" aria-labelledby="single-button"> + <ul class="dropdown-menu pull-right" uib-dropdown-menu role="menu" aria-labelledby="extra-data-button"> <li role="menuitem" > <a href="" ng-click="vm.showJson('stepContext', stepContext)" ng-class="{'selected' : jsonMode === 'stepContext'}"> <i class="fa fa-check check"></i> Last Execution Context</a> </li> diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js b/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js index 1d509ada..8eb950d0 100644 --- a/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js +++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js @@ -54,7 +54,7 @@ export function workflowStepsDirective() { } $scope.expandStates = {}; - if ($scope.workflow.tag && $scope.workflow.tag.stepIndex) { + if ($scope.workflow.tag && !_.isNil($scope.workflow.tag.stepIndex)) { $scope.expandStates[$scope.workflow.tag.stepIndex] = true; } diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less index 0a9bd803..2c067037 100644 --- a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less +++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less @@ -122,6 +122,7 @@ display: flex; margin-top: 3px; margin-bottom: 3px; + align-items: baseline; .A { flex: 0 0 auto; width: 30%; @@ -134,6 +135,9 @@ } .B { flex: 1 1 auto; + &.multiline-code { + margin-top: 3px; + } } &.nested { @@ -171,6 +175,9 @@ } } } + .workflow-button-small { + padding: 1px 5px; + } } .workflow-step-status-indicators { 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 02388f97..7ef65aba 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 @@ -43,6 +43,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou activityId: activityId, childFilter: {'EFFECTOR': true, 'SUB-TASK': false}, accordion: {summaryOpen: true, subTaskOpen: true, streamsOpen: true, workflowOpen: true}, + activity: {}, workflow: {}, }; @@ -52,7 +53,39 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou let observers = []; if ($state.current.name === detailState.name) { - + + function loadWorkflow(workflowTag, optimistic) { + if (!workflowTag) { + workflowTag = {} + optimistic = true; + } + vm.model.workflow.loading = 'loading'; + + return entityApi.getWorkflow(workflowTag.applicationId || applicationId, workflowTag.entityId || entityId, workflowTag.workflowId || activityId).then(wResponse => { + workflowTag = {applicationId, entityId, workflowId: activityId, ...workflowTag}; + if (optimistic) { + vm.model.workflow.tag = workflowTag; + } + vm.model.workflow.data = wResponse.data; + vm.model.workflow.loading = 'loaded'; + vm.model.workflow.applicationId = workflowTag.applicationId; + vm.model.workflow.entityId = workflowTag.entityId; + + observers.push(wResponse.subscribe((wResponse2)=> { + // change the workflow object so widgets get refreshed + vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data }; + })); + }).catch(error => { + if (optimistic) { + vm.model.workflow.loading = null; + throw error; + } + + console.log("ERROR loading workflow " + workflowTag.workflowId, error); + vm.model.workflow.loading = 'error'; + }); + }; + activityApi.activity(activityId).then((response)=> { vm.model.activity = response.data; @@ -72,21 +105,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou const workflowTag = findWorkflowTag(vm.model.activity); if (workflowTag) { vm.model.workflow.tag = workflowTag; - vm.model.workflow.loading = 'loading'; - entityApi.getWorkflow(applicationId, entityId, workflowTag.workflowId).then(wResponse => { - vm.model.workflow.data = wResponse.data; - vm.model.workflow.loading = 'loaded'; - vm.model.workflow.applicationId = applicationId; - vm.model.workflow.entityId = entityId; - - observers.push(wResponse.subscribe((wResponse2)=> { - // change the workflow object so widgets get refreshed - vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data }; - })); - }).catch(error => { - console.log("ERROR loading workflow " + workflowTag.workflowId, error); - vm.model.workflow.loading = 'error'; - }); + loadWorkflow(workflowTag); } } @@ -96,12 +115,24 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou vm.error = undefined; vm.errorBasic = false; })); + }).catch((error)=> { $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.'); + + // in case it corresponds to a workflow and not a task, try loading as a workflow + + loadWorkflow(null).then(()=> { + // 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.'); + + }).catch(error2 => { + $log.debug("ID "+activityId+" does not correspond to workflow either", error2); + }); }); activityApi.activityChildren(activityId).then((response)=> { @@ -173,7 +204,6 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou $timeout(function() { $scope.$broadcast('resize') }, 100); }; - vm.stringifyActivity = () => JSON.stringify(vm.model.activity, null, 2); vm.stringify = (data) => JSON.stringify(data, null, 2); vm.invokeEffector = (effectorName, effectorParams) => { @@ -206,6 +236,12 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou // so transient tasks etc less relevant } + vm.showReplayHelp = () => { + $scope.showReplayHelp = !$scope.showReplayHelp; + } + + vm.isNullish = _.isNil; + } function findWorkflowTag(task) { diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less index b6eb05e5..2dc74ee4 100644 --- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less +++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less @@ -106,10 +106,6 @@ .monospace(); } - .monospace { - .monospace(); - } - .table-responsive, .table { margin-bottom: 0; @@ -161,7 +157,7 @@ } } } - .result-parent { + .monospace, .result-parent { .monospace(); } .result-parent.big-result { @@ -190,4 +186,29 @@ margin-top: -12px; } + .workflow-body { + .btn-group { + > .dropdown-menu { + li a { + padding-left: 2em; + } + } + + .selected { + .check { + margin-left: -1.5em; + display: block; + width: 0; + height: 0; + overflow: visible; + margin-top: 3px; + margin-bottom: -3px; + } + } + .check { + display: none; + } + } + } + } 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 25eada16..09e45831 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 @@ -18,9 +18,9 @@ --> <ui-view class="activity-any"> <div> - <loading-state error="vm.error" trust-error="true" ng-if="!vm.model.activity || !vm.model.activityChildren"></loading-state> + <loading-state error="vm.error" trust-error="true" ng-if="!vm.model.activity.id || !vm.model.activityChildren"></loading-state> - <div ng-if="vm.model.activity && vm.model.activityChildren"> + <div ng-if="vm.model.activity.id && vm.model.activityChildren"> <ol class="breadcrumb" ng-show="showParents"> <li breadcrumb-navigation parent-id="{{vm.model.activity.submittedByTask.metadata.id}}" entity-id="{{vm.model.entityId}}"></li> @@ -34,22 +34,24 @@ </li> <li class="breadcrumb-item active">{{vm.model.activity.displayName}}</li> </ol> + </div> - <div ng-if="vm.model.activity" class="activity-detail"> + <div> + <div class="activity-detail"> <div class="alert alert-info" ng-if="vm.model.activity.blockingTask"> <strong>Blocked on:</strong> <span ng-if="vm.model.activity.blockingDetails">{{vm.model.activity.blockingDetails}}:</span> <code><a ui-sref="main.inspect.activities.detail({entityId: vm.model.activity.blockingTask.metadata.entityId, activityId: vm.model.activity.blockingTask.metadata.id})">{{vm.model.activity.blockingTask.metadata.taskName}}</a></code> for <strong><a ui-sref="main.inspect.summary({entityId: vm.model.activity.blockingTask.metadata.entityId})">{{vm.model.activity.blockingTask.metadata.entityDisplayName}} entity</a></strong> </div> - <div class="activity-header"> + <div class="activity-header" ng-if="vm.model.activity.id"> <div class="activity-title">{{vm.model.activity.displayName}}</div> <div class="activity-entity">{{vm.model.activity.entityDisplayName}}</div> <div class="activity-description" ng-if="vm.model.activity.description">{{vm.model.activity.description}}</div> </div> <div class="activity-body"> - <div class="summary-body"> + <div class="summary-body" ng-if="vm.model.activity.id"> <div class="summary-block"> <div class="row"> <div class="col-md-3 summary-item"> @@ -158,21 +160,56 @@ </br-collapsible> <br-collapsible state="vm.model.accordion.workflowOpen" - ng-if="vm.model.workflow"> + ng-if="vm.model.workflow.data"> <heading> Workflow</heading> <div class="workflow-body"> <div ng-if="vm.model.workflow.loading == 'loaded'"> - <p style="margin-top: 12px; margin-bottom: 24px;"> + <div ng-if="vm.model.workflow.data.taskIds.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}"> + <i class="fa fa-check check"></i> + <span class="monospace">{{ id }}</span></a> </li> + <li role="menuitem"> + <a href="" ng-click="vm.showReplayHelp()" ng-class="{'selected' : showReplayHelp}"><i>More information</i></a> + </li> + </ul> + </div> + </div> + <div style="margin-top: 12px; margin-bottom: 24px;"> This task is for - <span ng-if="vm.model.workflow.tag.stepIndex">step <b>{{ vm.model.workflow.tag.stepIndex+1 }}</b> - in workflow + <span ng-if="!vm.isNullish(vm.model.workflow.tag.stepIndex)">step <b>{{ vm.model.workflow.tag.stepIndex+1 }}</b> + in <a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.data.taskId})"> - <b>{{vm.model.workflow.data.name}}</b>. - </a> + workflow <span class="monospace">{{vm.model.workflow.data.workflowId}}</span>: + <b>{{vm.model.workflow.data.name}}</b></a>. + </span> + <span ng-if="vm.isNullish(vm.model.workflow.tag.stepIndex)"> + <span ng-if="vm.model.workflow.data.taskIds.length>1"> + <span ng-if="vm.model.workflow.data.taskIds[vm.model.workflow.data.taskIds.length-1] === vm.model.activityId"> + the most recent </span> + <span ng-if="vm.model.workflow.data.taskIds[vm.model.workflow.data.taskIds.length-1] !== vm.model.activityId"> + run {{vm.model.workflow.data.taskIds.indexOf(vm.model.activityId)+1}} </span> + of {{ vm.model.workflow.data.taskIds.length }} of + </span> + workflow <span class="monospace">{{vm.model.workflow.data.workflowId}}</span>: + <b>{{vm.model.workflow.data.name}}</b>. </span> - <span ng-if="!vm.model.workflow.tag.stepIndex"> workflow <b>{{vm.model.workflow.data.name}}</b>.</span> - </p> + </div> + <div ng-if="showReplayHelp" style="margin-top: 12px; margin-bottom: 24px;"> + Workflows can be replayed in certain situations, such as if they fail or the server is restarted. + This workflow invocation instance has been replayed, with a total of {{ vm.model.workflow.data.taskIds.length }} runs. + Individual replays can be viewed by selecting a task ID from the dropdown. + The workflow step data below shows the most recent run of each step in any replay. + The arrows between steps show all step transitions in any replay of this workflow instance. + Sub-task and log views further below can be useful to disambiguate multiple replays if required. + </div> + <workflow-steps workflow="vm.model.workflow" task="vm.model.activity"></workflow-steps> </div> <div ng-if="vm.model.workflow.loading != 'loaded'"> @@ -211,19 +248,23 @@ <br-collapsible ng-if="vm.model.activity.detailedStatus" state="vm.model.accordion.jsonOpen"> <heading> JSON</heading> - <pre>{{vm.stringifyActivity()}}</pre> + <b>Activity</b> + <pre>{{vm.stringify(vm.model.activity)}}</pre> + <b>Workflow</b> + <pre>{{vm.stringify(vm.model.workflow)}}</pre> </br-collapsible> + <div> + <br-collapsible state="vm.model.accordion.logbookOpen"> + <heading> Logbook (activity)</heading> + <br-logbook task-id="{{vm.model.activityId}}"></br-logbook> + </br-collapsible> + </div> + </div> </div> </div> - <div> - <br-collapsible state="vm.model.accordion.logbookOpen"> - <heading> Logbook (activity)</heading> - <br-logbook task-id="{{vm.model.activityId}}"></br-logbook> - </br-collapsible> - </div> </div> </ui-view>
