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 f66c75d59b97585dd0244478efb8c1b1d9acabb9 Author: Alex Heneveld <[email protected]> AuthorDate: Tue Oct 11 01:57:00 2022 +0100 ui for replay, replay scope improvements, and misc tidies to recent workflow ui --- .../components/providers/entity-api.provider.js | 2 +- .../components/task-list/task-list.directive.js | 7 +- .../workflow/workflow-step.template.html | 4 +- .../workflow/workflow-steps.directive.js | 19 +- .../app/components/workflow/workflow-steps.less | 30 +-- .../inspect/activities/detail/detail.controller.js | 212 +++++++++++++-------- .../main/inspect/activities/detail/detail.less | 17 ++ .../inspect/activities/detail/detail.template.html | 69 +++++-- .../inspect/activities/detail/dropdown-nested.js | 71 ++++--- .../providers/api-observer-interceptor.provider.js | 2 +- 10 files changed, 264 insertions(+), 169 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 aa34cfd3..39c28fe0 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 @@ -199,6 +199,6 @@ function EntityApi($http, $q) { } function replayWorkflow(applicationId, entityId, workflowId, step, options) { return $http.post('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflows/' + workflowId - + '/replay/from/' + step, {params: options, observable: true, ignoreLoadingBar: true}); + + '/replay/from/' + step, {params: options}); } } \ 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 db0d30c1..5f12c8a4 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 @@ -395,7 +395,6 @@ export function taskListDirective() { countAbsolute: countWorkflowsReplayedTopLevel, } - const filterWorkflowsReplayedNested = t => !t.isWorkflowFirstRun && t.isWorkflowLastRun && !t.isWorkflowTopLevel; const countWorkflowsReplayedNested = tasksAll.filter(filterWorkflowsReplayedNested).length; filtersFullList['_workflowReplayedNested'] = { display: 'Include replayed sub-workflows', @@ -514,12 +513,14 @@ export function taskListDirective() { } } +const filterWorkflowsReplayedNested = t => !t.isWorkflowFirstRun && t.isWorkflowLastRun && !t.isWorkflowTopLevel; function isScheduled(task) { return task && task.currentStatus && task.currentStatus.startsWith("Schedule"); } function isTopLevelTask(t, tasksById) { + if (filterWorkflowsReplayedNested(t)) return false; if (!t.submittedByTask) return true; if (t.forceTopLevel) return true; if (t.tags && t.tags.includes("TOP-LEVEL")) return true; @@ -533,11 +534,11 @@ function isNonTopLevelTask(t, tasksById) { } function isCrossEntityTask(t, tasksById) { if (isTopLevelTask(t, tasksById)) return false; - return t.submittedByTask.metadata.entityId !== t.entityId; + return t.submittedByTask && t.submittedByTask.metadata.entityId !== t.entityId; } function isNestedSameEntityTask(t, tasksById) { if (isTopLevelTask(t, tasksById)) return false; - return t.submittedByTask.metadata.entityId === t.entityId; + return t.submittedByTask && t.submittedByTask.metadata.entityId === t.entityId; } function filterWithId(tasks, tasksById, nextFilter) { if (!tasks) return tasks; 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 5ec2a31c..0554213c 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 @@ -164,14 +164,14 @@ </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})"> + <a href="" ui-sref="main.inspect.activities.detail({applicationId: sub.applicationId, entityId: sub.entityId, activityId: sub.workflowId, workflowLatestRun: true})"> <i class="fa fa-check check"></i> <span>{{ vm.getWorkflowNameFromReference(sub) }}</span> <span class="monospace">{{ sub.workflowId }}</span></a> </li> </ul> </div> <div class="btn-group" uib-dropdown ng-if="stepContext.subWorkflows.length==1"> - <a href="" ui-sref="main.inspect.activities.detail({applicationId: stepContext.subWorkflows[0].applicationId, entityId: stepContext.subWorkflows[0].entityId, activityId: stepContext.subWorkflows[0].workflowId})"> + <a href="" ui-sref="main.inspect.activities.detail({applicationId: stepContext.subWorkflows[0].applicationId, entityId: stepContext.subWorkflows[0].entityId, activityId: stepContext.subWorkflows[0].workflowId, workflowLatestRun: true})"> <span>{{ vm.getWorkflowNameFromReference(stepContext.subWorkflows[0]) }}</span> <span class="monospace">{{ stepContext.subWorkflows[0].workflowId }}</span> </a> 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 143b665c..15c9dce9 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 @@ -88,8 +88,8 @@ function makeArrows(workflow, steps) { const defs = []; defs.push('<marker id="arrowhead" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon fill="#000" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><'); - defs.push('<marker id="arrowhead-gray" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon fill="#C0C0C0" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><'); - defs.push('<marker id="arrowhead-red" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon fill="red" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><'); + defs.push('<marker id="arrowhead-gray" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon class="fill-future" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><'); + defs.push('<marker id="arrowhead-red" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon class="fill-failed" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><'); if (steps) { let gradientCount = 0; @@ -103,7 +103,7 @@ function makeArrows(workflow, steps) { } if (!opts) opts = {}; - const color = opts.color || (opts.colorEnd && opts.colorEnd==opts.colorStart ? opts.colorEnd : '#000'); + const color = opts.class ? '' : opts.color || (opts.colorEnd && opts.colorEnd==opts.colorStart ? opts.colorEnd : '#000'); const rightFarEdge = 56; const rightArrowheadStart = rightFarEdge - arrowheadLength; @@ -121,14 +121,14 @@ function makeArrows(workflow, steps) { const controlPointStart = controlPointRightArrowheadStart; const controlPointEnd = controlPointRightArrowheadStart; - const strokeConstant = - 'stroke="'+color+'"'; + const strokeConstant = color ? 'stroke="'+color+'"' : '' let standard = 'stroke-width="'+(opts.lineWidth || strokeWidth)+'" '+ 'fill="transparent" '+ '/>'; - if (!opts.hideArrowhead) standard = 'marker-end="url(#'+(opts.arrowheadId || 'arrowhead')+'" ' +standard; + if (opts.class) standard = 'class="'+opts.class+'" '+standard; + if (!opts.hideArrowhead) standard = 'marker-end="url(#'+(opts.arrowheadId || 'arrowhead')+')" ' +standard; if (opts.dashLength) standard = 'stroke-dasharray="'+opts.dashLength+'" '+standard; if (start) { @@ -154,10 +154,11 @@ function makeArrows(workflow, steps) { standard = 'stroke="url(#'+gradientId+')" ' + standard; } - return '<path d="M ' + rightFarEdge + ' ' + y1 + + const result = '<path d="M ' + rightFarEdge + ' ' + y1 + // ' L ' + r0 + ' ' + y1 + ' ' + ' C ' + controlPointStart + ' ' + y1 + ', ' + leftActive + ' ' + (yM - yMCH) + ', ' + leftActive + ' ' + yM + ' ' + ' S ' + controlPointEnd + ' ' + y2 + ', ' + rightArrowheadStart + ' ' + y2 + '" '+standard; + return result; } function stepY(n) { @@ -264,10 +265,10 @@ function makeArrows(workflow, steps) { let opts = { insertionPoint: 0 }; if (workflow.data.currentStepIndex === i && workflow.data.status && workflow.data.status.startsWith('ERROR')) { - recordTransition(i, -1, { ...opts, color: 'red', arrowheadId: 'arrowhead-red' }); + recordTransition(i, -1, { ...opts, class: 'arrow-failed', arrowheadId: 'arrowhead-red' }); } - opts = { ...opts, color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 }; + opts = { ...opts, class: 'arrow-future', arrowheadId: 'arrowhead-gray', dashLength: 8 }; let next = null; if (s.next) { 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 f17e70bc..9948ee90 100644 --- a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less +++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less @@ -37,29 +37,6 @@ border: solid @gray-light-lighter 1px; - // could do borders around active/error steps. but instead icons at left. - //&.current-step { - // border: solid @gray-light-lighter 2px; - // padding-left: 3px; - // padding-right: 3px; - //} - } - //&.workflow-status-RUNNING { - // .workflow-step.current-step { - // border: solid #363 2px; - // padding-left: 3px; - // padding-right: 3px; - // } - //} - //&.workflow-error { - // .workflow-step.current-step { - // border: solid #820 2px; - // padding-left: 3px; - // padding-right: 3px; - // } - //} - - .workflow-step { .rhs-icons { float: right; display: flex; @@ -256,8 +233,15 @@ svg.workflow-arrows { //opacity: 10%; + + .arrow-failed { stroke: @color-failed; } + .arrow-future { stroke: @gray-lighter; } + + .fill-failed { fill: @color-failed; } + .fill-future { fill: @gray-lighter; } } svg.workflow-arrows:hover { opacity: 100%; } + } 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 a5cbe6b2..1e7aedba 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 @@ -23,12 +23,12 @@ import {makeTaskStubFromWorkflowRecord} from "../activities.controller"; export const detailState = { name: 'main.inspect.activities.detail', - url: '/:activityId?workflowId', + url: '/:activityId?workflowId?workflowLatestRun', template: template, - controller: ['$scope', '$state', '$stateParams', '$log', '$uibModal', '$timeout', '$sanitize', '$sce', 'activityApi', 'entityApi', 'brUtilsGeneral', DetailController], + controller: ['$scope', '$state', '$stateParams', '$location', '$log', '$uibModal', '$timeout', '$sanitize', '$sce', 'activityApi', 'entityApi', 'brUtilsGeneral', DetailController], controllerAs: 'vm', } -function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeout, $sanitize, $sce, activityApi, entityApi, Utils) { +function DetailController($scope, $state, $stateParams, $location, $log, $uibModal, $timeout, $sanitize, $sce, activityApi, entityApi, Utils) { $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT); const { @@ -39,6 +39,10 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou $scope.workflowId = $stateParams.workflowId; let vm = this; + vm.redirectToWorkflowLatestRun = $stateParams.workflowLatestRun; + $stateParams.workflowLatestRun = null; + $location.search('workflowLatestRun', null) + vm.model = { appId: applicationId, entityId: entityId, @@ -51,17 +55,31 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou vm.modalTemplate = modalTemplate; vm.wideKilt = false; + vm.toggleOldWorkflowRunStepDetails = () => { $scope.showOldWorkflowRunStepDetails = !$scope.showOldWorkflowRunStepDetails; } + $scope.actions = {}; let observers = []; if ($state.current.name === detailState.name) { - function loadWorkflow(workflowTag, optimistic) { + function onActivityOrWorkflowUpdate() { + delete $scope.actions['cancel']; + if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) { + $scope.actions.cancel = { label: 'Cancel task', doAction: () => { activityApi.cancelActivity(activityId); } }; + } else if (vm.model.workflow.data && vm.model.workflow.data.taskId && vm.model.workflow.data.status === 'RUNNING') { + $scope.actions.cancel = { label: 'Cancel workflow', doAction: () => { activityApi.cancelActivity(vm.model.workflow.taskId); } }; + } + } + + function loadWorkflow(workflowTag, opts) { + if (!opts) opts = {}; if (!workflowTag) { workflowTag = {} - optimistic = true; + opts.optimistic = true; } + const optimistic = opts.optimistic; + vm.model.workflow.loading = 'loading'; $scope.workflowId = workflowTag.workflowId || $scope.workflowId || activityId; @@ -76,71 +94,123 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou vm.model.workflow.applicationId = workflowTag.applicationId; vm.model.workflow.entityId = workflowTag.entityId; - $scope.actions.workflowReplays = []; - if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) { - // can't replay if active (same logic as 'cancel') - } else { - $scope.actions.workflowReplays = []; - const stepIndex = (vm.model.workflow.tag || {}).stepIndex; + if (opts.nonTask) { + const wft = (vm.model.workflow.data.replays || []).find(t => t.taskId === activityId); + if (wft) { + vm.model.activity = makeTaskStubFromWorkflowRecord(vm.model.workflow.data, wft); + vm.model.workflow.tag = getTaskWorkflowTag(vm.model.activity); + } else { + throw "Workflow task " + activityId + " not stored on workflow"; + } + + // give a better error + vm.error = $sce.trustAsHtml('Limited information on workflow task <b>' + _.escape(activityId) + '</b>.<br/><br/>' + + (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc == -1 + ? "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.')); + } + + function processWorkflowData(wResponse2) { + // change the workflow object so widgets get refreshed + vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data }; - let replayableFromStart = vm.model.workflow.data.replayableFromStart, replayableContinuing = vm.model.workflow.data.replayableLastStep>=0; + const replays = (vm.model.workflow.data.replays || []); - if (replayableContinuing) { - $scope.actions.workflowReplays.push({ targetId: 'end', targetName: 'Resume '+(stepIndex>=0 ? 'workflow ' : '')+' (at step '+(vm.model.workflow.data.replayableLastStep+1)+')' }); + vm.model.workflow.runMultipleTimes = replays.length > 1; + let workflowReplayId = activityId; + if (!replays.find(r => r.taskId === workflowReplayId)) { + let submittedById = ((vm.model.activity.submittedByTask || {}).metadata || {}).id; + if (replays.find(r => r.taskId === submittedById)) workflowReplayId = submittedById; + else workflowReplayId = null; } + if (workflowReplayId) { + vm.model.workflow.runReplayId = workflowReplayId; + vm.model.workflow.runIsLatest = workflowReplayId == (replays[replays.length - 1] || {}).taskId; + vm.model.workflow.runIsOld = !vm.model.workflow.runIsLatest; + } + if (vm.model.workflow.runIsOld && vm.redirectToWorkflowLatestRun) { + vm.redirectToWorkflowLatestRun = false; + $state.go('main.inspect.activities.detail', { + applicationId: applicationId, + entityId: entityId, + activityId: (replays[replays.length - 1] || {}).taskId, + }); + } + + $scope.actions.workflowReplays = []; + if (vm.model.workflow.data.status !== 'RUNNING') { - // get current step, replay from that step - if (stepIndex>=0) { - const osi = workflow.data.oldStepInfo[stepIndex] || {}; - if (osi.replayableFromHere) { - $scope.actions.workflowReplays.push({ targetId: ''+stepIndex, targetName: 'Replay from here (step '+(stepIndex+1) }); - } else { - $scope.actions.workflowReplays.push({ targetId: ''+stepIndex, targetName: 'Force replay from here (step '+(stepIndex+1), force: true }); + $scope.actions.workflowReplays = []; + const stepIndex = (vm.model.workflow.tag || {}).stepIndex; + + let replayableFromStart = vm.model.workflow.data.replayableFromStart, replayableContinuing = vm.model.workflow.data.replayableLastStep>=0; + + if (replayableContinuing) { + $scope.actions.workflowReplays.push({ targetId: 'end', reason: 'Resume workflow at step '+(vm.model.workflow.data.replayableLastStep+1)+' from UI', + label: 'Resume '+(stepIndex>=0 ? 'workflow ' : '')+' (at step '+(vm.model.workflow.data.replayableLastStep+1)+')' }); } - } - if (replayableFromStart) { - let w1 = 'Restart', w2 = '(not resumable)'; - if (stepIndex<0) { w1 = 'Run'; w2 = 'again'; } - else if (_.isNil(stepIndex)) { w2 = '(did not start)'; } - else if (replayableContinuing) w2 = ''; + // get current step, replay from that step + if (stepIndex>=0) { + const osi = vm.model.workflow.data.oldStepInfo[stepIndex] || {}; + if (osi.replayableFromHere) { + $scope.actions.workflowReplays.push({ targetId: ''+stepIndex, reason: 'Replay workflow from step '+(stepIndex+1)+' from UI', + label: 'Replay from here (step '+(stepIndex+1) }); + } else { + $scope.actions.workflowReplays.push({ targetId: ''+stepIndex, reason: 'Force replay from step '+(stepIndex+1)+' from UI', + label: 'Force replay from here (step '+(stepIndex+1), force: true }); + } + } - $scope.actions.workflowReplays.push({targetId: 'start', targetName: 'Restart '+(stepIndex>=0 ? 'workflow ' : '')+reason}); - } + if (replayableFromStart) { + let w1 = 'Restart', w2 = '(not resumable)'; + if (stepIndex<0) { w1 = 'Run'; w2 = 'again'; } + else if (_.isNil(stepIndex)) { w2 = '(did not start)'; } + else if (replayableContinuing) w2 = ''; - if (!replayableFromStart) { - $scope.actions.workflowReplays.push({targetId: 'start', targetName: 'Force restart', force: true}); - } - // force replays - $scope.actions.workflowReplays.forEach(r => { - // could prompt for a reason - const targetId = r.targetId; - const opts = {}; - opts.reason = "UI manual replay"; - if (r.force) { - opts.force = true; - opts.reason += " (forced)"; + $scope.actions.workflowReplays.push({targetId: 'start', reason: 'Restart workflow from UI', + label: 'Restart '+(stepIndex>=0 ? 'workflow ' : '')+reason}); } - r.action = () => { - entityApi.replay(applicationId, entityId, $scope.workflowId. targetId, opts); - }; - }); + + if (!replayableFromStart) { + $scope.actions.workflowReplays.push({targetId: 'start', reason: 'Force restart from UI', + label: 'Force restart', force: true}); + } + // force replays + $scope.actions.workflowReplays.forEach(r => { + // could prompt for a reason + r.action = () => { + const opts = {}; + opts.reason = r.reason; + if (r.force) opts.force = true; + entityApi.replayWorkflow(applicationId, entityId, $scope.workflowId, r.targetId, opts) + .then(response => { + $state.go('main.inspect.activities.detail', { + applicationId: applicationId, + entityId: entityId, + activityId: response.data, + }); + }); + }; + }); + } + if (!$scope.actions.workflowReplays.length) delete $scope.actions['workflowReplays']; + + onActivityOrWorkflowUpdate(); } - if (!$scope.actions.workflowReplays.length) delete $scope.actions['workflowReplays']; + + processWorkflowData(wResponse); if (vm.model.workflow.data.status === 'RUNNING') wResponse.interval(1000); - observers.push(wResponse.subscribe((wResponse2)=> { - // change the workflow object so widgets get refreshed - vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data }; - })); + observers.push(wResponse.subscribe(processWorkflowData)); }).catch(error => { if (optimistic) { vm.model.workflow.loading = null; throw error; } + console.log("Unable to load workflow", $scope.workflowId, error); - console.log("ERROR loading workflow " + $scope.workflowId, error); vm.model.workflow.loading = 'error'; }); }; @@ -161,11 +231,6 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou } } - delete $scope.actions['cancel']; - if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) { - $scope.actions.cancel = { doAction: () => { activityApi.cancelActivity(activityId); } }; - } - $scope.workflowId = null; // if the task loads, force the workflow id to be found on it, otherwise ignore it if ((vm.model.activity.tags || []).find(t => t=="WORKFLOW")) { const workflowTag = getTaskWorkflowTag(vm.model.activity); @@ -175,13 +240,17 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou } } - vm.error = undefined; - if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<0) response.interval(1000); - observers.push(response.subscribe((response)=> { + function saveActivity(response) { vm.model.activity = response.data; + onActivityOrWorkflowUpdate(); vm.error = undefined; vm.errorBasic = false; - })); + } + + saveActivity(response); + + if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<0) response.interval(1000); + observers.push(response.subscribe(saveActivity)); }).catch((error)=> { $log.warn('Error loading activity for '+activityId, error); @@ -191,25 +260,9 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou '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 - function onNonTaskWorkflowLoad() { - const wft = (vm.model.workflow.data.replays || []).find(t => t.taskId === activityId); - if (wft) { - vm.model.activity = makeTaskStubFromWorkflowRecord(vm.model.workflow.data, wft); - vm.model.workflow.tag = getTaskWorkflowTag(vm.model.activity); - } else { - throw "Workflow task "+activityId+" not stored on workflow"; - } - - // give a better error - vm.error = $sce.trustAsHtml('Limited information on workflow task <b>' + _.escape(activityId) + '</b>.<br/><br/>' + - (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc==-1 - ? "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.') ); - } - - loadWorkflow({workflowId: activityId}).then(onNonTaskWorkflowLoad) + loadWorkflow({workflowId: activityId}, { nonTask: true }) .catch(error => { - loadWorkflow(null).then(onNonTaskWorkflowLoad) + loadWorkflow(null, { nonTask: true }) .catch(error => { $log.debug("ID "+activityId+"/"+$scope.workflowId+" does not correspond to workflow either", error); }); @@ -235,7 +288,8 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou activityApi.activityDescendants(activityId, 8, true).then((response)=> { vm.model.activitiesDeep = response.data; vm.error = undefined; - // TODO would be nice to subscribe more often, e.g. every second + + if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<0) response.interval(1000); observers.push(response.subscribe((response)=> { vm.model.activitiesDeep = response.data; if (!vm.errorBasic) { 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 0c01da1d..f72d2922 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 @@ -219,6 +219,23 @@ } } } + + .workflow-buttons { + float: right; + margin-top: -9px; + button { + //min-width: 15em; + //text-align: left; + } + } + + div.workflow-steps { + margin-top: 24px; + } + div.workflow-preface-para { + margin-top: 12px; + margin-bottom: 24px; + } } .dropdown-at-root { 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 08b4b889..3916a9eb 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 @@ -51,14 +51,14 @@ </br-button> <ul uib-dropdown-menu-nested class="dropdown-at-root dropdown-menu-right"> - <li><a href="" ng-if="actions.cancel" ng-click="actions.cancel.doAction()">Cancel</a></li> + <li><a href="" ng-if="actions.cancel" ng-click="actions.cancel.doAction()">{{actions.cancel.label}}</a></li> <li ng-if="actions.workflowReplays" uib-dropdown-nested dropdown-append-to-body="true"> <a href="" uib-dropdown-toggle-nested>Replay workflow <span class="caret"></span></a> <ul class="dropdown-submenu-left dropdown-at-root dropdown-menu-right" uib-dropdown-menu-nested> <li ng-repeat="replay in actions.workflowReplays track by $index" id="replay-{{replay.targetId}}"> - <a class="dropdown-item" href="" ng-click="replay.action()">{{ replay.targetName }}</a> + <a class="dropdown-item" href="" ng-click="replay.action()">{{ replay.label }}</a> </li> </ul> @@ -187,17 +187,17 @@ <div class="workflow-body"> <div ng-if="vm.model.workflow.loading == 'loaded'"> <div ng-if="vm.model.workflow.data.replays.length > 1"> - <div style="float: right; margin-top: -9px;" class="btn-group" uib-dropdown> + <div class="workflow-buttons 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 dropdown-menu-right dropdown-menu-replays" uib-dropdown-menu role="menu" aria-labelledby="replay-button"> - <li role="menuitem" ng-repeat="replay in vm.model.workflow.data.replays" id="workflow-replay-{{ replay.taskId }}"> + <li role="menuitem" ng-repeat="replay in vm.model.workflow.data.replays.slice().reverse()" id="workflow-replay-{{ replay.taskId }}"> <a href="" ui-sref="main.inspect.activities.detail({activityId: replay.taskId, workflowId: workflowId})" ng-class="{'selected' : vm.model.activityId === replay.taskId}"> <i class="fa fa-check check if-selected"></i> <!-- <span class="monospace">{{ replay.taskId }}</span>--> <span ng-if="replay.reasonForReplay">{{ replay.reasonForReplay }} (</span - ><span>{{ replay.submitTimeUtc | dateFilter: 'short' }}</span + ><span ng-if="replay.submitTimeUtc>0">{{ replay.submitTimeUtc | dateFilter: 'short' }}</span ><span ng-if="replay.reasonForReplay">)</span> </a> </li> @@ -214,6 +214,10 @@ This task is <span ng-if="!vm.isNullish(vm.model.workflow.tag.stepIndex)">for step <b>{{ vm.model.workflow.tag.stepIndex+1 }}</b> in + <span ng-if="vm.model.workflow.runReplayId"> + <span ng-if="vm.model.workflow.runIsOld">a previous run of </span> + <span ng-if="vm.model.workflow.runIsLatest">the most recent run of </span> + </span> <a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.data.taskId, workflowId})"> workflow <span class="monospace">{{vm.model.workflow.data.workflowId}}</span>: <b>{{vm.model.workflow.data.name}}</b></a>. @@ -225,18 +229,53 @@ </span> <span ng-if="vm.isNullish(vm.model.workflow.tag.stepIndex) && vm.model.activity.id"> for - <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 ng-if="vm.model.workflow.runIsOld" style="font-weight: bold;"> + a previous run of + <a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.data.taskId, workflowId})"> + workflow <span class="monospace">{{vm.model.workflow.data.workflowId}}</span>: + <b>{{vm.model.workflow.data.name}}</b>.</a> + </span> + <span ng-if="!vm.model.workflow.runIsOld"> + <span ng-if="vm.model.workflow.runIsLatest">the most recent run of </span> + workflow <span class="monospace">{{vm.model.workflow.data.workflowId}}</span>: + <b>{{vm.model.workflow.data.name}}</b>. </span> - workflow <span class="monospace">{{vm.model.workflow.data.workflowId}}</span>: - <b>{{vm.model.workflow.data.name}}</b>. </span> </div> - <div ng-if="showReplayHelp" style="margin-top: 12px; margin-bottom: 24px;"> + + <div ng-if="vm.model.workflow.data.parentId" class="workflow-preface-para"> + This is a sub-workflow in + <b><a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.data.parentId, workflowId: vm.model.workflow.data.parentId, workflowLatestRun: true})"> + workflow <span class="monospace">{{vm.model.workflow.data.parentId}}</span + ></a></b>. + </div> + + <div ng-if="vm.model.workflow.runIsOld" class="workflow-preface-para"> + For previous runs, the subtask view + <span ng-if="!vm.isNullish(vm.model.workflow.tag.stepIndex)">for + <a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.runReplayId, workflowId})"> + the relevant run + </a> + </span> + <span ng-if="vm.isNullish(vm.model.workflow.tag.stepIndex)"> + <span ng-if="showOldWorkflowRunStepDetails">further</span> + below + </span> + is normally more informative than workflow steps. + <span ng-if="!showOldWorkflowRunStepDetails"> + However the workflow step view + <a href="" ng-click="vm.toggleOldWorkflowRunStepDetails()"> + can be shown</a> + below if desired. + </span> + <span ng-if="showOldWorkflowRunStepDetails"> + The workflow step view immediately below + <a href="" ng-click="vm.toggleOldWorkflowRunStepDetails()"> + can be hidden</a>. + </span> + </div> + + <div ng-if="showReplayHelp" class="workflow-preface-para"> 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. @@ -245,7 +284,7 @@ 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> + <workflow-steps workflow="vm.model.workflow" task="vm.model.activity" ng-if="!vm.model.workflow.runIsOld || showOldWorkflowRunStepDetails"></workflow-steps> </div> <div ng-if="vm.model.workflow.loading != 'loaded'"> <loading-state error="vm.model.workflow.loading !== 'loading' ? 'Details of this workflow are no longer available.' : ''"></loading-state> diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js index 00680a61..93ec9d95 100644 --- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js +++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js @@ -67,6 +67,12 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) } }; + function containsNested(container, target) { + if (!container || !target || !container[0] || !target[0]) return false; + if (container[0]==target[0]) return true; + return containsNested(container, angular.element(target.parent())); + } + this.close = function(dropdownScope, element, appendTo) { if (openScope === dropdownScope) { openScope = null; @@ -79,6 +85,8 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) $document.off('click', closeDropdown); $document.off('keydown', this.keybindFilter); } + [openScope, oldOpenScopes].filter(candidateChild => candidateChild && candidateChild.getToggleElement && containsNested(dropdownScope.getDropdownElement(), candidateChild.getToggleElement())) + .forEach(containedChild => containedChild.isOpen = false); if (!appendTo) { return; @@ -104,52 +112,43 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) // unbound this event handler. So check openScope before proceeding. let scopesToApply = []; - function containsNested(container, target) { - if (!container) return false; - if (container==target) return true; - if (container[0] && container[0].contains && container[0].contains(target)) return true; - if (container.contains && container.contains(target)) return true; - - let kids = angular.element(container).children(); - if (kids && kids.length) { - for (let i=0; i<kids.length; i++) { - let found = containsNested(kids[i], target); - if (found) return true; - } - } - return false; + function isAnyTrigger($element) { + return $element.hasClass('dropdown-toggle'); } - function isAnyTrigger(element) { - return element.hasClass('dropdown-toggle'); + function isAnyAncestor($element, test) { + if (!$element || !$element[0]) return false; + if (test($element)) { + return true; + } + return isAnyAncestor(angular.element($element.parent()), test); } function closeIfApplicable(scope) { - if (evt && scope.getAutoClose() === 'disabled') { - return; - } + if (evt) { + if (scope.getAutoClose() === 'disabled') { + return; + } - if (evt && evt.which === 3) { - return; - } + if (evt.which === 3) { + return; + } - if (evt && - isAnyTrigger(angular.element(evt.target))) { - return; - } - // could do "is contained in any trigger"; but doesn't seem needed yet + if (isAnyAncestor(angular.element(evt.target), isAnyTrigger)) { + return; + } - var toggleElement = scope.getToggleElement(); - if (evt && toggleElement && containsNested(toggleElement, evt.target)) { - return; - } + var toggleElement = scope.getToggleElement(); + if (toggleElement && containsNested(angular.element(toggleElement), angular.element(evt.target))) { + return; + } - var dropdownElement = scope.getDropdownElement(); - if (evt && - scope.getAutoClose() === 'outsideClick' && - dropdownElement && containsNested(dropdownElement, evt.target)) { - return; + if (scope.getAutoClose() === 'outsideClick' && + containsNested(angular.element(scope.getDropdownElement()), angular.element(evt.target))) { + return; + } } + scope.isOpen = false; scopesToApply.push(scope); diff --git a/ui-modules/utils/providers/api-observer-interceptor.provider.js b/ui-modules/utils/providers/api-observer-interceptor.provider.js index a5ab9f4b..df8d21f8 100644 --- a/ui-modules/utils/providers/api-observer-interceptor.provider.js +++ b/ui-modules/utils/providers/api-observer-interceptor.provider.js @@ -47,7 +47,7 @@ export function apiObserverInterceptorProvider() { }; function doDriveBy(response, error = false) { - if (response.config.hasOwnProperty(OBSERVABLE) && response.config[OBSERVABLE]) { + if ((response.config || {}).hasOwnProperty(OBSERVABLE) && response.config[OBSERVABLE]) { response.clock = clock; response.interval = (interval) => { response.clock = Observable.interval(interval);
