You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2022/10/21 13:58:27 UTC
[brooklyn-ui] 18/24: ui for replay, replay scope improvements, and misc tidies to recent workflow ui
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 <al...@cloudsoft.io>
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);