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:09 UTC

[brooklyn-ui] branch master updated (51293560 -> fa05e72a)

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-ui.git


    from 51293560 fix ui test for duration computation
     new da317de9 ui for workflow on activities detail page
     new 0714fb85 workflow UI polish, subworkflows, bug-fixes
     new 17bc3ee4 exact copy of uib dropdown.js, to allow for nested dropdowns
     new d585befe essential changes to drodown-nested so it loads without conflicting
     new 0526f027 expand actions, move to top-right
     new 50e6787d workflow ui tidies following review
     new efb5511e improve workflow ui for GC'd tasks, plus other minor ui fixes
     new 6879475f tidy up workflow step title, show more info on steps
     new e86f96c6 tidy workflow consistency (when tasks don't load), date, misc
     new d14c56c0 update active tasks and workflows more often
     new 73eb8033 align effectors in table better
     new 54a57c41 consistent colours and icons for task and br status
     new c4e545f1 richer dropdowns on tasks list, filter by workflow
     new 8d2f4668 improve dropdowns, code, extend logic for workflows
     new 6c064438 fix a couple things on step arrows
     new afd62cf2 minor fixes for workflow arrows and indicators
     new 62c2b866 replay buttons and arrow tidies
     new f66c75d5 ui for replay, replay scope improvements, and misc tidies to recent workflow ui
     new f9cefb39 Merge branch 'master' into workflow-ui
     new 0e566e4e fix license header
     new 21e2f2ec tweaks to messages for clarity
     new 961e2ad9 support running a workflow from ui
     new 3c2649e4 improve task filters - subtasks, scheduled, initialization
     new fa05e72a more task list filter selection tweaks

The 24 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../entity-effector/entity-effector.html           |  18 +-
 .../entity-effector/entity-effector.less           |  50 +-
 .../components/providers/activity-api.provider.js  |   8 +-
 .../components/providers/entity-api.provider.js    |  19 +-
 .../components/task-list/task-list.directive.js    | 631 +++++++++++++++++----
 .../app/components/task-list/task-list.less        | 148 ++++-
 .../components/task-list/task-list.template.html   |  79 ++-
 .../components/workflow/workflow-step.directive.js | 195 +++++++
 .../workflow/workflow-step.template.html           | 235 ++++++++
 .../workflow/workflow-steps.directive.js           | 370 ++++++++++++
 .../app/components/workflow/workflow-steps.less    | 252 ++++++++
 .../workflow/workflow-steps.template.html}         |  27 +-
 ui-modules/app-inspector/app/index.js              |   5 +
 ui-modules/app-inspector/app/index.less            |   1 +
 .../inspect/activities/activities.controller.js    | 138 ++++-
 .../inspect/activities/activities.template.html    |   2 +-
 .../inspect/activities/detail/detail.controller.js | 240 +++++++-
 .../main/inspect/activities/detail/detail.less     |  84 ++-
 .../inspect/activities/detail/detail.template.html | 221 +++++++-
 .../inspect/activities/detail/dropdown-nested.js   | 519 +++++++++++++++++
 .../views/main/inspect/confirm.modal.template.html |   2 +-
 .../app/views/main/inspect/inspect.controller.js   |  52 +-
 .../app/views/main/inspect/inspect.template.html   |   3 +
 .../inspect/management/detail/detail.template.html |   4 +-
 ...plate.html => run-workflow-modal.template.html} |   6 +-
 .../home/app/views/about/about.controller.js       |   2 +
 ui-modules/shared/style/first.less                 |   2 +-
 ui-modules/utils/br-core/style/variables.less      |  14 +
 .../providers/api-observer-interceptor.provider.js |   8 +-
 ui-modules/utils/status/status.js                  |  14 +-
 ui-modules/utils/status/status.less                |   9 +
 ui-modules/utils/status/status.template.html       |  21 +
 ui-modules/utils/utils/momentp.js                  |   1 +
 33 files changed, 3099 insertions(+), 281 deletions(-)
 create mode 100644 ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js
 create mode 100644 ui-modules/app-inspector/app/components/workflow/workflow-step.template.html
 create mode 100644 ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js
 create mode 100644 ui-modules/app-inspector/app/components/workflow/workflow-steps.less
 copy ui-modules/{shared/partials/interstitial.html => app-inspector/app/components/workflow/workflow-steps.template.html} (53%)
 create mode 100644 ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js
 copy ui-modules/app-inspector/app/views/main/inspect/{add-child-modal.template.html => run-workflow-modal.template.html} (82%)


[brooklyn-ui] 04/24: essential changes to drodown-nested so it loads without conflicting

Posted by he...@apache.org.
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 d585befe1199ccbc33b08e5e938644203828fe4f
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Wed Oct 5 13:57:49 2022 +0100

    essential changes to drodown-nested so it loads without conflicting
---
 ui-modules/app-inspector/app/index.js              |  2 ++
 .../inspect/activities/detail/dropdown-nested.js   | 34 ++++++++++++----------
 2 files changed, 21 insertions(+), 15 deletions(-)

diff --git a/ui-modules/app-inspector/app/index.js b/ui-modules/app-inspector/app/index.js
index c82707b3..d8c00ebf 100755
--- a/ui-modules/app-inspector/app/index.js
+++ b/ui-modules/app-inspector/app/index.js
@@ -21,6 +21,7 @@ import ngCookies from "angular-cookies";
 import ngResource from "angular-resource";
 import ngSanitize from "angular-sanitize";
 import uiRouter from "angular-ui-router";
+import dropdownNested from "views/main/inspect/activities/detail/dropdown-nested";
 
 import brCore from 'brooklyn-ui-utils/br-core/br-core';
 
@@ -68,6 +69,7 @@ import brandAngularJs from 'brand-angular-js';
 const IS_PRODUCTION = process.env.NODE_ENV === 'production' || false;
 
 angular.module('brooklynAppInspector', [ngResource, ngCookies, ngSanitize, uiRouter, brCore, brUtilsGeneral,
+    dropdownNested,
     brServerStatus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brSensitiveField, brooklynUserManagement,
     brYamlEditor, brWebNotifications, brExpandablePanel, 'xeditable', brLogbook, apiProvider, entityTree, loadingState,
     configSensorTable, entityEffector, entityPolicy, breadcrumbNavigation, taskList, taskSunburst, stream, adjunctsList,
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 cbfe420f..6d983bb9 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
@@ -1,11 +1,15 @@
-angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
+const MODULE_NAME = 'ui.bootstrap.dropdown.nested';
 
-    .constant('uibDropdownConfig', {
+export default MODULE_NAME;
+
+angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
+
+    .constant('uibDropdownConfigNested', {
         appendToOpenClass: 'uib-dropdown-open',
         openClass: 'open'
     })
 
-    .service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
+    .service('uibDropdownServiceNested', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
         var openScope = null;
         var openedContainers = $$multiMap.createNew();
 
@@ -135,7 +139,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.
         };
     }])
 
-    .controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) {
+    .controller('UibDropdownControllerNested', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfigNested', 'uibDropdownServiceNested', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownServiceNested, $animate, $position, $document, $compile, $templateRequest) {
         var self = this,
             scope = $scope.$new(), // create a child scope so we are not polluting original one
             templateScope,
@@ -317,7 +321,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.
             var openContainer = appendTo ? appendTo : $element;
             var dropdownOpenClass = appendTo ? appendToOpenClass : openClass;
             var hasOpenClass = openContainer.hasClass(dropdownOpenClass);
-            var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo);
+            var isOnlyOpen = uibDropdownServiceNested.isOnlyOpen($scope, appendTo);
 
             if (hasOpenClass === !isOpen) {
                 var toggleClass;
@@ -341,17 +345,17 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.
                             var newEl = dropdownElement;
                             self.dropdownMenu.replaceWith(newEl);
                             self.dropdownMenu = newEl;
-                            $document.on('keydown', uibDropdownService.keybindFilter);
+                            $document.on('keydown', uibDropdownServiceNested.keybindFilter);
                         });
                     });
                 } else {
-                    $document.on('keydown', uibDropdownService.keybindFilter);
+                    $document.on('keydown', uibDropdownServiceNested.keybindFilter);
                 }
 
                 scope.focusToggleElement();
-                uibDropdownService.open(scope, $element, appendTo);
+                uibDropdownServiceNested.open(scope, $element, appendTo);
             } else {
-                uibDropdownService.close(scope, $element, appendTo);
+                uibDropdownServiceNested.close(scope, $element, appendTo);
                 if (self.dropdownMenuTemplateUrl) {
                     if (templateScope) {
                         templateScope.$destroy();
@@ -370,19 +374,19 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.
         });
     }])
 
-    .directive('uibDropdown', function() {
+    .directive('uibDropdownNested', function() {
         return {
-            controller: 'UibDropdownController',
+            controller: 'UibDropdownControllerNested',
             link: function(scope, element, attrs, dropdownCtrl) {
                 dropdownCtrl.init();
             }
         };
     })
 
-    .directive('uibDropdownMenu', function() {
+    .directive('uibDropdownMenuNested', function() {
         return {
             restrict: 'A',
-            require: '?^uibDropdown',
+            require: '?^uibDropdownNested',
             link: function(scope, element, attrs, dropdownCtrl) {
                 if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) {
                     return;
@@ -402,9 +406,9 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.
         };
     })
 
-    .directive('uibDropdownToggle', function() {
+    .directive('uibDropdownToggleNested', function() {
         return {
-            require: '?^uibDropdown',
+            require: '?^uibDropdownNested',
             link: function(scope, element, attrs, dropdownCtrl) {
                 if (!dropdownCtrl) {
                     return;


[brooklyn-ui] 10/24: update active tasks and workflows more often

Posted by he...@apache.org.
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 d14c56c011d2d31565d9c67b3ad3b5b3b6cbcafe
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 6 14:54:05 2022 +0100

    update active tasks and workflows more often
---
 .../inspect/activities/activities.controller.js    | 23 ----------------------
 .../inspect/activities/detail/detail.controller.js |  9 +++------
 .../providers/api-observer-interceptor.provider.js |  6 +++++-
 ui-modules/utils/utils/momentp.js                  |  1 +
 4 files changed, 9 insertions(+), 30 deletions(-)

diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index 39f0622b..3e561ee8 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -86,7 +86,6 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                     newActivity.isWorkflowOldReplay = wft.workflowId !== wft.taskId;
                 });
             });
-            newActivitiesMap['extra'] = makeTaskStubMock("Extra workflow", "extra", applicationId, entityId);
 
             vm.activitiesMap = newActivitiesMap;
             vm.activities = Object.values(vm.activitiesMap);
@@ -166,25 +165,3 @@ export function makeTaskStubFromWorkflowRecord(wf, wft) {
         ],
     };
 };
-
-// for testing only
-export function makeTaskStubMock(name, id, applicationId, entityId) {
-    return {
-        id,
-        displayName: name,
-        entityId: entityId,
-        isError: true,
-        currentStatus: "Unavailable",
-        submitTimeUtc: Date.now()-5000,
-        startTimeUtc: Date.now()-4000,
-        endTimeUtc: Date.now()-1000,
-        tags: [
-            "WORKFLOW",
-            {
-                workflowId: 'extra',
-                applicationId: applicationId,
-                entityId: entityId,
-            },
-        ],
-    };
-}
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
index b8dfc022..a106bc04 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
@@ -19,7 +19,7 @@
 import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
 import template from "./detail.template.html";
 import modalTemplate from './kilt.modal.template.html';
-import {makeTaskStubFromWorkflowRecord, makeTaskStubMock} from "../activities.controller";
+import {makeTaskStubFromWorkflowRecord} from "../activities.controller";
 
 export const detailState = {
     name: 'main.inspect.activities.detail',
@@ -92,6 +92,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 }
                 if (!vm.actions.workflowReplays.length) delete vm.actions['workflowReplays'];
 
+                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 };
@@ -139,6 +140,7 @@ 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)=> {
                 vm.model.activity = response.data;
                 vm.error = undefined;
@@ -171,11 +173,6 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
 
             }).catch(error2 => {
                 $log.debug("ID "+activityId+" does not correspond to workflow either", error2);
-
-                // vm.error = $sce.trustAsHtml('Mock data for workflow task <b>' + _.escape(activityId) + '</b>.');
-                //
-                // vm.model.activity = makeTaskStubMock("Extra workflow task", "extra", applicationId, entityId);
-                // vm.model.workflow.tag = findWorkflowTag(vm.model.activity);
             });
         });
 
diff --git a/ui-modules/utils/providers/api-observer-interceptor.provider.js b/ui-modules/utils/providers/api-observer-interceptor.provider.js
index 2720b663..a5ab9f4b 100644
--- a/ui-modules/utils/providers/api-observer-interceptor.provider.js
+++ b/ui-modules/utils/providers/api-observer-interceptor.provider.js
@@ -48,9 +48,13 @@ export function apiObserverInterceptorProvider() {
 
             function doDriveBy(response, error = false) {
                 if (response.config.hasOwnProperty(OBSERVABLE) && response.config[OBSERVABLE]) {
+                    response.clock = clock;
+                    response.interval = (interval) => {
+                        response.clock = Observable.interval(interval);
+                    }
                     response.subscribe = (next, error, complete)=> {
                         if (!OBSERVER_CACHE.has(response.config.url)) {
-                            OBSERVER_CACHE.set(response.config.url, clock.mapTo(coldObservableFactory(response.config)).exhaust().share());
+                            OBSERVER_CACHE.set(response.config.url, response.clock.mapTo(coldObservableFactory(response.config)).exhaust().share());
                         }
                         return OBSERVER_CACHE.get(response.config.url).subscribe(next, error, complete);
                     }
diff --git a/ui-modules/utils/utils/momentp.js b/ui-modules/utils/utils/momentp.js
index d8691736..a0beb714 100644
--- a/ui-modules/utils/utils/momentp.js
+++ b/ui-modules/utils/utils/momentp.js
@@ -22,6 +22,7 @@
 import {capitalize, rounded} from "./general";
 
 export class MomentPrecise {
+    // means all time-ago round to 5s which is a nicer UX than constantly updating (even if occasionally misleading)
     precisionForFromNow = 5000;
     summaryForBelowPrecisionThreshhold = "a few seconds";
     capitalized = false;


[brooklyn-ui] 13/24: richer dropdowns on tasks list, filter by workflow

Posted by he...@apache.org.
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 c4e545f11e76b9f3f515db84eac07a3234e7e78d
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Oct 7 13:50:26 2022 +0100

    richer dropdowns on tasks list, filter by workflow
    
    much better approach to initializing the dropdowns for task list view
---
 .../components/task-list/task-list.directive.js    | 532 +++++++++++++++++----
 .../app/components/task-list/task-list.less        |  97 +++-
 .../components/task-list/task-list.template.html   |  50 +-
 .../components/workflow/workflow-step.directive.js |   6 +
 .../workflow/workflow-step.template.html           |  11 +-
 .../inspect/activities/activities.controller.js    |  68 +--
 .../inspect/activities/detail/detail.controller.js |  16 +-
 .../main/inspect/activities/detail/detail.less     |   5 +-
 .../inspect/activities/detail/detail.template.html |  11 +-
 9 files changed, 644 insertions(+), 152 deletions(-)

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 35fc8b03..e890a283 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
@@ -29,7 +29,6 @@ angular.module(MODULE_NAME, [])
     .filter('timeAgoFilter', timeAgoFilter)
     .filter('dateFilter', dateFilter)
     .filter('durationFilter', durationFilter)
-    .filter('activityTagFilter', activityTagFilter)
     .filter('activityFilter', ['$filter', activityFilter]);
 
 export default MODULE_NAME;
@@ -54,34 +53,140 @@ export function taskListDirective() {
             // transient set when those tags seen
         };
 
-        setFiltersForTasks($scope, isActivityChildren);
-        $scope.filterValue = $scope.search;
-
+        $scope.isEmpty = x => _.isNil(x) || x.length==0 || (typeof x === "object" && Object.keys(x).length==0);
+        $scope.filters = { available: {}, selectedFilters: {}, selectedIds: {} };
         $scope.model = {
             appendTo: $element,
             filterResult: null,
-            filterByTag: isActivityChildren ? $scope.filters['_top']
-                : $scope.taskType === 'activity' ? $scope.filters['_effectorsTop']
-                : $scope.filters[$scope.taskType || '_effectorsTop'],
         };
+        $scope.tasksFilteredByTag = [];
+
+        $scope.findTasksExcludingCategory = (tasks, selected, categoryToExclude) => {
+            let result = tasks || [];
+
+            if (selected) {
+                _.uniq(Object.values(selected).map(f => f.category)).forEach(category => {
+                    if (categoryToExclude === '' || categoryToExclude != category) {
+                        let newResult = [];
+                        if ($scope.filters.startingSetFilterForCategory[category]) {
+                            newResult = $scope.filters.startingSetFilterForCategory[category](result);
+                        }
+                        Object.values(selected).filter(f => f.category === category).forEach(f => {
+                            const filter = f.filter;
+                            if (!filter) {
+                                console.warn("Incomplete activities tag filter", tagF);
+                            } else {
+                                newResult = newResult.concat(filter(result));
+                            }
+                        });
+
+                        // limit result, but preserving order
+                        newResult = newResult.map(t => t.id);
+                        result = result.filter(t => newResult.includes(t.id));
+                    }
+                })
+            }
+            return result;
+        };
+        $scope.recomputeTasks = () => {
+            $scope.tasksFilteredByTag = $scope.findTasksExcludingCategory(
+                tasksAfterGlobalFilters($scope.tasks, $scope.globalFilters),
+                $scope.filters.selectedFilters, '');
+
+            // do this to update the counts
+            setFiltersForTasks($scope, isActivityChildren);
+
+            // now update name
+            const enabledCategories = _.uniq(Object.values($scope.filters.selectedFilters).map(f => f.category));
+            let filterNameParts = Object.entries($scope.filters.displayNameForCategory).map(([category, nameFn]) => {
+                if (!enabledCategories.includes(category)) return null;
+                let nf = $scope.filters.displayNameForCategory[category];
+                return nf ? nf(Object.values($scope.filters.selectedFilters).filter(f => f.category === category)) : null;
+            }).filter(x => x);
+            $scope.filters.selectedDisplayName = filterNameParts.length ? filterNameParts.join('; ') :
+                isActivityChildren ? 'all sub-tasks' : 'all tasks';
+        };
+
+        function selectFilter(filterId, state) {
+            // annoying, but since task list is live updated, we store the last value of selectedIds in the event filters come and go;
+            // mainly tried because initial order could be too strange, but now we correct that, so this isn't so important
+            let oldTheoreticalEnablement = $scope.filters.selectedIds[filterId];
+
+            const f = $scope.filters.available[filterId];
+            if (!f) {
+                console.log("FILTER "+filterId+" not available yet, storing theoretical enablement");
 
-        const activityTagFilterApplication = () => activityTagFilter()($scope.tasks, [$scope.model.filterByTag, $scope.globalFilters]);
-        if ((!$scope.taskType || $scope.taskType.startsWith('activity')) && (!$scope.model.filterByTag || activityTagFilterApplication().length==0 )) {
-            // show all if default view is empty, unless explicit tag was requested
-            $scope.model.filterByTag = $scope.filters['_top'];
-            if (!$scope.model.filterByTag || activityTagFilterApplication().length == 0 ) {
-                $scope.model.filterByTag = $scope.filters['_recursive'] || $scope.model.filterByTag;
+                if (!_.isNil(state) ? state : !oldTheoreticalEnablement) {
+                    $scope.filters.selectedIds[filterId] = 'theoretically-enabled';
+                } else {
+                    delete $scope.filters.selectedIds[filterId];
+                }
+
+                // we tried to select eg effector, when it didn't exist
+                return false;
+            } else {
+                f.select(filterId, f, state);
+                return true;
             }
         }
+
+        setFiltersForTasks($scope, isActivityChildren);
+        $scope.filterValue = $scope.search;
+
+        selectFilter("_top", true);
+        selectFilter("_anyTypeTag", true);
+        if ($scope.taskType === 'activity') {
+            // default?
+            selectFilter('EFFECTOR');
+            selectFilter('WORKFLOW');
+        } else if ($scope.taskType) {
+            selectFilter($scope.taskType);
+        } else {
+            // TODO when is this called?
+            selectFilter('EFFECTOR');
+            selectFilter('WORKFLOW');
+        }
+
+        cacheSelectedIdsFromFilters($scope);
+        selectFilter("_workflowReplayed");
+        selectFilter("_workflowNonLastReplayHidden");
+
+        console.log($scope.filters);
+
+        // // this would be nice, but it doesn't play nice with dynamic task updates
+        // // sometimes no tasks are loaded yet and this enables the "all" but then tasks get loaded
+        // if ($scope.tasksFilteredByTag.length==0) {
+        //     // if nothing found at top level then broaden
+        //     selectFilter("_top", false);
+        // }
+
+
+
+        // TODO check taskType=activity...  .... can they not all just leave it off, to send the default; send the default?
+        // and make sure others send EFFECTOR
+
+        // if ((!$scope.taskType || $scope.taskType.startsWith('activity')) && (!filterPreselected || $scope.tasksFilteredByTag.length==0 )) {
+        //     // if nothing found with filters, try disabling the filters
+        //     filterPreselected = selectFilter('_top', false);
+        //     if (!filterPreselected || $scope.tasksFilteredByTag.length == 0 ) {
+        //         selectFilter('_top', true);
+        //     }
+        // }
+
         $scope.isScheduled = isScheduled;
 
         $scope.$watch('tasks', ()=>{
-            setFiltersForTasks($scope, isActivityChildren);
+            $scope.recomputeTasks();
         });
+        $scope.$watch('globalFilters', ()=>{
+            $scope.recomputeTasks();
+        });
+
         $scope.getTaskDuration = function(task) {
             if (!task.startTimeUtc) {
                 return null;
             }
+            if (!_.isNil(task.endTimeUtc) && task.endTimeUtc <= 0) return null;
             return (task.endTimeUtc === null ? new Date().getTime() : task.endTimeUtc) - task.startTimeUtc;
         }
 
@@ -93,21 +198,8 @@ export function taskListDirective() {
             if (tag) return tag.workflowId;
             return null;
         };
-    }
 
-    function tagReducer(result, tag) {
-        if (typeof tag === 'string') {
-            if (result.hasOwnProperty(tag)) {
-                result[tag].count ++;
-            } else {
-                result[tag] = {
-                    display: 'Tag: '+tag.toLowerCase(),
-                    tag,
-                    count: 1,
-                }
-            }
-        }
-        return result;
+        $scope.recomputeTasks();
     }
 
     function setFiltersForTasks(scope, isActivityChildren) {
@@ -116,7 +208,7 @@ export function taskListDirective() {
 
         // include a toggle for transient tasks
         if (!globalFilters.transient) {
-            const numTransient = tasksWithTag(tasksAll, 'TRANSIENT').length;
+            const numTransient = filterForTasksWithTag('TRANSIENT')(tasksAll).length;
             if (numTransient>0 && numTransient<tasksAll.length) {
                 // only default to filtering transient if some but not all are transient
                 globalFilters.transient = {
@@ -132,49 +224,323 @@ export function taskListDirective() {
         }
 
         const tasks = tasksAfterGlobalFilters(tasksAll, globalFilters);
-        const tops = topLevelTasks(tasks);
 
-        let defaultTags = {};
-        defaultTags['_top'] = {
-            display: 'All top-level tasks',
-            filter: topLevelTasks,
-            count: tops.length,
+        function defaultToggleFilter(tag, value, forceValue, fromUi, skipRecompute) {
+            if ((scope.filters.selectedIds[tag] && _.isNil(forceValue)) || forceValue===false) {
+                delete scope.filters.selectedIds[tag];
+                delete scope.filters.selectedFilters[tag];
+                if (value.onDisabledPost) value.onDisabledPost(tag, value, forceValue);
+            } else {
+                if (value.onEnabledPre) value.onEnabledPre(tag, value, forceValue);
+                scope.filters.selectedIds[tag] = 'enabled';
+                scope.filters.selectedFilters[tag] = value;
+            }
+            if (fromUi) {
+                // on a UI click, don't try to be too clever about remembered IDs
+                cacheSelectedIdsFromFilters(scope);
+            }
+
+            if (!skipRecompute) scope.recomputeTasks();
         }
-        if (tasks.length > tops.length) {
-            defaultTags['_recursive'] = {
-                display: 'All tasks (recursive)',
-                filter: input => input,
-                count: tasks.length,
+
+        /*
+          MENU should look like following, with group-specific behaviour for filtering and enablement,
+          e.g. auto-enable all of first group if only is de-selected:
+
+          Only show top-level tasks
+        x Show tasks called from other entities
+                submittedByTask==null ||
+                submittedByTask.metadata.entityId != entityId
+          Show tasks nested within this entity
+          ---
+          Any task type/tag
+        x Effector calls
+        x Workflows
+          Tag: tag1
+          Tag: tag2
+
+
+TODO workflow ui
+          ? most recent run of workflow only
+             combine others under last if loaded
+          ? show individual workflows resumed on startup! ? label as top-level?
+         */
+
+        function clearCategory(category) {
+            return function(filterId, filter, forceValue) {
+                Object.entries(scope.filters.selectedFilters).forEach( ([k,v])=> {
+                    if (v.category === (category || filter.category)) {
+                        delete scope.filters.selectedFilters[k];
+                    }
+                });
             }
         }
-        defaultTags['_effectorsTop'] = {
-            display: 'Effectors (top-level)',
-            filter: tt => tasksWithTag(topLevelTasks(tt), 'EFFECTOR'),
-            count: tasksWithTag(tops, 'EFFECTOR').length,
+        function clearOther(idToClear) {
+            return function(filterId, filter, forceValue) {
+                delete scope.filters.selectedFilters[idToClear];
+            }
         }
-        defaultTags['EFFECTOR'] = {
-            display: 'Effectors (recursive)',
-            tag: 'EFFECTOR',
-            count: 0,
+        function enableFilterIfCategoryEmpty(idToEnable, category) {
+            return function(filterId, filter, forceValue) {
+                if (!Object.values(scope.filters.selectedFilters).find(f => f.category === (category||filter.category))) {
+                    // empty
+                    const other = scope.filters.available[idToEnable || filterId];
+                    if (other) scope.filters.selectedFilters[idToEnable || filterId] = other;
+                }
+            }
         }
-        if (isActivityChildren) {
-            defaultTags['_top'].display = 'Direct sub-tasks';
-            if (defaultTags['_recursive']) defaultTags['_recursive'].display = 'All sub-tasks (recursive)';
+        function enableOthersIfCategoryEmpty(idToLeaveDisabled, category) {
+            return function(filterId, filter, forceValue) {
+                if (!Object.values(scope.filters.selectedFilters).find(f => f.category === (category||filter.category))) {
+                    // empty
+                    Object.entries(scope.filters.available).forEach( ([k,f]) => {
+                        if (f.category === (category||filter.category) && k !== (idToLeaveDisabled || filterId)) {
+                            scope.filters.selectedFilters[k] = f;
+                        }
+                    });
+                }
+            }
         }
 
-        const result = tasks.reduce((result, subTask)=> {
-            return subTask.tags.reduce(tagReducer, result);
-        }, defaultTags);
+        const defaultFilters = {};
 
-        // could suppress if no effectors
-        // if (!result['_effectorsTop'].count) {
-        //     delete result['_effectorsTop'];
-        //     if (!result['EFFECTOR'].count) {
-        //         delete result['EFFECTOR'];
-        //     }
-        // }
+        let tasksById = tasksAll.reduce( (result,t) => { result[t.id] = t; return result; }, {} );
+        function filterTopLevelTasks(tasks) { return filterWithId(tasks, tasksById, isTopLevelTask); }
+        function filterNonTopLevelTasks(tasks) { return filterWithId(tasks, tasksById, isNonTopLevelTask); }
+        function filterCrossEntityTasks(tasks) { return filterWithId(tasks, tasksById, isCrossEntityTask); }
+        function filterNestedSameEntityTasks(tasks) { return filterWithId(tasks, tasksById, isNestedSameEntityTask); }
 
-        scope.filters = result; //previously we extended, but now allow to clear
+        scope.filters.startingSetFilterForCategory = {
+            nested: filterTopLevelTasks,
+        };
+        function getFilterOrEmpty(id) {
+            return id && (id.filter ? id : scope.filters.available[id]) || {};
+        }
+        scope.filters.displayNameForCategory = {
+            nested: set => {
+                if (!set || !set.length) return null;
+                let nestedFiltersAvailable = Object.values(scope.filters.available).filter(f => f.category === 'nested');
+                if (set.length == nestedFiltersAvailable.length-1 && !set[0].isDefault) {
+                    // everything but first is selected, so no message
+                    return null;
+                }
+                if (set.length==1) {
+                    return getFilterOrEmpty(set[0]).displaySummary;
+                }
+                // all tasks
+                return null;
+            },
+            'type/tag': set => {
+                if (!set || !set.length) return null;
+                if (set.length<=3) {
+                    let tags = set.map(s => (getFilterOrEmpty(s).displaySummary || '').toLowerCase()).filter(x => x);
+                    if (tags.length==0) return null;
+                    if (tags.length==1) return tags[0];
+                    if (tags.length==2) return tags[0] + ' or ' + tags[1];
+                    if (tags.length==3) return tags[0] + ', ' + tags[1] + ', or ' + tags[2];
+                }
+                return 'any of multiple tags'
+            },
+        };
+        defaultFilters['_top'] = {
+            display: 'Only show ' + (isActivityChildren ? 'direct sub-tasks' : 'top-level tasks'),
+            displaySummary: 'only top-level tasks',
+            isDefault: true,
+            filter: filterTopLevelTasks,  // redundant with starting set, but contributes the right count
+            category: 'nested',
+            onEnabledPre: clearCategory(),
+            onDisabledPost: enableOthersIfCategoryEmpty('_top'),
+        }
+        if (!isActivityChildren) {
+            defaultFilters['_cross_entity'] = {
+                display: 'Include cross-entity sub-tasks',
+                displaySummary: 'cross-entity tasks',
+                filter: filterCrossEntityTasks,
+                category: 'nested',
+                onEnabledPre: clearOther('_top'),
+                onDisabledPost: enableFilterIfCategoryEmpty('_top'),
+            }
+            defaultFilters['_recursive'] = {
+                display: 'Include sub-tasks on this entity',
+                displaySummary: 'sub-tasks',
+                filter: filterNestedSameEntityTasks,
+                category: 'nested',
+                onEnabledPre: clearOther('_top'),
+                onDisabledPost: enableFilterIfCategoryEmpty('_top'),
+            }
+        } else {
+            defaultFilters['_recursive'] = {
+                display: 'Show all sub-tasks',
+                displaySummary: 'sub-tasks',
+                filter: filterNonTopLevelTasks,
+                category: 'nested',
+                onEnabledPre: clearOther('_top'),
+                onDisabledPost: enableFilterIfCategoryEmpty('_top'),
+            }
+        }
+
+        const countWorkflowsWhichAreNestedButHaveReplayed = tasksAll.filter(t =>
+            t.isReplayedWorkflowLatest && t.submittedByTask
+        ).length;
+        defaultFilters['_workflowReplayed'] = {
+            display: 'Include workflow sub-tasks which are replayed',
+            displaySummary: null,
+            filter: tasks => tasks.filter(t => t.isReplayedWorkflowLatest && t.submittedByTask),
+            category: 'nested',
+            count: countWorkflowsWhichAreNestedButHaveReplayed,
+            countAbsolute: countWorkflowsWhichAreNestedButHaveReplayed,
+            onEnabledPre: clearCategory(),
+            onDisabledPost: enableOthersIfCategoryEmpty('_anyTypeTag'),
+        }
+
+        const countWorkflowsWhichArePreviousReplays = tasksAll.filter(t => t.isWorkflowPreviousRun).length;
+        defaultFilters['_workflowNonLastReplayHidden'] = {
+            display: 'Exclude old runs of workflows',
+            help: 'Some workflows have been replayed, either manually or on a server restart or failover. ' +
+                'To simplify the display, old runs of workflow invocations which have been replayed are excluded here by default. ' +
+                'The most recent replay will be included, subject to other filters, and previous replays can be accessed ' +
+                'on the workflow page.',
+            displaySummary: null,
+            filter: tasks => tasks.filter(t => {
+                    return _.isNil(t.isWorkflowPreviousRun) || !t.isWorkflowPreviousRun;
+                }),
+            count: countWorkflowsWhichArePreviousReplays,
+            countAbsolute: countWorkflowsWhichArePreviousReplays,
+            category: 'workflow',
+            onEnabledPre: null,
+            onDisabledPost: null,
+        }
+
+        const countWorkflowsWithoutTaskWhichAreCompleted = tasksAll.filter(t => t.endTimeUtc>0 && t.isTaskStubFromWorkflowRecord).length;
+        defaultFilters['_workflowCompletedWithoutTaskHidden'] = {
+            display: 'Exclude old completed workflows',
+            help: 'Some older workflows no longer have a task record, '+
+                'either because they completed in a previous server prior to a server restart or failover, ' +
+                'or because their tasks have been cleared from memory in this server. ' +
+                'These can be excluded to focus on more recent tasks.',
+            displaySummary: null,
+            // filter: tasks => tasks.filter(t => t.isWorkflowPreviousRun !== false),
+            filter: tasks => tasks.filter(t => !(t.endTimeUtc>0 && t.isTaskStubFromWorkflowRecord)),
+            count: countWorkflowsWithoutTaskWhichAreCompleted,
+            countAbsolute: countWorkflowsWithoutTaskWhichAreCompleted,
+            category: 'workflow2',
+            onEnabledPre: null,
+            onDisabledPost: null,
+        }
+
+        defaultFilters['_anyTypeTag'] = {
+            display: 'Any task type or tag',
+            displaySummary: null,
+            filter: input => input,
+            category: 'type/tag',
+            onEnabledPre: clearCategory(),
+            onDisabledPost: enableOthersIfCategoryEmpty('_anyTypeTag'),
+        }
+
+        function addTagFilter(tag, target, display, displaySummary) {
+            if (!target[tag]) target[tag] = {
+                display: display,
+                displaySummary: displaySummary || tag.toLowerCase(),
+                filter: filterForTasksWithTag(tag),
+                category: 'type/tag',
+                onEnabledPre: clearOther('_anyTypeTag'),
+                onDisabledPost: enableFilterIfCategoryEmpty('_anyTypeTag'),
+            }
+        }
+        // put these first
+        addTagFilter('EFFECTOR', defaultFilters, 'Effectors', 'effector');
+        addTagFilter('WORKFLOW', defaultFilters, 'Workflow');
+
+        const filtersIncludingTags = {...defaultFilters};
+
+        // add filters for other tags
+        tasks.forEach(t =>
+            (t.tags || []).filter(tag => typeof tag === 'string' && tag.length < 32).forEach(tag =>
+                    addTagFilter(tag, filtersIncludingTags, 'Tag: ' + tag.toLowerCase())
+            ));
+
+        // fill in fields
+
+        Object.entries(filtersIncludingTags).forEach(([k, f]) => {
+            if (!f.select) f.select = defaultToggleFilter;
+            if (!f.onClick) f.onClick = (filterId, filter) => defaultToggleFilter(filterId, filter, null, true);
+
+            if (_.isNil(f.count)) f.count = scope.findTasksExcludingCategory(f.filter(tasks), scope.filters.selectedFilters, f.category).length;
+            if (_.isNil(f.countAbsolute)) f.countAbsolute = f.filter(tasks).length;
+        });
+
+        function updateSelectedFilters(newValues) {
+            const deferredCalls = [];
+            Object.entries(scope.filters.selectedIds).forEach(([filterId,filterSelectionNote]) => {
+                const newValue = newValues[filterId];
+                const oldValue = scope.filters.selectedFilters[filterId];
+                //console.log("enabling ",filterId,filterSelectionNote,newValue,oldValue);
+                scope.filters.selectedFilters[filterId] = newValue;
+                scope.filters.selectedIds[filterId] = newValue ? 'updated' : filterSelectionNote;
+                if (!newValue) delete scope.filters.selectedFilters[filterId];
+
+                if (newValue && filterSelectionNote==="theoretically-enabled") {
+                    deferredCalls.push(()=> {
+                        // trigger the handler, update other categories, if a category becomes available late
+                        console.log("Delayed enablement of filter ", filterId);
+                        // console.log("=");
+                        newValue.select(filterId, newValue, true, false, true);
+                        // console.log("--");
+                        // console.log("CATS 1", Object.keys(scope.filters.selectedIds));
+                        // console.log("CATS 2", Object.keys(scope.filters.selectedFilters));
+                        // console.log("CATS 3", Object.keys(scope.filters.selectedIds));
+                    });
+                }
+            });
+            deferredCalls.forEach(c => c());
+        }
+
+        // add counts
+        //updateSelectedFilters(filtersIncludingTags);
+
+        // filter and move to new map
+        let result = {};
+        Object.entries(filtersIncludingTags).forEach(([k, f]) => {
+            if (f.countAbsolute > 0) result[k] = f;
+        });
+
+        // and delete categories that are redundant
+        function deleteCategoryIfAllCountsAreEqual(category) {
+            if (_.uniq(Object.values(result).filter(f => f.category === category).map(f => f.countAbsolute)).length==1) {
+                Object.entries(result).filter(([k,f]) => f.category === category).forEach(([k,f])=>delete result[k]);
+            }
+        }
+        function deleteFiltersInCategoryThatAreEmpty(category) {
+            Object.entries(result).filter(([k,f]) => f.category === category && f.countAbsolute==0).forEach(([k,f])=>delete result[k]);
+        }
+        function deleteCategoryIfSize1(category) {
+            const found = Object.entries(result).filter(([k,f]) => f.category === category);
+            if (found.length==1) delete result[found[0][0]];
+        }
+        deleteFiltersInCategoryThatAreEmpty('nested');
+        deleteCategoryIfSize1('nested');
+        deleteCategoryIfAllCountsAreEqual('type/tag');  // because all tags are on all tasks
+
+        if (!result['_cross_entity'] && result['_recursive']) {
+            // if we don't have cross-entity sub-tasks, tidy this message
+            result['_recursive'].display = 'Include sub-tasks';
+        }
+
+        // // but if we deleted everything, restore them (better to have pointless categories than no categories)
+        // if (!Object.keys(result).length) result = filtersIncludingTags;
+
+
+        // now add dividers between categories
+        let lastCat = null;
+        for (let v of Object.values(result)) {
+            if (lastCat!=null && lastCat!=v.category) {
+                v.classes = (v.classes || '') + ' divider-above';
+            }
+            lastCat = v.category;
+        }
+
+        scope.filters.available = result;
+        updateSelectedFilters(result);
         return result;
     }
 }
@@ -186,16 +552,27 @@ function isScheduled(task) {
 
 function isTopLevelTask(t, tasksById) {
     if (!t.submittedByTask) return true;
+    if (t.forceTopLevel) return true;
+    if (t.tags && t.tags.includes("TOP-LEVEL")) return true;
     let submitter = tasksById[t.submittedByTask.metadata.id];
     if (!submitter) return true;
     if (isScheduled(submitter) && (!t.endTimeUtc || t.endTimeUtc<=0)) return true;
     return false;
 }
-
-function topLevelTasks(tasks) {
+function isNonTopLevelTask(t, tasksById) {
+    return !isTopLevelTask(t, tasksById);
+}
+function isCrossEntityTask(t, tasksById) {
+    if (isTopLevelTask(t, tasksById)) return false;
+    return t.submittedByTask.metadata.entityId !== t.entityId;
+}
+function isNestedSameEntityTask(t, tasksById) {
+    if (isTopLevelTask(t, tasksById)) return false;
+    return t.submittedByTask.metadata.entityId === t.entityId;
+}
+function filterWithId(tasks, tasksById, nextFilter) {
     if (!tasks) return tasks;
-    let tasksById = tasks.reduce( (result,t) => { result[t.id] = t; return result; }, {} );
-    return tasks.filter(t => isTopLevelTask(t, tasksById));
+    return tasks.filter(t => nextFilter(t, tasksById));
 }
 
 export function timeAgoFilter() {
@@ -220,7 +597,6 @@ export function dateFilter() {
         } else {
             return moment(input).format('MMM D, yyyy @ HH:mm:ss.SSS');
         }
-        return "TODO - "+input;
     }
 
     return date;
@@ -240,8 +616,8 @@ function isTaskWithTag(task, tag) {
     return task.tags.indexOf(tag)>=0;
 }
 
-function tasksWithTag(tasks, tag) {
-    return tasks.filter(t => isTaskWithTag(t, tag));
+function filterForTasksWithTag(tag) {
+    return (tasks) => tasks.filter(t => isTaskWithTag(t, tag));
 }
 
 function tasksAfterGlobalFilters(inputs, globalFilters) {
@@ -253,23 +629,7 @@ function tasksAfterGlobalFilters(inputs, globalFilters) {
     return inputs;
 }
 
-export function activityTagFilter() {
-    return function (inputs, args) {
-        const [tagF, globalFilters] = args;
-        inputs = tasksAfterGlobalFilters(inputs, globalFilters);
-        if (inputs && tagF) {
-            const filter = tagF.filter || (tagF.tag ? inp => tasksWithTag(inp, tagF.tag) : null);
-            if (!filter) {
-                console.warn("Incomplete activities tag filter", tagF);
-                return inputs;
-            }
-            return filter(inputs);
-        } else {
-            if (inputs) console.warn("Unknown activities tag filter", tagF);
-            return inputs;
-        }
-    }
-}
+function cacheSelectedIdsFromFilters(scope) { scope.filters.selectedIds = { ...scope.filters.selectedFilters }; }
 
 export function activityFilter($filter) {
     return function (activities, searchText) {
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.less b/ui-modules/app-inspector/app/components/task-list/task-list.less
index 614cce11..4c92ad3c 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.less
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.less
@@ -45,6 +45,14 @@ task-list {
 
       .activity-tag-filter {
         flex: 0;
+
+        .selection-summary {
+          background-color: @gray-dark;
+          color: @gray-lighter;
+          padding: 3px 6px;
+          border-radius: 5px;
+        }
+
       }
       .activity-name-filter {
         flex: 1;
@@ -53,40 +61,99 @@ task-list {
   }
 
 
-  .activity-tag-filter-tag {
-    color: #A8B2B9;
-  }
-  .activity-tag-filter-action {
-    color: mix(#A8B2B9, #444);
-  }
-  .activity-tag-filter-tag, .activity-tag-filter-action {
+  //.activity-tag-filter-tag {
+  //  color: #A8B2B9;
+  //}
+  //.activity-tag-filter-action {
+  //  color: mix(#A8B2B9, #444);
+  //}
+  .activity-tag-filter-error, .activity-tag-filter-tag, .activity-tag-filter-action {
     -webkit-font-smoothing: antialiased;
     padding: 5px 10px 5px 10px;
+  }
+  .activity-tag-filter-tag, .activity-tag-filter-action {
     cursor: pointer;
     transition: color 0.5s;
 
     &:hover {
-      color: #7B8C98;
+      //color: #7B8C98;
       background-color: @dropdown-link-hover-bg;
+
       .badge {
-        background-color: #7B8C98;
+        //background-color: #7B8C98;
       }
     }
+
     &.active {
-      color: #3B558A;
+      //color: #3B558A;
       background-color: @dropdown-link-active-bg;
+
       .badge {
-        background-color: #3B558A;
+//        background-color: #3B558A;
       }
+
       &:hover {
-        color: #3B558A;
+        //color: #3B558A;
       }
     }
+
+    .main {
+      margin-right: 1em;
+    }
     .badge {
-      color: #3b558a;
-      color: white;
+      //color: #3b558a;
+      //color: white;
       background-color: #e6e6e6;
-      background-color: #A8B2B9;;
+      //background-color: #A8B2B9;
+
+      margin-left: 1ex;
+      float: right;
+
+      &.included {
+        background-color: @brand-primary;
+      }
+      &.more-excluded-elsewhere, &.more-excluded-elsewhere {
+        background-color: @gray-lighter;
+      }
+      &.excluded-here {
+        background-color: @primary-50;
+      }
+    }
+  }
+
+  .dropdown-menu.with-checks {
+    width: auto;
+
+    li {
+      padding-left: 2em;
+      &.divider-above {
+        border-top: 1px solid @gray-lighter;
+        margin-top: 6px;
+        padding-top: 10px;
+      }
+    }
+    .selected {
+      .check.if-selected {
+        margin-left: -1.5em;
+        display: block;
+        width: 0;
+        height: 0;
+        overflow: visible;
+        margin-top: 3px;
+        margin-bottom: -3px;
+      }
+      .included, .more-excluded-elsewhere {
+        display: block;
+      }
+      .excluded-here {
+        display: none;
+      }
+    }
+    .included, .more-excluded-elsewhere {
+      display: none;
+    }
+    .check.if-selected {
+      display: none;
     }
   }
 }
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.template.html b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
index 2db6f8ba..54ce89f1 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.template.html
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
@@ -19,16 +19,45 @@
 <div class="no-activities" ng-if="tasks.length === 0">No activities</div>
 <div ng-if="tasks.length !== 0" class="task-list">
     <div class="form-group search-bar-with-controls">
-        <div class="btn-group activity-tag-filter" uib-dropdown keyboard-nav="true" dropdown-append-to="model.appendTo">
+        <div class="btn-group activity-tag-filter" uib-dropdown keyboard-nav="true" dropdown-append-to="model.appendTo" ng-if="filters.selectedDisplayName">
             <button id="single-button" type="button" class="btn btn-default" uib-dropdown-toggle>
-                Displaying <kbd>{{model.filterByTag.display}}</kbd> <span class="caret"></span>
+                Show <span class="selection-summary">{{filters.selectedDisplayName}}</span> <span class="caret"></span>
             </button>
-            <ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="single-button">
-                <li role="menuitem" class="activity-tag-filter-tag" ng-repeat="(tag,value) in filters track by tag" ng-model="model.filterByTag" uib-btn-radio="value">
-                    <span>{{value.display}}</span> <span class="badge">{{value.count}}</span>
+            <ul class="dropdown-menu with-checks" uib-dropdown-menu role="menu" aria-labelledby="single-button">
+                <li role="menuitem" ng-repeat="(tag,value) in filters.available track by tag"
+                        class="activity-tag-filter-tag {{value.classes}}"
+                        ng-click="value.onClick(tag, value)"
+                        ng-class="{'selected': filters.selectedFilters[tag]}">
+
+                    <i class="fa fa-check check if-selected"></i>
+
+                    <span class="main" title="{{value.help}}">{{value.display}}</span>
+
+                    <span class="badge included"
+                          title="Activities included by this filter">
+                        {{value.count}}
+                    </span>
+                    <span class="badge excluded-here"
+                          ng-if="value.count > 0"
+                          title="Activities included by this filter">
+                        {{value.count}}
+                    </span>
+                    <span class="badge more-excluded-elsewhere"
+                          title="Additional activities excluded by other filter categories"
+                          ng-if="value.count > 0 && value.countAbsolute > value.count">
+                        {{ value.countAbsolute - value.count}}
+                    </span>
+                    <span class="badge all-excluded-elsewhere"
+                          title="Activities are excluded by other filter categories"
+                          ng-if="value.count == 0 && value.countAbsolute > 0">
+                        {{ value.countAbsolute - value.count}}
+                    </span>
                 </li>
                 <li role="menuitem" class="activity-tag-filter-action" ng-if="globalFilters.transient" ng-click="globalFilters.transient.action()">
-                    <i><span>{{globalFilters.transient.display}}</span></i>
+                    <i><span class="main">{{globalFilters.transient.display}}</span></i>
+                </li>
+                <li role="menuitem" class="activity-tag-filter-error" ng-if="!globalFilters.transient && isEmpty(filters.available)">
+                    <i><span class="main">No filter options</span></i>
                 </li>
             </ul>
         </div>
@@ -55,7 +84,7 @@
             </tr>
             </thead>
             <tbody>
-            <tr ng-repeat="task in tasks | activityTagFilter : [model.filterByTag, globalFilters] | activityFilter:filterValue as filterResult track by task.id">
+            <tr ng-repeat="task in tasksFilteredByTag | activityFilter:filterValue as filterResult track by task.id">
                 <td class="status">
                     <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">
                     <brooklyn-status-icon value="{{task.currentStatus}}" ng-if="!isScheduled(task)"></brooklyn-status-icon>
@@ -65,6 +94,9 @@
                 <td class="name">
                     <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">{{task.displayName}}
                     </a>
+                    {{ task.id }} -
+                    prevRun={{ task.isWorkflowPreviousRun }}
+                    replLatest={{ task.isReplayedWorkflowLatest }}
                 </td>
                 <td class="started">
                     <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">
@@ -73,7 +105,7 @@
                 </td>
                 <td class="duration">
                     <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})"
-                        	ng-if="task.startTimeUtc">
+                        	ng-if="task.startTimeUtc && task.startTimeUtc>0">
                         {{getTaskDuration(task) | durationFilter}} <span ng-if="task.endTimeUtc === null">and counting</span>
                     </a>
                 </td>
@@ -83,7 +115,7 @@
                 <td colspan="4" class="text-center"><h4>
                     No tasks found matching
                     <span ng-if="filterValue">current search <code>{{filterValue}}</code> and</span>
-                    filter <code>{{model.filterByTag.display}}</code>
+                    filter <code>{{filters.selectedDisplayName}}</code>
                 </h4></td>
             </tr>
             </tbody>
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 30b8b8cf..7d6bb40c 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
@@ -64,6 +64,12 @@ export function workflowStepDirective() {
             vm.nonEmpty = (data) => data && (data.length || Object.keys(data).length);
             vm.isNullish = _.isNil;
 
+            vm.getWorkflowNameFromReference = (ref) => {
+                // would be nice to get a name, but all we have is appId, entityId, workflowId; and no lookup table;
+                // could look it up or store at server, but seems like overkill
+                return null;
+            };
+
             $scope.json = null;
             $scope.jsonMode = null;
             vm.showJson = (mode, json) => {
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 5f0b78b4..57cb6d71 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
@@ -166,7 +166,7 @@
                         </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>
+                            <div class="btn-group" uib-dropdown ng-if="stepContext.subWorkflows.length>1">
                                 <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>
@@ -174,9 +174,16 @@
                                     <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>{{ 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})">
+                                    <span>{{ vm.getWorkflowNameFromReference(stepContext.subWorkflows[0]) }}</span>
+                                    <span class="monospace">{{ stepContext.subWorkflows[0].workflowId }}</span>
+                                </a>
+                            </div>
                         </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>
@@ -194,7 +201,7 @@
 
                 <div class="btn-group right" uib-dropdown>
                     <button id="extra-data-button" type="button" class="btn btn-select-dropdown pull-right" uib-dropdown-toggle>
-                        View data <span class="caret"></span>
+                        JSON <span class="caret"></span>
                     </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'}">
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index 3e561ee8..96c15948 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -71,24 +71,37 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                 newActivitiesMap[activity.id] = activity;
             });
 
-            // TODO
-            //(vm.workflows || [])
             Object.values(vm.workflows || {})
+                .filter(wf => wf.replays && wf.replays.length)
                 .forEach(wf => {
-                (wf.replays || []).forEach(wft => {
-                    let newActivity = newActivitiesMap[wft.taskId];
-                    if (!newActivity) {
-                        // create stub tasks for the replays of workflows
-                        newActivity = makeTaskStubFromWorkflowRecord(wf, wft);
-                        newActivitiesMap[wft.taskId] = newActivity;
-                    }
-                    newActivity.workflowId = wft.workflowId;
-                    newActivity.isWorkflowOldReplay = wft.workflowId !== wft.taskId;
+                    const last = wf.replays[wf.replays.length-1];
+                    let submitted = {};
+                    let lastTask;
+
+                    wf.replays.forEach(wft => {
+                        let t = newActivitiesMap[wft.taskId];
+                        if (!t) {
+                            // create stub tasks for the replays of workflows
+                            t = makeTaskStubFromWorkflowRecord(wf, wft);
+                            newActivitiesMap[wft.taskId] = t;
+                        }
+                        t.workflowId = wft.workflowId;
+
+                        // overriding submitters breaks things (infinite loop, in kilt?)
+                        // so instead just set whether it is the latest replay
+                        t.isWorkflowPreviousRun = last && wft.taskId !== last.taskId;
+                        lastTask = t;
+                    });
+                    if (wf.replays.length>=2) lastTask.isReplayedWorkflowLatest = true;
                 });
-            });
 
             vm.activitiesMap = newActivitiesMap;
             vm.activities = Object.values(vm.activitiesMap);
+            // TODO weird bug
+            // vm.activitiesUniq = _.uniq(Object.values(vm.activitiesMap));
+            // if (vm.activities.length != Object.values(vm.activitiesMap).length) {
+            //     console.log("MISMATCH", vm.activitiesMap);
+            // }
         }
     }
 
@@ -107,7 +120,18 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
             $log.warn('Error loading activities for entity '+entityId, error);
             vm.error = 'Cannot load activities for entity with ID: ' + entityId;
         });
-        
+
+        entityApi.getWorkflows(applicationId, entityId).then((response) => {
+            vm.workflows = response.data;
+            mergeActivities();
+            observers.push(response.subscribe((response) => {
+                vm.workflows = response.data;
+                mergeActivities();
+            }));
+        }).catch((error) => {
+            $log.warn('Error loading workflows for entity ' + entityId, error);
+        });
+
         entityApi.entityActivitiesDeep(applicationId, entityId).then((response) => {
             vm.activitiesDeep = response.data;
             observers.push(response.subscribe((response) => {
@@ -115,22 +139,10 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                 vm.error = undefined;
             }));
         }).catch((error) => {
-            $log.warn('Error loading activity children deep for entity '+entityId, error);
+            $log.warn('Error loading activity children deep for entity ' + entityId, error);
             vm.error = 'Cannot load activities (deep) for entity with ID: ' + entityId;
         });
 
-        entityApi.getWorkflows(applicationId, entityId).then((response) => {
-          vm.workflows = response.data;
-          mergeActivities();
-          observers.push(response.subscribe((response) => {
-              vm.workflows = response.data;
-              mergeActivities();
-          }));
-        }).catch((error) => {
-          $log.warn('Error loading workflows for entity '+entityId, error);
-        });
-
-
         $scope.$on('$destroy', () => {
           observers.forEach((observer) => {
               observer.unsubscribe();
@@ -146,7 +158,7 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
 }
 
 export function makeTaskStubFromWorkflowRecord(wf, wft) {
-    return {
+    const result = {
         id: wft.taskId,
         displayName: wf.name + (wft.reasonForReplay ? " ("+wft.reasonForReplay+")" : ""),
         entityId: (wf.entity || {}).id,
@@ -155,6 +167,7 @@ export function makeTaskStubFromWorkflowRecord(wf, wft) {
         submitTimeUtc: wft.submitTimeUtc,
         startTimeUtc: wft.startTimeUtc,
         endTimeUtc: wft.endTimeUtc,
+        isTaskStubFromWorkflowRecord: true,
         tags: [
             "WORKFLOW",
             {
@@ -164,4 +177,5 @@ export function makeTaskStubFromWorkflowRecord(wf, wft) {
             },
         ],
     };
+    return result;
 };
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 a106bc04..de1fa975 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
@@ -155,8 +155,7 @@ 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
-
-            loadWorkflow(null).then(()=> {
+            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);
@@ -167,13 +166,18 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
 
                 // 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 || 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.') );
+            }
 
-            }).catch(error2 => {
-                $log.debug("ID "+activityId+" does not correspond to workflow either", error2);
-            });
+            loadWorkflow({workflowId: activityId}).then(onNonTaskWorkflowLoad)
+                .catch(error => {
+                    loadWorkflow(null).then(onNonTaskWorkflowLoad)
+                        .catch(error => {
+                            $log.debug("ID "+activityId+"/"+$scope.workflowId+" does not correspond to workflow either", error);
+                        });
+                });
         });
 
         activityApi.activityChildren(activityId).then((response)=> {
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 38eb4b34..0c01da1d 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
@@ -204,7 +204,7 @@
             }
 
             .selected {
-                .check {
+                .check.if-selected {
                     margin-left: -1.5em;
                     display: block;
                     width: 0;
@@ -214,8 +214,7 @@
                     margin-bottom: -3px;
                 }
             }
-
-            .check {
+            .check.if-selected {
                 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 c96f9b75..dd8d6115 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
@@ -101,14 +101,14 @@
                         </div>
                         <div class="summary-block" ng-mouseenter="showUTC=true" ng-mouseleave="showUTC=false">
                             <div class="row">
-                                <div ng-if="vm.model.activity.endTimeUtc" class="col-md-3 summary-item summary-item-timestamp">
+                                <div ng-if="vm.model.activity.endTimeUtc && vm.model.activity.endTimeUtc>0" class="col-md-3 summary-item summary-item-timestamp">
                                     <div class="summary-item-icon">
                                         <div class="icon-stopwatch"></div>
                                     </div>
                                     <div class="summary-item-label">Duration</div>
                                     <div class="summary-item-value">
                                         <div class="humanized fade" ng-show="!showUTC">
-                                            took {{vm.model.activity.endTimeUtc- vm.model.activity.startTimeUtc | durationFilter}}
+                                            took {{vm.model.activity.endTimeUtc - vm.model.activity.startTimeUtc | durationFilter}}
                                         </div>
                                         <div class="utcTime fade" ng-show="showUTC">
                                             {{vm.model.activity.endTimeUtc- vm.model.activity.startTimeUtc }} ms
@@ -194,7 +194,7 @@
                                         <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 }}">
                                                 <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"></i>
+                                                    <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
@@ -202,7 +202,10 @@
 
                                                 </a> </li>
                                             <li role="menuitem">
-                                                <a href="" ng-click="vm.showReplayHelp()" ng-class="{'selected' : showReplayHelp}"><i>More information</i></a>
+                                                <a href="" ng-click="vm.showReplayHelp()" ng-class="{'selected' : showReplayHelp}">
+                                                    <i class="fa fa-check check if-selected"></i>
+                                                    <i>More information</i>
+                                                </a>
                                             </li>
                                         </ul>
                                     </div>


[brooklyn-ui] 03/24: exact copy of uib dropdown.js, to allow for nested dropdowns

Posted by he...@apache.org.
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 17bc3ee4e134a9a1ca0c27e940bf70fcbbedaf3f
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Wed Oct 5 13:54:31 2022 +0100

    exact copy of uib dropdown.js, to allow for nested dropdowns
---
 .../inspect/activities/detail/dropdown-nested.js   | 440 +++++++++++++++++++++
 1 file changed, 440 insertions(+)

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
new file mode 100644
index 00000000..cbfe420f
--- /dev/null
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js
@@ -0,0 +1,440 @@
+angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
+
+    .constant('uibDropdownConfig', {
+        appendToOpenClass: 'uib-dropdown-open',
+        openClass: 'open'
+    })
+
+    .service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
+        var openScope = null;
+        var openedContainers = $$multiMap.createNew();
+
+        this.isOnlyOpen = function(dropdownScope, appendTo) {
+            var openedDropdowns = openedContainers.get(appendTo);
+            if (openedDropdowns) {
+                var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) {
+                    if (dropdown.scope === dropdownScope) {
+                        return dropdown;
+                    }
+
+                    return toClose;
+                }, {});
+                if (openDropdown) {
+                    return openedDropdowns.length === 1;
+                }
+            }
+
+            return false;
+        };
+
+        this.open = function(dropdownScope, element, appendTo) {
+            if (!openScope) {
+                $document.on('click', closeDropdown);
+            }
+
+            if (openScope && openScope !== dropdownScope) {
+                openScope.isOpen = false;
+            }
+
+            openScope = dropdownScope;
+
+            if (!appendTo) {
+                return;
+            }
+
+            var openedDropdowns = openedContainers.get(appendTo);
+            if (openedDropdowns) {
+                var openedScopes = openedDropdowns.map(function(dropdown) {
+                    return dropdown.scope;
+                });
+                if (openedScopes.indexOf(dropdownScope) === -1) {
+                    openedContainers.put(appendTo, {
+                        scope: dropdownScope
+                    });
+                }
+            } else {
+                openedContainers.put(appendTo, {
+                    scope: dropdownScope
+                });
+            }
+        };
+
+        this.close = function(dropdownScope, element, appendTo) {
+            if (openScope === dropdownScope) {
+                $document.off('click', closeDropdown);
+                $document.off('keydown', this.keybindFilter);
+                openScope = null;
+            }
+
+            if (!appendTo) {
+                return;
+            }
+
+            var openedDropdowns = openedContainers.get(appendTo);
+            if (openedDropdowns) {
+                var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) {
+                    if (dropdown.scope === dropdownScope) {
+                        return dropdown;
+                    }
+
+                    return toClose;
+                }, {});
+                if (dropdownToClose) {
+                    openedContainers.remove(appendTo, dropdownToClose);
+                }
+            }
+        };
+
+        var closeDropdown = function(evt) {
+            // This method may still be called during the same mouse event that
+            // unbound this event handler. So check openScope before proceeding.
+            if (!openScope || !openScope.isOpen) { return; }
+
+            if (evt && openScope.getAutoClose() === 'disabled') { return; }
+
+            if (evt && evt.which === 3) { return; }
+
+            var toggleElement = openScope.getToggleElement();
+            if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
+                return;
+            }
+
+            var dropdownElement = openScope.getDropdownElement();
+            if (evt && openScope.getAutoClose() === 'outsideClick' &&
+                dropdownElement && dropdownElement[0].contains(evt.target)) {
+                return;
+            }
+
+            openScope.focusToggleElement();
+            openScope.isOpen = false;
+
+            if (!$rootScope.$$phase) {
+                openScope.$apply();
+            }
+        };
+
+        this.keybindFilter = function(evt) {
+            if (!openScope) {
+                // see this.close as ESC could have been pressed which kills the scope so we can not proceed
+                return;
+            }
+
+            var dropdownElement = openScope.getDropdownElement();
+            var toggleElement = openScope.getToggleElement();
+            var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target);
+            var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target);
+            if (evt.which === 27) {
+                evt.stopPropagation();
+                openScope.focusToggleElement();
+                closeDropdown();
+            } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) {
+                evt.preventDefault();
+                evt.stopPropagation();
+                openScope.focusDropdownEntry(evt.which);
+            }
+        };
+    }])
+
+    .controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) {
+        var self = this,
+            scope = $scope.$new(), // create a child scope so we are not polluting original one
+            templateScope,
+            appendToOpenClass = dropdownConfig.appendToOpenClass,
+            openClass = dropdownConfig.openClass,
+            getIsOpen,
+            setIsOpen = angular.noop,
+            toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
+            keynavEnabled = false,
+            selectedOption = null,
+            body = $document.find('body');
+
+        $element.addClass('dropdown');
+
+        this.init = function() {
+            if ($attrs.isOpen) {
+                getIsOpen = $parse($attrs.isOpen);
+                setIsOpen = getIsOpen.assign;
+
+                $scope.$watch(getIsOpen, function(value) {
+                    scope.isOpen = !!value;
+                });
+            }
+
+            keynavEnabled = angular.isDefined($attrs.keyboardNav);
+        };
+
+        this.toggle = function(open) {
+            scope.isOpen = arguments.length ? !!open : !scope.isOpen;
+            if (angular.isFunction(setIsOpen)) {
+                setIsOpen(scope, scope.isOpen);
+            }
+
+            return scope.isOpen;
+        };
+
+        // Allow other directives to watch status
+        this.isOpen = function() {
+            return scope.isOpen;
+        };
+
+        scope.getToggleElement = function() {
+            return self.toggleElement;
+        };
+
+        scope.getAutoClose = function() {
+            return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
+        };
+
+        scope.getElement = function() {
+            return $element;
+        };
+
+        scope.isKeynavEnabled = function() {
+            return keynavEnabled;
+        };
+
+        scope.focusDropdownEntry = function(keyCode) {
+            var elems = self.dropdownMenu ? //If append to body is used.
+                angular.element(self.dropdownMenu).find('a') :
+                $element.find('ul').eq(0).find('a');
+
+            switch (keyCode) {
+                case 40: {
+                    if (!angular.isNumber(self.selectedOption)) {
+                        self.selectedOption = 0;
+                    } else {
+                        self.selectedOption = self.selectedOption === elems.length - 1 ?
+                            self.selectedOption :
+                            self.selectedOption + 1;
+                    }
+                    break;
+                }
+                case 38: {
+                    if (!angular.isNumber(self.selectedOption)) {
+                        self.selectedOption = elems.length - 1;
+                    } else {
+                        self.selectedOption = self.selectedOption === 0 ?
+                            0 : self.selectedOption - 1;
+                    }
+                    break;
+                }
+            }
+            elems[self.selectedOption].focus();
+        };
+
+        scope.getDropdownElement = function() {
+            return self.dropdownMenu;
+        };
+
+        scope.focusToggleElement = function() {
+            if (self.toggleElement) {
+                self.toggleElement[0].focus();
+            }
+        };
+
+        function removeDropdownMenu() {
+            $element.append(self.dropdownMenu);
+        }
+
+        scope.$watch('isOpen', function(isOpen, wasOpen) {
+            var appendTo = null,
+                appendToBody = false;
+
+            if (angular.isDefined($attrs.dropdownAppendTo)) {
+                var appendToEl = $parse($attrs.dropdownAppendTo)(scope);
+                if (appendToEl) {
+                    appendTo = angular.element(appendToEl);
+                }
+            }
+
+            if (angular.isDefined($attrs.dropdownAppendToBody)) {
+                var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope);
+                if (appendToBodyValue !== false) {
+                    appendToBody = true;
+                }
+            }
+
+            if (appendToBody && !appendTo) {
+                appendTo = body;
+            }
+
+            if (appendTo && self.dropdownMenu) {
+                if (isOpen) {
+                    appendTo.append(self.dropdownMenu);
+                    $element.on('$destroy', removeDropdownMenu);
+                } else {
+                    $element.off('$destroy', removeDropdownMenu);
+                    removeDropdownMenu();
+                }
+            }
+
+            if (appendTo && self.dropdownMenu) {
+                var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true),
+                    css,
+                    rightalign,
+                    scrollbarPadding,
+                    scrollbarWidth = 0;
+
+                css = {
+                    top: pos.top + 'px',
+                    display: isOpen ? 'block' : 'none'
+                };
+
+                rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
+                if (!rightalign) {
+                    css.left = pos.left + 'px';
+                    css.right = 'auto';
+                } else {
+                    css.left = 'auto';
+                    scrollbarPadding = $position.scrollbarPadding(appendTo);
+
+                    if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
+                        scrollbarWidth = scrollbarPadding.scrollbarWidth;
+                    }
+
+                    css.right = window.innerWidth - scrollbarWidth -
+                        (pos.left + $element.prop('offsetWidth')) + 'px';
+                }
+
+                // Need to adjust our positioning to be relative to the appendTo container
+                // if it's not the body element
+                if (!appendToBody) {
+                    var appendOffset = $position.offset(appendTo);
+
+                    css.top = pos.top - appendOffset.top + 'px';
+
+                    if (!rightalign) {
+                        css.left = pos.left - appendOffset.left + 'px';
+                    } else {
+                        css.right = window.innerWidth -
+                            (pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px';
+                    }
+                }
+
+                self.dropdownMenu.css(css);
+            }
+
+            var openContainer = appendTo ? appendTo : $element;
+            var dropdownOpenClass = appendTo ? appendToOpenClass : openClass;
+            var hasOpenClass = openContainer.hasClass(dropdownOpenClass);
+            var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo);
+
+            if (hasOpenClass === !isOpen) {
+                var toggleClass;
+                if (appendTo) {
+                    toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass';
+                } else {
+                    toggleClass = isOpen ? 'addClass' : 'removeClass';
+                }
+                $animate[toggleClass](openContainer, dropdownOpenClass).then(function() {
+                    if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
+                        toggleInvoker($scope, { open: !!isOpen });
+                    }
+                });
+            }
+
+            if (isOpen) {
+                if (self.dropdownMenuTemplateUrl) {
+                    $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
+                        templateScope = scope.$new();
+                        $compile(tplContent.trim())(templateScope, function(dropdownElement) {
+                            var newEl = dropdownElement;
+                            self.dropdownMenu.replaceWith(newEl);
+                            self.dropdownMenu = newEl;
+                            $document.on('keydown', uibDropdownService.keybindFilter);
+                        });
+                    });
+                } else {
+                    $document.on('keydown', uibDropdownService.keybindFilter);
+                }
+
+                scope.focusToggleElement();
+                uibDropdownService.open(scope, $element, appendTo);
+            } else {
+                uibDropdownService.close(scope, $element, appendTo);
+                if (self.dropdownMenuTemplateUrl) {
+                    if (templateScope) {
+                        templateScope.$destroy();
+                    }
+                    var newEl = angular.element('<ul class="dropdown-menu"></ul>');
+                    self.dropdownMenu.replaceWith(newEl);
+                    self.dropdownMenu = newEl;
+                }
+
+                self.selectedOption = null;
+            }
+
+            if (angular.isFunction(setIsOpen)) {
+                setIsOpen($scope, isOpen);
+            }
+        });
+    }])
+
+    .directive('uibDropdown', function() {
+        return {
+            controller: 'UibDropdownController',
+            link: function(scope, element, attrs, dropdownCtrl) {
+                dropdownCtrl.init();
+            }
+        };
+    })
+
+    .directive('uibDropdownMenu', function() {
+        return {
+            restrict: 'A',
+            require: '?^uibDropdown',
+            link: function(scope, element, attrs, dropdownCtrl) {
+                if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) {
+                    return;
+                }
+
+                element.addClass('dropdown-menu');
+
+                var tplUrl = attrs.templateUrl;
+                if (tplUrl) {
+                    dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
+                }
+
+                if (!dropdownCtrl.dropdownMenu) {
+                    dropdownCtrl.dropdownMenu = element;
+                }
+            }
+        };
+    })
+
+    .directive('uibDropdownToggle', function() {
+        return {
+            require: '?^uibDropdown',
+            link: function(scope, element, attrs, dropdownCtrl) {
+                if (!dropdownCtrl) {
+                    return;
+                }
+
+                element.addClass('dropdown-toggle');
+
+                dropdownCtrl.toggleElement = element;
+
+                var toggleDropdown = function(event) {
+                    event.preventDefault();
+
+                    if (!element.hasClass('disabled') && !attrs.disabled) {
+                        scope.$apply(function() {
+                            dropdownCtrl.toggle();
+                        });
+                    }
+                };
+
+                element.on('click', toggleDropdown);
+
+                // WAI-ARIA
+                element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
+                scope.$watch(dropdownCtrl.isOpen, function(isOpen) {
+                    element.attr('aria-expanded', !!isOpen);
+                });
+
+                scope.$on('$destroy', function() {
+                    element.off('click', toggleDropdown);
+                });
+            }
+        };
+    });


[brooklyn-ui] 01/24: ui for workflow on activities detail page

Posted by he...@apache.org.
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 da317de996accffe0af7e3b49f477b0fded6ec24
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Mon Oct 3 18:39:05 2022 +0100

    ui for workflow on activities detail page
    
    shows list of steps with arrows, details, references to tasks
---
 .../entity-effector/entity-effector.less           |   8 +-
 .../components/providers/entity-api.provider.js    |  14 +-
 .../components/task-list/task-list.directive.js    |   1 -
 .../components/workflow/workflow-step.directive.js | 179 +++++++++++
 .../workflow/workflow-step.template.html           | 194 +++++++++++
 .../workflow/workflow-steps.directive.js           | 354 +++++++++++++++++++++
 .../app/components/workflow/workflow-steps.less    | 245 ++++++++++++++
 .../workflow/workflow-steps.template.html          |  35 ++
 ui-modules/app-inspector/app/index.js              |   3 +
 ui-modules/app-inspector/app/index.less            |   1 +
 .../inspect/activities/detail/detail.controller.js |  32 +-
 .../main/inspect/activities/detail/detail.less     |   2 -
 .../inspect/activities/detail/detail.template.html |  26 +-
 ui-modules/shared/style/first.less                 |   5 +
 14 files changed, 1087 insertions(+), 12 deletions(-)

diff --git a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
index 88872833..dd6eaf97 100644
--- a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
+++ b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
@@ -83,10 +83,10 @@
                     border-top-right-radius: 12px;
                 }
             }
-            .effector-succeeded { color: #363; }
-            .effector-failed { color: #820; }
-            .effector-cancelled { color: #660; }
-            .effector-active { color: #6a2; }
+            .effector-succeeded { color: @color-succeeded; }
+            .effector-failed { color: @color-failed; }
+            .effector-cancelled { color: @color-cancelled; }
+            .effector-active { color: @color-active; }
         }
     }
 }
\ No newline at end of file
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 f8d86e23..8ec4e2bd 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
@@ -70,8 +70,10 @@ function EntityApi($http, $q) {
         startEntityAdjunct: startEntityAdjunct,
         stopEntityAdjunct: stopEntityAdjunct,
         destroyEntityAdjunct: destroyEntityAdjunct,
-        updateEntityAdjunctConfig: updateEntityAdjunctConfig
-        
+        updateEntityAdjunctConfig: updateEntityAdjunctConfig,
+
+        getWorkflows: getWorkflows,
+        getWorkflow: getWorkflow,
     };
 
     function getEntity(applicationId, entityId) {
@@ -187,5 +189,11 @@ function EntityApi($http, $q) {
     }
     function updateEntityAdjunctConfig(applicationId, entityId, adjunctId, configId, data) {
         return $http.post('/v1/applications/'+ applicationId +'/entities/' + entityId + '/adjuncts/' + adjunctId + '/config/' + configId, data);
-    }    
+    }
+    function getWorkflows(applicationId, entityId) {
+        return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflows/', {observable: true, ignoreLoadingBar: true});
+    }
+    function getWorkflow(applicationId, entityId, workflowId) {
+        return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflow/' + workflowId, {observable: true, ignoreLoadingBar: true});
+    }
 }
\ 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 457d0268..8f9ac4e3 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
@@ -190,7 +190,6 @@ function topLevelTasks(tasks) {
     return tasks.filter(t => isTopLevelTask(t, tasksById));
 }
 
-
 export function timeAgoFilter() {
     function timeAgo(input) {
         return fromNow(input);
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
new file mode 100644
index 00000000..9a322297
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import template from "./workflow-step.template.html";
+import angular from "angular";
+import jsyaml from 'js-yaml';
+
+const MODULE_NAME = 'inspector.workflow-step';
+
+angular.module(MODULE_NAME, [])
+    .directive('workflowStep', workflowStepDirective);
+
+export default MODULE_NAME;
+
+let count = 0;
+
+export function workflowStepDirective() {
+    return {
+        template: template,
+        restrict: 'E',
+        scope: {
+            workflow: '=',
+            task: '=?',
+            step: '<',  // definition
+            stepIndex: '<',
+            expanded: '=',
+            onSizeChange: '=',
+        },
+        controller: ['$sce', '$scope', controller],
+        controllerAs: 'vm',
+    };
+
+    function controller($sce, $scope) {
+        try {
+            let vm = this;
+
+            let step = $scope.step;
+            let index = $scope.stepIndex;
+
+            vm.stepDetails = () => stepDetails($sce, $scope.workflow, step, index, $scope.expanded);
+            vm.toggleExpandState = () => {
+                $scope.expanded = !$scope.expanded;
+                if ($scope.onSizeChange) $scope.onSizeChange();
+            }
+            vm.stringify = stringify;
+            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);
+
+            $scope.json = null;
+            $scope.jsonMode = null;
+            vm.showJson = (mode, json) => {
+                $scope.jsonMode = mode;
+                $scope.json = json ? stringify(json) : null;
+            }
+
+            if (typeof step === 'string') {
+                $scope.stepPrefixClass = 'step-index';
+                $scope.stepPrefix = index + 1;
+                $scope.stepTitleDetail = step;
+            } else {
+
+                let shorthand = step.userSuppliedShorthand || step.s || step.shorthand;
+                $scope.stepTitleDetail = shorthand;
+                if (step.name) {
+                    $scope.stepPrefixClass = 'step-name';
+                    $scope.stepPrefix = step.name;
+                } else {
+                    if (step.id) {
+                        $scope.stepPrefixClass = 'step-id';
+                        $scope.stepPrefix = step.id;
+                    } else {
+                        $scope.stepPrefixClass = 'step-index';
+                        $scope.stepPrefix = index + 1;
+
+                        if (!shorthand) {
+                            $scope.stepTitleDetail = step.type || '';
+                            if (step.input) $scope.stepTitleDetail += ' ...';
+                        }
+                    }
+                }
+            }
+
+            function updateData() {
+                let workflow = $scope.workflow;
+                workflow.data = workflow.data || {};
+                $scope.workflowStepClasses = [];
+                if (workflow.data.currentStepIndex === index) $scope.workflowStepClasses.push('current-step');
+
+                $scope.isCurrent = (workflow.data.currentStepIndex === index);
+                $scope.isRunning = (workflow.data.status === 'RUNNING');
+                $scope.isWorkflowError = (workflow.data.status && workflow.data.status.startsWith('ERROR'));
+                $scope.osi = workflow.data.oldStepInfo[index] || {};
+                $scope.stepContext = ($scope.isCurrent ? workflow.data.currentStepInstance : $scope.osi.context) || {};
+
+                $scope.isFocusStep = $scope.workflow.tag && ($scope.workflow.tag.stepIndex === index);
+                $scope.isFocusTask = false;
+
+                if ($scope.task) {
+                    if ($scope.stepContext.taskId === $scope.task.id) {
+                        $scope.isFocusTask = true;
+
+                    } else if ($scope.isFocusStep) {
+                        // TODO other instance of this tag selected
+                    }
+                }
+            }
+            $scope.$watch('workflow', updateData);
+            updateData();
+
+        } catch (error) {
+            console.log("error showing workflow step", error);
+            // the ng-repeat seems to swallow and mask any error in the above - can't understand why! but log it here in case something breaks.
+            throw error;
+        }
+    }
+
+}
+
+function stepDetails($sce, workflow, step, index, expanded) {
+    let v;
+    if (typeof step === 'string') {
+        v = '<span class="step-index">'+_.escape(index+1)+'</span> ';
+        v += ' <span class="step-body">' + _.escape(step) + '</span>';
+    } else {
+        let shorthand = step.userSuppliedShorthand || step.s || step.shorthand;
+        if (step.name) {
+            v = '<span class="step-name">' + _.escape(step.name) + '</span>';
+            if (shorthand) {
+                v += ' <span class="step-body">' + _.escape(shorthand) + '</span>';
+            }
+        } else {
+            if (step.id) {
+                v = '<span class="step-id">' + _.escape(step.id) + '</span>';
+                if (shorthand) {
+                    v += ' <span class="step-body">' + _.escape(shorthand) + '</span>';
+                }
+            } else {
+                v = '<span class="step-index">'+_.escape(index+1)+'</span> ';
+                if (shorthand) {
+                    v += '<span class="step-body">' + _.escape(shorthand);
+                } else {
+                    v += _.escape(step.type);
+                    if (step.input) v += ' ...';
+                }
+                v += '</span>';
+            }
+        }
+    }
+    v = '<div class="step-block-title">'+v+'</div>';
+
+    if (expanded) {
+        v += '<br/>';
+        const oldStepInfo = (workflow.data.oldStepInfo || {})[index]
+        if (oldStepInfo) {
+            v += '<pre>' + _.escape(stringify(oldStepInfo)) + '</pre>';
+        } else {
+            v += _.escape("Step has not been run yet.");
+        }
+    }
+    return $sce.trustAsHtml(v);
+}
+
+function stringify(data) { return JSON.stringify(data, null, 2); }
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
new file mode 100644
index 00000000..b3ac63c9
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html
@@ -0,0 +1,194 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<div class="workflow-step-outer">
+
+    <div class="workflow-step-status-indicators">
+        <span ng-if="isCurrent">
+            <span ng-if="isRunning" class="running-status">
+                <brooklyn-status-icon value="STARTING"></brooklyn-status-icon>
+            </span>
+        </span>
+
+        <span ng-if="osi.countCompleted && osi.countStarted === osi.countStarted">
+            <span class="color-succeeded">
+                <i class="fa fa-check-circle"></i>
+            </span>
+<!--            <span ng-if="osi.countCompleted > 1">{{ osi.countCompleted }}</span>-->
+        </span>
+        <span ng-if="osi.countStarted && osi.countStarted != osi.countCompleted && !(isCurrent && isRunning)">
+            <span class="color-failed" ng-if="isWorkflowError">
+                <i class="fa fa-times-circle"></i>
+            </span>
+            <span class="color-cancelled" ng-if="!isWorkflowError">
+                <i class="fa fa-exclamation-circle"></i>
+            </span>
+        </span>
+    </div>
+
+    <div class="workflow-step" id="workflow-step-{{stepIindex}}" ng-class="vm.getWorkflowStepClasses(stepIndex)">
+        <div class="rhs-icons">
+            <div ng-if="isFocusTask" class="workflow-step-pill focus-step">
+                selected
+            </div>
+            <div ng-click="vm.toggleExpandState()" class="expand-toggle">
+                <i ng-class="expanded ? 'fa fa-chevron-up' : 'fa fa-chevron-down'"></i>
+            </div>
+        </div>
+
+        <div class="step-block-title">
+            <span ng-class="stepPrefixClass">{{ stepPrefix }}</span>
+            <span class="step-title-detail" ng-if="stepTitleDetail">{{ stepTitleDetail }}</span>
+        </div>
+
+        <div ng-if="expanded" class="step-details">
+            <div ng-if="osi.countStarted" class="space-above">
+                <div>
+                    <span ng-if="osi.countCompleted == osi.countStarted">
+                        <span ng-if="osi.countCompleted > 1">
+                            This step has run
+                            <span ng-if="osi.countCompleted == 2">
+                                twice,
+                            </span>
+                            <span ng-if="osi.countCompleted > 2">
+                                {{ osi.countCompleted }} times,
+                            </span>
+                            most recently
+                        </span>
+                        <span ng-if="osi.countCompleted == 1">
+                            This step ran
+                        </span>
+                    </span>
+                    <span ng-if="osi.countCompleted != osi.countStarted">
+                        <span ng-if="isCurrent">
+                            <span ng-if="osi.countCompleted == osi.countStarted - 1">
+                                This step is currently running
+                            </span>
+                            <span ng-if="osi.countCompleted <= osi.countStarted - 2">
+                                This step has had errors previously and is currently running
+                            </span>
+                        </span>
+                        <span ng-if="!isCurrent">
+                            <span ng-if="osi.countStarted == 1">
+                                This step had errors when it ran
+                            </span>
+                            <span ng-if="osi.countStarted > 2 && osi.countCompleted==0">
+                                This step has had errors on all previous runs, including when last run
+                            </span>
+                            <span ng-if="osi.countStarted > 2 && osi.countCompleted>0">
+                                This step has had errors on some previous runs. It most recently ran
+                            </span>
+                        </span>
+                    </span>
+
+                    <span ng-if="isFocusTask">
+                        in this task ({{ stepContext.taskId }}).
+                    </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>.
+                    </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>
+                </div>
+
+                <div  class="more-space-above">
+                    <div class="data-row" ng-if="step.name"><div class="A">Name</div> <div class="B">{{ step.name }}</div></div>
+                    <div class="data-row" ng-if="step.id"><div class="A">ID</div> <div class="B fixed-width">{{ step.id }}</div></div>
+                    <div class="data-row"><div class="A">Step Number</div> <div class="B">{{ stepIndex+1 }}</div></div>
+                    <div class="data-row"><div class="A">Definition</div> <div class="B multiline-code">{{ vm.yamlOrPrimitive(step) }}</div></div>
+                </div>
+
+                <div ng-if="osi.countStarted > 1 && osi.countStarted > osi.countCompleted" class="space-above">
+                    <div class="data-row"><div class="A">Runs</div> <div class="B"><b>{{ osi.countStarted }}</b></div></div>
+                    <div class="data-row"><div class="A">Succeeded</div> <div class="B">{{ osi.countCompleted }}</div></div>
+                    <div class="data-row"><div class="A">Failed</div> <div class="B">{{ osi.countCompleted - osi.countStarted - (isCurrent ? 1 : 0) }}</div></div>
+                </div>
+
+                <div class="more-space-above" ng-if="stepContext.taskId">
+                    <div class="data-row">
+                        <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 }}
+                                    </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>
+                                    </span>
+                        </div>
+                    </div>
+                    <div ng-if="!isFocusStep || isFocusTask">
+                        <div class="data-row nested"><div class="A">Preceeded by</div> <div class="B">
+                            <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>)
+                            </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>)
+                            </span>
+                            <span ng-if="!osi.nextTaskId">(workflow end)</span>
+                        </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>
+                    </div>
+                </div>
+
+            </div>
+            <div ng-if="!osi.countStarted" class="space-above">
+                This step has not been run<span ng-if="isRunning"> yet</span>.
+            </div>
+
+            <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>
+                        View data <span class="caret"></span>
+                    </button>
+                    <ul class="dropdown-menu pull-right" uib-dropdown-menu role="menu" aria-labelledby="single-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>
+                        <li role="menuitem" > <a href="" ng-click="vm.showJson('osi', osi)" ng-class="{'selected' : jsonMode === 'osi'}">
+                            <i class="fa fa-check check"></i>
+                            Executions Record</a> </li>
+                        <li role="menuitem" > <a href="" ng-click="vm.showJson('step', step)" ng-class="{'selected' : jsonMode === 'step'}">
+                            <i class="fa fa-check check"></i>
+                            Step Definition</a> </li>
+                        <li role="menuitem" > <a href="" ng-click="vm.showJson(null)" ng-class="{'selected' : jsonMode === null}">
+                            <i class="fa fa-check check"></i>
+                            None</a> </li>
+                    </ul>
+                </div>
+
+                <pre ng-if="json" class="space-above">{{ json }}</pre>
+            </div>
+        </div>
+
+    </div>
+
+</div>
+
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
new file mode 100644
index 00000000..1d509ada
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js
@@ -0,0 +1,354 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import template from "./workflow-steps.template.html";
+import angular from "angular";
+
+const MODULE_NAME = 'inspector.workflow-steps';
+
+angular.module(MODULE_NAME, [])
+    .directive('workflowSteps', workflowStepsDirective);
+
+export default MODULE_NAME;
+
+export function workflowStepsDirective() {
+    return {
+        template: template,
+        restrict: 'E',
+        scope: {
+            workflow: '=',
+            task: '=?',
+        },
+        controller: ['$sce', '$timeout', '$scope', '$element', controller],
+        controllerAs: 'vm',
+    };
+
+    function controller($sce, $timeout, $scope, $element) {
+        let vm = this;
+        //console.log("controller for workflow steps", $scope.workflow);
+
+        vm.stringify = stringify;
+
+        vm.getWorkflowStepsClasses = () => {
+            const c = [];
+            c.push('workflow-status-'+$scope.workflow.data.status);
+            if ($scope.workflow.data.status && $scope.workflow.data.status.startsWith('ERROR')) {
+                c.push('workflow-error');
+            }
+            return c;
+        }
+
+        $scope.expandStates = {};
+        if ($scope.workflow.tag && $scope.workflow.tag.stepIndex) {
+            $scope.expandStates[$scope.workflow.tag.stepIndex] = true;
+        }
+
+        vm.onSizeChange = () => $timeout(()=>recompute($scope, $element));
+
+        $scope.$watch('workflow', vm.onSizeChange);
+        vm.onSizeChange();
+    }
+
+    function recompute($scope, $element) {
+        let svg = $element[0].querySelector('#workflow-step-arrows');
+
+        let steps = $element[0].querySelectorAll('div');
+        // let steps = $element[0].querySelectorAll('.workflow-steps-main');
+        steps = $element[0].querySelectorAll('.workflow-step');
+        let arrows = makeArrows($scope.workflow, steps);
+
+        svg.innerHTML = arrows.join('\n');
+    }
+}
+
+function makeArrows(workflow, steps) {
+    workflow = workflow || {};
+    workflow.data = workflow.data || {};
+
+    let [stepsPrev,stepsNext] = getWorkflowStepsPrevNext(workflow);
+
+    const arrows = [];
+    const strokeWidth = 1.5;
+    const arrowheadLength = 6;
+    const arrowheadWidth = arrowheadLength/3/strokeWidth;
+    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><');
+
+    if (steps) {
+        let gradientCount = 0;
+        function arrowSvg(y1, y2, opts) {
+            var start = y1==='start/end';
+            var end = y2==='start/end';
+
+            if (y1==null || y2==null || (start&&end)) {
+                // ignore if out of bounds
+                return "";
+            }
+
+            if (!opts) opts = {};
+            const color = opts.color || (opts.colorEnd && opts.colorEnd==opts.colorStart ? opts.colorEnd : '#000');
+
+            const rightFarEdge = 56;
+            const rightArrowheadStart = rightFarEdge - arrowheadLength;
+            const leftFarEdge = 10;
+            const leftActive = rightArrowheadStart + (leftFarEdge - rightArrowheadStart) * (opts.width || 1);
+
+            const curveX = opts.curveX || 1;
+            const curveY = opts.curveY || 1;
+
+            // const controlPointRightFarEdge = rightFarEdge + (leftActive - rightFarEdge) * curveX;
+            const controlPointRightArrowheadStart = rightArrowheadStart + (leftActive - rightArrowheadStart) * curveX;
+            // average of above two, to see which works best
+            // const controlPointRightIntermediate = (rightFarEdge+rightArrowheadStart)/2 + (leftActive - (rightFarEdge+rightArrowheadStart)/2) * curveX;
+            // const controlPointRightExaggerated = rightArrowheadStart + (leftActive - rightFarEdge) * curveX;
+            const controlPointStart = controlPointRightArrowheadStart;
+            const controlPointEnd = controlPointRightArrowheadStart;
+
+            const strokeConstant =
+                'stroke="'+color+'"';
+
+            let standard =
+                'stroke-width="'+(opts.lineWidth || strokeWidth)+'" '+
+                'fill="transparent" '+
+                '/>';
+            if (!opts.hideArrowhead) standard = 'marker-end="url(#'+(opts.arrowheadId || 'arrowhead')+'" ' +standard;
+            if (opts.dashLength) standard = 'stroke-dasharray="'+opts.dashLength+'" '+standard;
+
+            if (start) {
+                return '<path d="M ' + leftFarEdge + ' ' + y2 +
+                    ' L ' + rightArrowheadStart + ' ' + y2 + '" '+
+                    strokeConstant+' '+standard;
+            }
+            if (end) {
+                return '<path d="M ' + rightFarEdge + ' ' + y1 +
+                    ' L ' + (leftFarEdge+arrowheadLength) + ' ' + y1 + '" '+
+                    strokeConstant+' '+standard;
+            }
+
+            const yMCH = ((y2 - y1) / 2) * curveY;
+            const yM = (y1 + y2) / 2;
+
+            if (!opts.colorEnd || opts.colorEnd==opts.colorStart || y2==y1) {
+                standard = strokeConstant + ' ' + standard;
+            } else {
+                const gradientId = 'gradient'+(gradientCount++);
+                const gradY = y2>=y1 ? 'y2="1"' : 'y1="1"';
+                defs.push('<linearGradient id="'+gradientId+'" x2="0" '+gradY+'><stop offset="0" stop-color="'+opts.colorStart+'"/><stop offset="1" stop-color="'+opts.colorEnd+'"/></linearGradient>');
+                standard = 'stroke="url(#'+gradientId+')" ' + standard;
+            }
+
+            return '<path d="M ' + rightFarEdge + ' ' + y1 +
+                // ' L ' + r0 + ' ' + y1 + ' ' +
+                ' C ' + controlPointStart + ' ' + y1 + ', ' + leftActive + ' ' + (yM - yMCH) + ', ' + leftActive + ' ' + yM + ' ' +
+                ' S ' + controlPointEnd + ' ' + y2 + ', ' + rightArrowheadStart + ' ' + y2 + '" '+standard;
+        }
+
+        function stepY(n) {
+            if (n==-1) return 'start/end';
+            if (!steps || n<0 || n>=steps.length) {
+                console.log("workflow arrow bounds error", steps, n);
+                return null;
+            }
+            return steps[n].offsetTop + steps[n].offsetHeight / 2;
+        }
+
+        function arrowStep(n1, n2, opts) {
+            let s1 = stepY(n1);
+            let s2 = stepY(n2);
+
+            const deltaForArrowMax = 6;
+            const deltaForArrowTarget = 0.125;
+            if (typeof s1 === "number") s1 += Math.min(steps[n1].offsetHeight * deltaForArrowTarget, deltaForArrowMax);
+            if (typeof s2 === "number") s2 -= Math.min(steps[n2].offsetHeight * deltaForArrowTarget, deltaForArrowMax);
+            return arrowSvg(s1, s2, opts);
+        }
+
+        function colorFor(step, references) {
+            if (!references) return 'red';
+            const i = references.indexOf(step);
+            if (i==-1) return 'red';
+            // skew quadratically for lightness
+            const skewTowards1 = x => (1 - (1-x)*(1-x));
+            let gray = Math.round(240 * skewTowards1(i / references.length) );
+            return 'rgb('+gray+','+gray+','+gray+')';
+        }
+
+        let jumpSizes = {1: 0};
+        for (var i = -1; i < steps.length - 1; i++) {
+            const prevsHere = stepsPrev[i];
+            if (prevsHere && prevsHere.length) {
+                prevsHere.forEach(prev => {
+                    if (i!=-1 && prev!=-1 && i!=prev) {
+                        jumpSizes[Math.abs(prev - i)] = true;
+                    }
+                });
+            }
+        }
+        jumpSizes = Object.keys(jumpSizes).sort();
+
+        function arrowStep2(prev, i, opts) {
+            let curveX = 0.5;
+            let curveY = 0.75;
+            let width = 0.5;
+            if (prev==-1 || i==-1) {
+                // curve values don't matter for start/end
+            } else if (prev==i) {
+                width = 0.15;
+                curveX = 0.1;
+                curveY = 0.75;
+            } else {
+                let rank = jumpSizes.indexOf(''+Math.abs(prev-i));
+                if (rank<0) {
+                    console.log("Missing workflow link: ", prev, i);
+                    rank = 0;
+                }
+                if (prev > i) rank = rank + 0.5;
+                width = 0.2 + 0.6 * (rank + 0.5) / (jumpSizes.length + 0.5);
+                // curveX = 0.8 + 0.2*width;
+                // curveY = 0.8 + 0.2*width;
+                // higher values (above) look nicer, but make disambiguation of complex paths harder
+                curveX = 0.5 + 0.3*width;
+                curveY = 0.4 + 0.4*width;
+            }
+            return arrowStep(prev, i, {hideArrowhead: prev==i, width, curveX, curveY, ...opts});
+        }
+
+        for (var i = -1; i < steps.length; i++) {
+            const prevsHere = stepsPrev[i];
+            if (prevsHere && prevsHere.length) {
+                let insertionPoint = 0;
+                prevsHere.forEach(prev => {
+                    const colorStart = colorFor(i, stepsNext[prev]);
+                    const colorEnd = colorFor(prev, prevsHere);
+
+                    // last in list has higher z-order; this ensures within each prevStep we preserve order,
+                    // so inbound arrows are correct. currently we also prefer earlier steps, which isn't quite right for outbound arrows;
+                    // ideally we'd reconstruct the flow order, but that's a bit more work than we want to do just now.
+                    // so insertion point is always 0. (header items added at end so we don't need to include those here.)
+                    arrows.splice(insertionPoint, 0, arrowStep2(prev, i, { colorStart, colorEnd }));
+                });
+            }
+        }
+
+        // now make pale arrows for the default flow
+        var indexOfId = {};
+        for (var i = 0; i < steps.length; i++) {
+            const s = workflow.data.stepsDefinition[i];
+            if (s.id) {
+                indexOfId[s.id] = i;
+            }
+        }
+        if (steps.length>0) {
+            arrows.splice(0, 0, arrowStep2(-1, 0, {color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 }));
+        }
+        for (var i = 0; i < steps.length; i++) {
+            const s = workflow.data.stepsDefinition[i];
+            var next = null;
+            if (s.next) {
+                if (indexOfId[s.next]) {
+                    next = indexOfId[s.next];
+                } else {
+                    next = null;
+                }
+            } else {
+                if (s.type === 'return' || (s.userSuppliedShorthand && s.userSuppliedShorthand.startsWith("return"))) {
+                    next = -1;
+                } else {
+                    next = i + 1;
+                    if (next >= steps.length) next = -1;  //end
+                }
+            }
+            if (next!=null) arrows.splice(0, 0, arrowStep2(i, next, { color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 }));
+        }
+
+        // put defs at start
+        arrows.splice(0, 0, '<defs>'+defs.join('')+'</defs>');
+    }
+
+    return arrows;
+}
+
+function getWorkflowStepsPrevNext(workflow) {
+    let stepsPrev = {}
+    let stepsNext = {}
+
+    if (workflow && workflow.data.oldStepInfo) {
+        Object.entries(workflow.data.oldStepInfo).forEach(([k,v]) => {
+            stepsPrev[k] = v.previous || [];
+            stepsNext[k] = v.next || [];
+        });
+    }
+
+    // mock data
+    // // first in list is most recent
+    // stepsPrev = {
+    //     '-1': [ 3 ],
+    //     0: [ -1 ],
+    //     1: [ 0 ],
+    //     2: [ 1 ],
+    //     3: [ 2 ],
+    // }
+    // stepsNext = {
+    //     '-1': [ 0 ],
+    //     0: [ 1 ],
+    //     1: [ 2 ],
+    //     2: [ 3 ],
+    //     3: [ -1 ],
+    // }
+    //
+    // stepsPrev = {
+    //     '-1': [ 2 ],
+    //     0: [ -1 ],
+    //     1: [ 1, 4, 0 ],
+    //     2: [ 3, 1 ],
+    //     3: [ 2 ],
+    //     4: [ 1 ],
+    // }
+    // stepsNext = {
+    //     '-1': [ 0 ],
+    //     0: [ 1 ],
+    //     1: [ 2, 1, 4, 0 ],
+    //     2: [ -1, 3 ],
+    //     3: [ 2 ],
+    //     4: [ 1 ],
+    // }
+
+    // // even more complex
+    // stepsPrev = {
+    //     '-1': [ 2 ],
+    //     0: [ 3, -1 ],
+    //     1: [ 1, 4, 0 ],
+    //     2: [ 3, 1 ],
+    //     3: [ 2, 0 ],
+    //     4: [ 1 ],
+    // }
+    // stepsNext = {
+    //     '-1': [ 0 ],
+    //     0: [ 1, 3 ],
+    //     1: [ 2, 1, 4, 0 ],
+    //     2: [ -1, 3 ],
+    //     3: [ 2, 0 ],
+    //     4: [ 1 ],
+    // }
+
+    return [stepsPrev, stepsNext];
+}
+
+function stringify(data) { return JSON.stringify(data, null, 2); }
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
new file mode 100644
index 00000000..0a9bd803
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+.workflow-steps {
+
+  position: relative;
+
+  .workflow-steps-main {
+    position: relative;
+  }
+
+  .workflow-step {
+    margin-left: 60px;
+    margin-right: 60px;
+    margin-top: 12px;
+    margin-bottom: 12px;
+
+    padding-left: 10px;
+    padding-right: 10px;
+    padding-top: 5px;
+    padding-bottom: 5px;
+
+    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;
+      gap: 1ex;
+      .expand-toggle {
+        cursor: pointer;
+      }
+    }
+    .workflow-step-pill {
+      padding: 2px 6px;
+      border-radius: 12px;
+      background: @gray-lighter;
+      font-size: 75%;
+      &.focus-step {
+        background: @primary-100;
+      }
+    }
+    .step-block-title {
+      overflow-x: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      padding-right: 1.5ex;
+
+      .step-name, .step-id, .step-index {
+        padding-right: 1.5ex;
+      }
+
+      .step-name {
+        font-weight: 600;
+        font-size: 100%;
+      }
+
+      .step-id {
+        .monospace();
+        font-size: 85%;
+        font-weight: 600;
+      }
+
+      .step-index {
+        font-size: 90%;
+        //font-style: italic;
+      }
+
+      .step-title-detail {
+        .monospace();
+        font-size: 85%;
+        font-weight: 300;
+        color: @gray-light;
+      }
+    }
+    .step-details {
+      margin-top: 12px;
+      .space-above {
+        margin-top: 6px;
+      }
+      .more-space-above, .data-row.more-space-above {
+        margin-top: 12px;
+      }
+      .data-row {
+        display: flex;
+        margin-top: 3px;
+        margin-bottom: 3px;
+        .A {
+          flex: 0 0 auto;
+          width: 30%;
+          overflow: hidden;
+          white-space: nowrap;
+          color: @gray-light;
+          font-family: @font-family-monospace;
+          font-size: 85%;
+          text-transform: uppercase;
+        }
+        .B {
+          flex: 1 1 auto;
+        }
+
+        &.nested {
+          .A {
+            padding-left: 1em;
+          }
+        }
+      }
+      .btn-group.right {
+        width: 100%;
+
+        > .pull-right {
+          float: none;
+        }
+        > .dropdown-menu {
+          width: auto;
+          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;
+        }
+      }
+    }
+  }
+
+  .workflow-step-status-indicators {
+    //position: absolute;
+    //width: 60px;
+    //text-align: right;
+    //padding-right: 1ex;
+    //margin-top: 8px;
+    position: absolute;
+    width: 60px;
+    text-align: right;
+    padding-right: 1ex;
+    margin-top: 0;
+    display: flex;
+    gap: 6px;
+    margin-top: 3px;
+    height: 30px;
+    align-items: center;
+    justify-content: end;
+
+    .color-succeeded { color: @color-succeeded; }
+    .color-failed { color: @color-failed; }
+    .color-cancelled { color: @color-cancelled; }
+    .color-active { color: @color-active; }
+
+    .running-status {
+      svg {
+        //margin: 0 auto;
+        //background: none;
+        //display: block;
+        //
+        width: 18px;
+        height: 18px;
+        margin-top: 2px;
+      }
+    }
+
+    i.fa {
+      font-size: 20px;
+    }
+
+    //// same as entity-effector.less
+    //.effector-pill {
+    //  background: @gray-lighter;
+    //  padding: 2px 6px;
+    //  &:first-child {
+    //    padding-left: 10px;
+    //    border-bottom-left-radius: 12px;
+    //    border-top-left-radius: 12px;
+    //  }
+    //  &:last-child {
+    //    padding-right: 10px;
+    //    border-bottom-right-radius: 12px;
+    //    border-top-right-radius: 12px;
+    //  }
+    //}
+  }
+
+  .multiline-code {
+    white-space: pre;
+    overflow: scroll;
+    max-height: 100px;
+  }
+  .multiline-code, .fixed-width {
+    .monospace();
+    font-size: 85%;
+  }
+  .fixed-width {
+    overflow: hidden;
+  }
+
+}
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html b/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html
new file mode 100644
index 00000000..309ce4a2
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html
@@ -0,0 +1,35 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<div class="workflow-steps" ng-class="vm.getWorkflowStepsClasses()">
+
+    <svg width="100%" height="100%" style="position: absolute;">
+        <g transform="scale(-1,1)" transform-origin="center" id="workflow-step-arrows">
+        </g>
+    </svg>
+
+    <div style="position: relative;">
+        <div ng-if="workflow.data.stepsDefinition" class="workflow-steps-main">
+            <div ng-repeat="step in workflow.data.stepsDefinition track by $index" id="workflow-step-outer-{{$index}}">
+                <workflow-step workflow="workflow" task="task" step="step" step-index="$index" expanded="expandStates[$index]" on-size-change="vm.onSizeChange"></workflow-step>
+            </div>
+        </div>
+    </div>
+
+</div>
+
diff --git a/ui-modules/app-inspector/app/index.js b/ui-modules/app-inspector/app/index.js
index 219e842f..c82707b3 100755
--- a/ui-modules/app-inspector/app/index.js
+++ b/ui-modules/app-inspector/app/index.js
@@ -48,6 +48,8 @@ import taskList from "components/task-list/task-list.directive";
 import taskSunburst from "components/task-sunburst/task-sunburst.directive";
 import stream from "components/stream/stream.directive";
 import adjunctsList from "components/adjuncts-list/adjuncts-list";
+import workflowSteps from "components/workflow/workflow-steps.directive";
+import workflowStep from "components/workflow/workflow-step.directive";
 import {mainState} from "views/main/main.controller";
 import {inspectState} from "views/main/inspect/inspect.controller";
 import {summaryState, specToLabelFilter} from "views/main/inspect/summary/summary.controller";
@@ -69,6 +71,7 @@ angular.module('brooklynAppInspector', [ngResource, ngCookies, ngSanitize, uiRou
     brServerStatus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brSensitiveField, brooklynUserManagement,
     brYamlEditor, brWebNotifications, brExpandablePanel, 'xeditable', brLogbook, apiProvider, entityTree, loadingState,
     configSensorTable, entityEffector, entityPolicy, breadcrumbNavigation, taskList, taskSunburst, stream, adjunctsList,
+    workflowSteps, workflowStep,
     managementDetail, brandAngularJs])
     .provider('catalogApi', catalogApiProvider)
     .provider('apiObserverInterceptor', apiObserverInterceptorProvider)
diff --git a/ui-modules/app-inspector/app/index.less b/ui-modules/app-inspector/app/index.less
index 9988be2f..8e446770 100644
--- a/ui-modules/app-inspector/app/index.less
+++ b/ui-modules/app-inspector/app/index.less
@@ -44,6 +44,7 @@
 @import "components/stream/stream.less";
 @import "components/task-list/task-list.less";
 @import "components/task-sunburst/task-sunburst.less";
+@import "components/workflow/workflow-steps.less";
 @import "components/breadcrumb-navigation/breadcrumb-navigation.less";
 @import "components/adjuncts-list/adjuncts-list.less";
 
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 d0e29f35..02388f97 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
@@ -42,7 +42,8 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
         entityId: entityId,
         activityId: activityId,
         childFilter: {'EFFECTOR': true, 'SUB-TASK': false},
-        accordion: {summaryOpen: true, subTaskOpen: true, streamsOpen: true}
+        accordion: {summaryOpen: true, subTaskOpen: true, streamsOpen: true, workflowOpen: true},
+        workflow: {},
     };
 
     vm.modalTemplate = modalTemplate;
@@ -67,6 +68,28 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 }
             }
 
+            if ((vm.model.activity.tags || []).find(t => t=="WORKFLOW")) {
+                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';
+                    });
+                }
+            }
+
             vm.error = undefined;
             observers.push(response.subscribe((response)=> {
                 vm.model.activity = response.data;
@@ -151,6 +174,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
     };
 
     vm.stringifyActivity = () => JSON.stringify(vm.model.activity, null, 2);
+    vm.stringify = (data) => JSON.stringify(data, null, 2);
 
     vm.invokeEffector = (effectorName, effectorParams) => {
         entityApi.invokeEntityEffector(applicationId, entityId, effectorName, effectorParams).then((response) => {
@@ -183,3 +207,9 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
     }
 
 }
+
+function findWorkflowTag(task) {
+    if (!task) return null;
+    if (!task.tags) return null;
+    return task.tags.find(t => t.workflowId);
+}
\ No newline at end of file
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 bdad7360..b6eb05e5 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
@@ -17,8 +17,6 @@
  * under the License.
  */
 
-@gray-light-lighter: lighten(@gray-light, 20%);
-
 .activity-detail {
     .activity-header {
         -webkit-font-smoothing: antialiased;
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 0598078c..25eada16 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
@@ -157,10 +157,34 @@
                         </div>
                     </br-collapsible>
 
+                    <br-collapsible state="vm.model.accordion.workflowOpen"
+                                    ng-if="vm.model.workflow">
+                        <heading> Workflow</heading>
+
+                        <div class="workflow-body">
+                            <div ng-if="vm.model.workflow.loading == 'loaded'">
+                                <p 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
+                                        <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>
+                                    </span>
+                                    <span ng-if="!vm.model.workflow.tag.stepIndex"> workflow <b>{{vm.model.workflow.data.name}}</b>.</span>
+                                </p>
+                                <workflow-steps workflow="vm.model.workflow" task="vm.model.activity"></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>
+                            </div>
+                        </div>
+                    </br-collapsible>
+
                     <br-collapsible state="vm.model.accordion.subTaskOpen"
                                     ng-if="vm.model.activityChildren && vm.model.activityChildren.length > 0">
                         <heading> Sub-tasks</heading>
-                        
+
                         <div class="row">
                             <div ng-class="{ 'col-md-12': true, 'col-lg-8': !vm.wideKilt && vm.isNonEmpty(vm.model.activitiesDeep), 'col-lg-12': vm.wideKilt || !vm.isNonEmpty(vm.model.activitiesDeep)}">
                                 <task-list tasks="vm.model.activityChildren" task-type="activityChildren" filtered-callback="vm.onFilteredActivitiesChange"></task-list>
diff --git a/ui-modules/shared/style/first.less b/ui-modules/shared/style/first.less
index 436e0c4c..faf640f7 100644
--- a/ui-modules/shared/style/first.less
+++ b/ui-modules/shared/style/first.less
@@ -28,6 +28,11 @@
 @import '~font-awesome/less/font-awesome';
 
 @navbar-divider-color: rgba(60, 85, 136, .5);
+@gray-light-lighter: lighten(@gray-light, 20%);
+@color-succeeded: #363;
+@color-failed: #820;
+@color-cancelled: #660;
+@color-active: #6a2;
 
 .navbar-text {
   float: left;


[brooklyn-ui] 02/24: workflow UI polish, subworkflows, bug-fixes

Posted by he...@apache.org.
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 <al...@cloudsoft.io>
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>


[brooklyn-ui] 05/24: expand actions, move to top-right

Posted by he...@apache.org.
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 0526f027e1c1682e56f71ca7cb360f141408ff28
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Wed Oct 5 15:51:33 2022 +0100

    expand actions, move to top-right
    
    supports cancel, most of what we need for replaying also.
    tweaks to the nested dropdown so that it works as we want.
---
 .../components/providers/activity-api.provider.js  |  8 +-
 .../inspect/activities/detail/detail.controller.js | 31 +++++++-
 .../main/inspect/activities/detail/detail.less     | 30 ++++++--
 .../inspect/activities/detail/detail.template.html | 31 ++++++--
 .../inspect/activities/detail/dropdown-nested.js   | 90 ++++++++++++++++++----
 5 files changed, 159 insertions(+), 31 deletions(-)

diff --git a/ui-modules/app-inspector/app/components/providers/activity-api.provider.js b/ui-modules/app-inspector/app/components/providers/activity-api.provider.js
index cdb36630..27deb5cc 100644
--- a/ui-modules/app-inspector/app/components/providers/activity-api.provider.js
+++ b/ui-modules/app-inspector/app/components/providers/activity-api.provider.js
@@ -39,7 +39,8 @@ function ActivityApi($http) {
         activity: getActivity,
         activityChildren: getActivityChildren,
         activityDescendants: getActivityDescendants,
-        activityStream: getActivityStream
+        activityStream: getActivityStream,
+        cancelActivity: cancelActivity,
     };
 
     function getActivities() {
@@ -64,4 +65,9 @@ function ActivityApi($http) {
             }
         }});
     }
+
+    function cancelActivity(activityId) {
+        return $http.post('/v1/activities/' + activityId + '/cancel');
+    }
+
 }
\ No newline at end of file
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 7ef65aba..9a9e1e0c 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
@@ -25,7 +25,7 @@ export const detailState = {
     url: '/:activityId',
     template: template,
     controller: ['$scope', '$state', '$stateParams', '$log', '$uibModal', '$timeout', '$sanitize', '$sce', 'activityApi', 'entityApi', 'brUtilsGeneral', DetailController],
-    controllerAs: 'vm'
+    controllerAs: 'vm',
 }
 function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeout, $sanitize, $sce, activityApi, entityApi, Utils) {
     $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
@@ -71,10 +71,27 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 vm.model.workflow.applicationId = workflowTag.applicationId;
                 vm.model.workflow.entityId = workflowTag.entityId;
 
+                vm.actions.workflowReplays = [];
+                if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) {
+                    // can't replay if active (same logic as 'cancel')
+                } else {
+                    [
+                        // TODO get from server
+                        // [ 'step 3 (continuing)', null ],
+                        // [ 'step 3 (replay point)', [2] ],
+                        // [ 'start (replay point)', [0] ],
+                    ].forEach(r => vm.actions.workflowReplays.push(r));
+                    vm.actions.workflowReplays.forEach(r => {
+                        r.push( () => console.log("TODO - replay from "+r[0], r[1]) );
+                    })
+                }
+                if (!vm.actions.workflowReplays.length) delete vm.actions['workflowReplays'];
+
                 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;
@@ -89,7 +106,9 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
         activityApi.activity(activityId).then((response)=> {
             vm.model.activity = response.data;
 
-            vm.actions = {};
+            vm.actions = vm.actions || {};
+            delete vm.actions['effector'];
+            delete vm.actions['invokeAgain'];
             if ((vm.model.activity.tags || []).find(t => t=="EFFECTOR")) {
                 const effectorName = (vm.model.activity.tags.find(t => t.effectorName) || {}).effectorName;
                 const effectorParams = (vm.model.activity.tags.find(t => t.effectorParams) || {}).effectorParams;
@@ -101,6 +120,11 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 }
             }
 
+            delete vm.actions['cancel'];
+            if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) {
+                vm.actions.cancel = { doAction: () => { activityApi.cancelActivity(activityId); } };
+            }
+
             if ((vm.model.activity.tags || []).find(t => t=="WORKFLOW")) {
                 const workflowTag = findWorkflowTag(vm.model.activity);
                 if (workflowTag) {
@@ -241,7 +265,8 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
     }
 
     vm.isNullish = _.isNil;
-
+    vm.isEmpty = x => vm.isNullish(x) || (x.length==0) || (typeof x === 'object' && !Object.keys(x).length);
+    vm.isNonEmpty = x => !vm.isEmpty(x);
 }
 
 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 2dc74ee4..63068c45 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
@@ -21,6 +21,7 @@
     .activity-header {
         -webkit-font-smoothing: antialiased;
         margin-bottom: 40px;
+
         status {
             width: 50px;
             height: 50px;
@@ -50,7 +51,7 @@
             color: @gray-light;
         }
     }
-    
+
     table.summary {
         tr td:first-child {
             width: 25%;
@@ -69,7 +70,7 @@
     .summary-body {
         margin-bottom: 24px;
     }
-    
+
     .summary-item {
         margin-bottom: 35px;
 
@@ -77,20 +78,24 @@
         @redColor: @brand-danger;
         @greenColor: #58BA58;
         @redColor: #BA5858;
+
         .summary-item-value {
             &.status-completed {
                 color: @greenColor;
                 font-weight: bold;
                 -webkit-font-smoothing: antialiased;
             }
+
             &.status-failed {
                 color: @redColor;
                 font-weight: bold;
                 -webkit-font-smoothing: antialiased;
             }
+
             &.status-in-progress {
 
             }
+
             &.status-unknown {
 
             }
@@ -137,6 +142,7 @@
                 > div {
                     opacity: 1.0;
                     position: absolute;
+
                     &.fade.ng-hide {
                         opacity: 0;
                     }
@@ -157,20 +163,23 @@
             }
         }
     }
+
     .monospace, .result-parent {
         .monospace();
     }
+
     .result-parent.big-result {
-       border: 1px solid @gray-lighter;
-      .result-body {
-        padding: 4px;
-      }
+        border: 1px solid @gray-lighter;
+        .result-body {
+            padding: 4px;
+        }
     }
+
     .result-body {
         max-height: 56pt;
         overflow: scroll;
     }
-    
+
     .collapsing {
         // internal class used by bootstrap when opening/closing:
         // activity viewers are probably power users - don't want to
@@ -205,10 +214,17 @@
                     margin-bottom: -3px;
                 }
             }
+
             .check {
                 display: none;
             }
         }
     }
+}
 
+.dropdown-at-root {
+    width: auto;
+    &.dropdown-submenu-left, .dropdown-submenu-left {
+        margin-right: 36px;
+    }
 }
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 09e45831..254435f4 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
@@ -38,12 +38,38 @@
 
         <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 style="float:right;" ng-if="vm.isNonEmpty(vm.actions)" class="btn-group dropdown-nested dropdown-menu-right" uib-dropdown-nested dropdown-append-to-body="true">
+                    <br-button uib-dropdown-toggle-nested type="btn-primary">
+                        Actions <span class="caret"></span>
+                    </br-button>
+                    <ul uib-dropdown-menu-nested class="dropdown-at-root dropdown-menu-right">
+
+                        <li><a href="" ng-if="vm.actions.cancel" ng-click="vm.actions.cancel.doAction()">Cancel</a></li>
+
+                        <li ng-if="vm.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 vm.actions.workflowReplays track by $index" id="replay {{ replay[0] }}">
+                                    <a class="dropdown-item" href="" ng-click="replay[2]()">From {{ replay[0] }}</a>
+                                </li>
+                            </ul>
+
+                        </li>
+
+                        <li><a href="" ng-if="vm.actions.invokeAgain" ng-click="vm.actions.invokeAgain.doAction()">Reinvoke effector</a></li>
+                        <li><a href="" ng-if="vm.actions.effector" ui-sref="main.inspect.effectors({search: vm.actions.effector.effectorName})">Open in effector tab</a></li>
+
+                    </ul>
+                </div>
+
                 <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>
@@ -126,11 +152,6 @@
                         </div>
                     </div>
 
-                    <div class="activity-actions" ng-if="vm.actions">
-                        <br-button ng-if="vm.actions.invokeAgain" on-click="vm.actions.invokeAgain.doAction()">Execute again</br-button>
-                        <br-button ng-if="vm.actions.effector" ui-sref="main.inspect.effectors({search: vm.actions.effector.effectorName})">Open effector tab</br-button>
-                    </div>
-
                     <br-collapsible class="activity-streams"
                                     ng-if="vm.isNonEmpty(vm.model.activity.streams)"
                                     state="vm.model.accordion.streamsOpen">
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 6d983bb9..00680a61 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
@@ -1,3 +1,5 @@
+import {drop} from "lodash/array";
+
 const MODULE_NAME = 'ui.bootstrap.dropdown.nested';
 
 export default MODULE_NAME;
@@ -11,6 +13,7 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
 
     .service('uibDropdownServiceNested', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
         var openScope = null;
+        var oldOpenScopes = [];
         var openedContainers = $$multiMap.createNew();
 
         this.isOnlyOpen = function(dropdownScope, appendTo) {
@@ -37,7 +40,8 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
             }
 
             if (openScope && openScope !== dropdownScope) {
-                openScope.isOpen = false;
+                // just remember it
+                oldOpenScopes.push(openScope);
             }
 
             openScope = dropdownScope;
@@ -65,9 +69,15 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
 
         this.close = function(dropdownScope, element, appendTo) {
             if (openScope === dropdownScope) {
+                openScope = null;
+            }
+            const indexOfOpen = oldOpenScopes.indexOf(dropdownScope);
+            if (indexOfOpen>=0) {
+                oldOpenScopes.splice(indexOfOpen, 1);
+            }
+            if (openScope==null && oldOpenScopes.length) {
                 $document.off('click', closeDropdown);
                 $document.off('keydown', this.keybindFilter);
-                openScope = null;
             }
 
             if (!appendTo) {
@@ -92,28 +102,78 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
         var closeDropdown = function(evt) {
             // This method may still be called during the same mouse event that
             // unbound this event handler. So check openScope before proceeding.
-            if (!openScope || !openScope.isOpen) { return; }
+            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;
+            }
 
-            if (evt && openScope.getAutoClose() === 'disabled') { return; }
+            function isAnyTrigger(element) {
+                return element.hasClass('dropdown-toggle');
+            }
 
-            if (evt && evt.which === 3) { return; }
+            function closeIfApplicable(scope) {
+                if (evt && scope.getAutoClose() === 'disabled') {
+                    return;
+                }
 
-            var toggleElement = openScope.getToggleElement();
-            if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
-                return;
+                if (evt && 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
+
+                var toggleElement = scope.getToggleElement();
+                if (evt && toggleElement && containsNested(toggleElement, evt.target)) {
+                    return;
+                }
+
+                var dropdownElement = scope.getDropdownElement();
+                if (evt &&
+                    scope.getAutoClose() === 'outsideClick' &&
+                    dropdownElement && containsNested(dropdownElement, evt.target)) {
+                    return;
+                }
+                scope.isOpen = false;
+                scopesToApply.push(scope);
+
+                return true;
             }
 
-            var dropdownElement = openScope.getDropdownElement();
-            if (evt && openScope.getAutoClose() === 'outsideClick' &&
-                dropdownElement && dropdownElement[0].contains(evt.target)) {
-                return;
+            if (openScope && openScope.isOpen) {
+                if (closeIfApplicable(openScope)) {
+                    openScope.focusToggleElement();
+                }
             }
 
-            openScope.focusToggleElement();
-            openScope.isOpen = false;
+            // close all the others too
+            const scopesToKeep = [];
+            oldOpenScopes.forEach(scope => {
+                if (!closeIfApplicable(scope)) {
+                    scopesToKeep.push(scope);
+                }
+            });
+            oldOpenScopes.splice(0, oldOpenScopes.length, ...scopesToKeep);
 
+            // and apply
             if (!$rootScope.$$phase) {
-                openScope.$apply();
+                scopesToApply.forEach(s => s.$apply());
             }
         };
 


[brooklyn-ui] 12/24: consistent colours and icons for task and br status

Posted by he...@apache.org.
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 54a57c412d28d4f3f834d568542817a191e73580
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 6 15:57:48 2022 +0100

    consistent colours and icons for task and br status
    
    switches tasks to use check and x
---
 ui-modules/shared/style/first.less            |  7 +------
 ui-modules/utils/br-core/style/variables.less | 13 +++++++++++++
 ui-modules/utils/status/status.js             | 12 +++++++-----
 ui-modules/utils/status/status.less           |  9 +++++++++
 ui-modules/utils/status/status.template.html  | 21 +++++++++++++++++++++
 5 files changed, 51 insertions(+), 11 deletions(-)

diff --git a/ui-modules/shared/style/first.less b/ui-modules/shared/style/first.less
index faf640f7..83df06d1 100644
--- a/ui-modules/shared/style/first.less
+++ b/ui-modules/shared/style/first.less
@@ -27,12 +27,7 @@
 @import '~brooklyn-ui-utils/br-core/br-core.less';
 @import '~font-awesome/less/font-awesome';
 
-@navbar-divider-color: rgba(60, 85, 136, .5);
-@gray-light-lighter: lighten(@gray-light, 20%);
-@color-succeeded: #363;
-@color-failed: #820;
-@color-cancelled: #660;
-@color-active: #6a2;
+// see variables.less (in our br-core, and stock bootstrap)
 
 .navbar-text {
   float: left;
diff --git a/ui-modules/utils/br-core/style/variables.less b/ui-modules/utils/br-core/style/variables.less
index 439ee28b..efa00800 100644
--- a/ui-modules/utils/br-core/style/variables.less
+++ b/ui-modules/utils/br-core/style/variables.less
@@ -20,6 +20,16 @@
 @brand-primary: #3C5588;
 @brand-accent: #bf3727;
 
+@color-succeeded: #363;
+@color-failed: #820;
+@color-cancelled: #660;
+@color-active: #6a2;
+
+// brand-success and others usually used for BG, so make lighter
+@color-succeeded-bg: #484;
+@color-failed-bg: #A41;
+@brand-success: @color-succeeded-bg;
+@brand-danger: @color-failed-bg;
 
 // bootstrap colours at
 // https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss
@@ -27,10 +37,13 @@
 @gray-light: #818899;   // override bootstrap default of #777
 @gray-lighter: #E1E5E7; // override bootstrap default of #eee
 @gray-lightest: #f8f8f9;
+
+@gray-light-lighter: lighten(@gray-light, 20%);
 @label-gray: darken(@gray-light, 10%);  // between @gray and @gray-light
 
 @body-bg: hsl(223,30%,97%);  // override bootstrap default, #333, same as @gray-dark, used for the button bar, body default bg colour from bootstrap scaffolding
 
+@navbar-divider-color: rgba(60, 85, 136, .5);
 @card-border-color: mix(black, @body-bg, 7%);
 
 /* Colors in pattern lab */
diff --git a/ui-modules/utils/status/status.js b/ui-modules/utils/status/status.js
index 17a21388..cf26b45a 100644
--- a/ui-modules/utils/status/status.js
+++ b/ui-modules/utils/status/status.js
@@ -27,7 +27,9 @@ const ICONS = {
     ON_FIRE: 'ON_FIRE',
     ERROR: 'ERROR',
     UNKNOWN: 'UNKNOWN',
-    NO_STATE: 'NO_STATE'
+    NO_STATE: 'NO_STATE',
+    COMPLETED: 'COMPLETED',
+    FAILED: 'FAILED',
 };
 
 const STATUS = {
@@ -43,10 +45,10 @@ const STATUS = {
 
     // for tasks
     'In progress': {name: 'In progress', icon: ICONS.STARTING},
-    'Completed': {name: 'Completed', icon: ICONS.RUNNING},
-    'Failed': {name: 'Failed', icon: ICONS.ERROR},
-    'Unavailable': {name: 'Incomplete', icon: ICONS.ERROR},
-    'Cancelled': {name: 'Cancelled', icon: ICONS.ERROR},
+    'Completed': {name: 'Completed', icon: ICONS.COMPLETED},
+    'Failed': {name: 'Failed', icon: ICONS.FAILED},
+    'Unavailable': {name: 'Incomplete', icon: ICONS.FAILED},
+    'Cancelled': {name: 'Cancelled', icon: ICONS.FAILED},
 };
 
 const MODULE_NAME = 'brooklyn.components.status';
diff --git a/ui-modules/utils/status/status.less b/ui-modules/utils/status/status.less
index 979b4613..fdbf6ec9 100644
--- a/ui-modules/utils/status/status.less
+++ b/ui-modules/utils/status/status.less
@@ -25,6 +25,12 @@
 @import "states/stopping.less";
 @import "states/unknown.less";
 
+brooklyn-status-icon, brooklyn-status-text {
+    .status-failed { color: @brand-danger; }
+    .status-completed { color: @brand-success; }
+}
+
+
 @circleBackgroundColor: rgba(0, 0, 0, 0.1);
 
 @status-stroke-width: 7;
@@ -41,6 +47,9 @@ brooklyn-status-icon {
         width: 100%;
         height: 100%;
     }
+    .fa {
+        font-size: 6vh;
+    }
 }
 
 
diff --git a/ui-modules/utils/status/status.template.html b/ui-modules/utils/status/status.template.html
index e2b89c6b..6204d204 100644
--- a/ui-modules/utils/status/status.template.html
+++ b/ui-modules/utils/status/status.template.html
@@ -60,6 +60,27 @@
             </g>
         </svg>
     </div>
+
+    <!-- for tasks -->
+    <div ng-switch-when="COMPLETED">
+        <svg viewBox="20 20 60 60">
+            <foreignObject x="20" y="15" width="100" height="100">
+                <body>
+                  <i class="fa fa-check-circle"></i>
+                </body>
+            </foreignObject>
+        </svg>
+    </div>
+    <div ng-switch-when="FAILED">
+        <svg viewBox="20 20 60 60">
+            <foreignObject x="20" y="15" width="100" height="100">
+                <body>
+                  <i class="fa fa-times-circle"></i>
+                </body>
+            </foreignObject>
+        </svg>
+    </div>
+
     <div ng-switch-when="UNKNOWN">
         <svg id="168033fd-2902-4e00-8797-d9d22be6bcd3" data-name="icon-unknown"
              xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">


[brooklyn-ui] 08/24: tidy up workflow step title, show more info on steps

Posted by he...@apache.org.
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 6879475f13b8f705002c050d2c5f11c58bdb42c7
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 6 11:46:32 2022 +0100

    tidy up workflow step title, show more info on steps
---
 .../components/workflow/workflow-step.directive.js | 34 ++++----
 .../workflow/workflow-step.template.html           | 96 ++++++++++++----------
 .../app/components/workflow/workflow-steps.less    | 49 ++++++-----
 3 files changed, 95 insertions(+), 84 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 51e2aef7..5ef1dde2 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
@@ -70,30 +70,25 @@ export function workflowStepDirective() {
                 $scope.json = json ? stringify(json) : null;
             }
 
+            $scope.stepTitle = {
+                index: index+1,
+            };
             if (typeof step === 'string') {
-                $scope.stepPrefixClass = 'step-index';
-                $scope.stepPrefix = index + 1;
-                $scope.stepTitleDetail = step;
+                $scope.stepTitle.code = step;
             } else {
 
                 let shorthand = step.userSuppliedShorthand || step.s || step.shorthand;
-                $scope.stepTitleDetail = shorthand;
+                $scope.stepTitle.code = shorthand;
+                if (!shorthand) {
+                    $scope.stepTitle.code = step.type || '';
+                    if (step.input) $scope.stepTitle.code += ' ...';
+                }
+
                 if (step.name) {
-                    $scope.stepPrefixClass = 'step-name';
-                    $scope.stepPrefix = step.name;
-                } else {
-                    if (step.id) {
-                        $scope.stepPrefixClass = 'step-id';
-                        $scope.stepPrefix = step.id;
-                    } else {
-                        $scope.stepPrefixClass = 'step-index';
-                        $scope.stepPrefix = index + 1;
-
-                        if (!shorthand) {
-                            $scope.stepTitleDetail = step.type || '';
-                            if (step.input) $scope.stepTitleDetail += ' ...';
-                        }
-                    }
+                    $scope.stepTitle.name = step.name;
+                }
+                if (step.id) {
+                    $scope.stepTitle.id = step.id;
                 }
             }
 
@@ -108,7 +103,6 @@ export function workflowStepDirective() {
                 $scope.isWorkflowError = (workflow.data.status && workflow.data.status.startsWith('ERROR'));
                 $scope.osi = workflow.data.oldStepInfo[index] || {};
                 $scope.stepContext = ($scope.isCurrent ? workflow.data.currentStepInstance : $scope.osi.context) || {};
-
                 $scope.isFocusStep = $scope.workflow.tag && ($scope.workflow.tag.stepIndex === index);
                 $scope.isFocusTask = false;
 
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 a0aa233a..86a79342 100644
--- a/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html
@@ -43,7 +43,14 @@
 
     <div class="workflow-step" id="workflow-step-{{stepIindex}}" ng-class="vm.getWorkflowStepClasses(stepIndex)">
         <div class="rhs-icons">
-            <div ng-if="isFocusTask" class="workflow-step-pill focus-step" title="This step instance is for the task currently selected in the activity view.">
+            <div ng-if="stepTitle.name" class="step-name">
+                {{ stepTitle.name}}
+            </div>
+            <div ng-if="stepTitle.id" class="workflow-step-pill step-id">
+                <i class="fa fa-id-card-o"></i>
+                {{ stepTitle.id }}
+            </div>
+            <div ng-if="isFocusTask" class="workflow-step-pill focus-step label-info" title="This step instance is for the task currently selected in the activity view.">
                 selected
             </div>
             <div ng-click="vm.toggleExpandState()" class="expand-toggle">
@@ -52,55 +59,60 @@
         </div>
 
         <div class="step-block-title">
-            <span ng-class="stepPrefixClass">{{ stepPrefix }}</span>
-            <span class="step-title-detail" ng-if="stepTitleDetail">{{ stepTitleDetail }}</span>
+            <span class="step-index">{{ stepTitle.index }}</span>
+            <span class="step-title-code">{{ stepTitle.code }}</span>
         </div>
 
         <div ng-if="expanded" class="step-details">
-            <div ng-if="osi.countStarted" class="space-above">
+            <div class="space-above">
                 <div>
-                    <span ng-if="osi.countCompleted == osi.countStarted">
-                        <span ng-if="osi.countCompleted > 1">
-                            This step has run
-                            <span ng-if="osi.countCompleted == 2">
-                                twice,
-                            </span>
-                            <span ng-if="osi.countCompleted > 2">
-                                {{ osi.countCompleted }} times,
-                            </span>
-                            most recently
-                        </span>
-                        <span ng-if="osi.countCompleted == 1">
-                            This step ran
-                        </span>
+                    <span ng-if="!osi.countStarted" class="space-above">
+                        This step has not been run<span ng-if="isRunning"> yet</span>.
                     </span>
-                    <span ng-if="osi.countCompleted != osi.countStarted">
-                        <span ng-if="isCurrent">
-                            <span ng-if="osi.countCompleted == osi.countStarted - 1">
-                                This step is currently running
+                    <span ng-if="osi.countStarted">
+                        <span ng-if="osi.countCompleted == osi.countStarted">
+                            <span ng-if="osi.countCompleted > 1">
+                                This step has run
+                                <span ng-if="osi.countCompleted == 2">
+                                    twice,
+                                </span>
+                                <span ng-if="osi.countCompleted > 2">
+                                    {{ osi.countCompleted }} times,
+                                </span>
+                                most recently
                             </span>
-                            <span ng-if="osi.countCompleted <= osi.countStarted - 2">
-                                This step has had errors previously and is currently running
+                            <span ng-if="osi.countCompleted == 1">
+                                This step ran
                             </span>
                         </span>
-                        <span ng-if="!isCurrent">
-                            <span ng-if="osi.countStarted == 1">
-                                This step had errors when it ran
+                        <span ng-if="osi.countCompleted != osi.countStarted">
+                            <span ng-if="isCurrent">
+                                <span ng-if="osi.countCompleted == osi.countStarted - 1">
+                                    This step is currently running
+                                </span>
+                                <span ng-if="osi.countCompleted <= osi.countStarted - 2">
+                                    This step has had errors previously and is currently running
+                                </span>
                             </span>
-                            <span ng-if="osi.countStarted > 2 && osi.countCompleted==0">
-                                This step has had errors on all previous runs, including when last run
-                            </span>
-                            <span ng-if="osi.countStarted > 2 && osi.countCompleted>0">
-                                This step has had errors on some previous runs. It most recently ran
+                            <span ng-if="!isCurrent">
+                                <span ng-if="osi.countStarted == 1">
+                                    This step had errors when it ran
+                                </span>
+                                <span ng-if="osi.countStarted > 2 && osi.countCompleted==0">
+                                    This step has had errors on all previous runs, including when last run
+                                </span>
+                                <span ng-if="osi.countStarted > 2 && osi.countCompleted>0">
+                                    This step has had errors on some previous runs. It most recently ran
+                                </span>
                             </span>
                         </span>
-                    </span>
 
-                    <span ng-if="isFocusTask">
-                        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 <span class="monospace">{{ stepContext.taskId }}</span></a>.
+                        <span ng-if="isFocusTask">
+                            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 <span class="monospace">{{ stepContext.taskId }}</span></a>.
+                        </span>
                     </span>
                 </div>
 
@@ -167,16 +179,16 @@
                             </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="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.otherMetadata" ng-repeat="(key,value) in stepContext.otherMetadata" id="$key">
+                            <div class="A">{{ key }}</div> <div class="B multiline-code">{{ vm.yamlOrPrimitive(value) }}</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>
                     </div>
                 </div>
 
             </div>
-            <div ng-if="!osi.countStarted" class="space-above">
-                This step has not been run<span ng-if="isRunning"> yet</span>.
-            </div>
 
             <div class="more-space-above" ng-if="vm.nonEmpty(stepContext) || vm.nonEmpty(step) || vm.nonEmpty(osi)">
 
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 59f2a854..f17e70bc 100644
--- a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
@@ -64,18 +64,33 @@
       float: right;
       display: flex;
       gap: 1ex;
+      align-items: center;
+      margin-left: 1em;
       .expand-toggle {
         cursor: pointer;
       }
+      .step-name {
+        font-weight: 600;
+        font-size: 90%;
+        margin-right: 1ex;
+        margin-top: 1px;
+      }
+
+      .step-id {
+        .monospace();
+        font-size: 80%;
+        background-color: @primary-50;
+      }
+      .focus-step {
+        //background color comes from label-info class
+        //background: @primary-100;
+      }
     }
     .workflow-step-pill {
       padding: 2px 6px;
-      border-radius: 12px;
-      background: @gray-lighter;
+      border-radius: 9px;
+      //background: @gray-lighter;
       font-size: 75%;
-      &.focus-step {
-        background: @primary-100;
-      }
     }
     .step-block-title {
       overflow-x: hidden;
@@ -83,33 +98,23 @@
       white-space: nowrap;
       padding-right: 1.5ex;
 
-      .step-name, .step-id, .step-index {
-        padding-right: 1.5ex;
-      }
-
-      .step-name {
-        font-weight: 600;
-        font-size: 90%;
-      }
-
-      .step-id {
-        .monospace();
-        font-size: 85%;
-        font-weight: 600;
-      }
-
       .step-index {
-        font-size: 90%;
+        font-size: 70%;
+        border: solid 1px black;
+        padding: 0px 3px;
+        border-radius: 14px;
+        margin-right: 1ex;
         //font-style: italic;
       }
 
-      .step-title-detail {
+      .step-title-code {
         .monospace();
         font-size: 85%;
         font-weight: 300;
         color: @gray-light;
       }
     }
+
     .step-details {
       margin-top: 12px;
       .space-above {


[brooklyn-ui] 09/24: tidy workflow consistency (when tasks don't load), date, misc

Posted by he...@apache.org.
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 e86f96c6b1583b166947b71147fd9a84ce8020e6
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 6 14:23:06 2022 +0100

    tidy workflow consistency (when tasks don't load), date, misc
---
 .../components/task-list/task-list.directive.js    | 27 ++++++++++++++++
 .../components/task-list/task-list.template.html   |  8 ++---
 .../components/workflow/workflow-step.directive.js |  6 ++--
 .../workflow/workflow-step.template.html           | 32 +++++++++----------
 .../inspect/activities/activities.controller.js    | 18 +++++------
 .../inspect/activities/detail/detail.controller.js | 28 +++++++++-------
 .../main/inspect/activities/detail/detail.less     |  5 +++
 .../inspect/activities/detail/detail.template.html | 37 ++++++++++++++--------
 .../inspect/management/detail/detail.template.html |  2 +-
 .../home/app/views/about/about.controller.js       |  2 ++
 10 files changed, 108 insertions(+), 57 deletions(-)

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 054aa305..35fc8b03 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
@@ -18,13 +18,16 @@
  */
 import angular from "angular";
 import {fromNow, duration} from "brooklyn-ui-utils/utils/momentp";
+import moment from "moment";
 import template from "./task-list.template.html";
+import {getTaskWorkflowTag} from "../../views/main/inspect/activities/detail/detail.controller";
 
 const MODULE_NAME = 'inspector.task-list';
 
 angular.module(MODULE_NAME, [])
     .directive('taskList', taskListDirective)
     .filter('timeAgoFilter', timeAgoFilter)
+    .filter('dateFilter', dateFilter)
     .filter('durationFilter', durationFilter)
     .filter('activityTagFilter', activityTagFilter)
     .filter('activityFilter', ['$filter', activityFilter]);
@@ -85,6 +88,11 @@ export function taskListDirective() {
         $scope.$watch('model.filterResult', function () {
             if ($scope.filteredCallback && $scope.model.filterResult) $scope.filteredCallback()( $scope.model.filterResult, $scope.globalFilters );
         });
+        $scope.getTaskWorkflowId = task => {
+            const tag = getTaskWorkflowTag(task);
+            if (tag) return tag.workflowId;
+            return null;
+        };
     }
 
     function tagReducer(result, tag) {
@@ -192,6 +200,7 @@ function topLevelTasks(tasks) {
 
 export function timeAgoFilter() {
     function timeAgo(input) {
+        if (!input || input<=0) return "-";
         return fromNow(input);
     }
 
@@ -199,6 +208,24 @@ export function timeAgoFilter() {
 
     return timeAgo;
 }
+
+export function dateFilter() {
+    function date(input, args) {
+        // if (!input || input<=0) return "-";
+
+        if (args==='short') {
+            return moment(input).format('MMM D, yyyy @ HH:mm:ss');
+        } else if (args==='iso') {
+            return moment(input).format('yyyy-MM-DD HH:mm:ss.SSS');
+        } else {
+            return moment(input).format('MMM D, yyyy @ HH:mm:ss.SSS');
+        }
+        return "TODO - "+input;
+    }
+
+    return date;
+}
+
 export function durationFilter() {
     return function (input) {
         return duration(input);
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.template.html b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
index a853d64f..2db6f8ba 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.template.html
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
@@ -57,22 +57,22 @@
             <tbody>
             <tr ng-repeat="task in tasks | activityTagFilter : [model.filterByTag, globalFilters] | activityFilter:filterValue as filterResult track by task.id">
                 <td class="status">
-                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id})">
+                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">
                     <brooklyn-status-icon value="{{task.currentStatus}}" ng-if="!isScheduled(task)"></brooklyn-status-icon>
                     <span ng-if="isScheduled(task)" class="custom-status-task-icon"><i class="fa fa-clock-o" style="font-size: 250%;"></i></span>
                     </a>
                 </td>
                 <td class="name">
-                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id})">{{task.displayName}}
+                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">{{task.displayName}}
                     </a>
                 </td>
                 <td class="started">
-                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id})">
+                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">
                         {{task.startTimeUtc | timeAgoFilter}}
                     </a>
                 </td>
                 <td class="duration">
-                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id})"
+                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})"
                         	ng-if="task.startTimeUtc">
                         {{getTaskDuration(task) | durationFilter}} <span ng-if="task.endTimeUtc === null">and counting</span>
                     </a>
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 5ef1dde2..30b8b8cf 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
@@ -51,6 +51,7 @@ export function workflowStepDirective() {
 
             let step = $scope.step;
             let index = $scope.stepIndex;
+            $scope.workflowId = ($scope.workflow && $scope.workflow.data || {}).workflowId;
 
             vm.stepDetails = () => stepDetails($sce, $scope.workflow, step, index, $scope.expanded);
             vm.toggleExpandState = () => {
@@ -98,11 +99,12 @@ export function workflowStepDirective() {
                 $scope.workflowStepClasses = [];
                 if (workflow.data.currentStepIndex === index) $scope.workflowStepClasses.push('current-step');
 
-                $scope.isCurrent = (workflow.data.currentStepIndex === index);
                 $scope.isRunning = (workflow.data.status === 'RUNNING');
+                $scope.isCurrentMaybeInactive = (workflow.data.currentStepIndex === index);
+                $scope.isCurrentAndActive = ($scope.isCurrentMaybeInactive && $scope.isRunning);
                 $scope.isWorkflowError = (workflow.data.status && workflow.data.status.startsWith('ERROR'));
                 $scope.osi = workflow.data.oldStepInfo[index] || {};
-                $scope.stepContext = ($scope.isCurrent ? workflow.data.currentStepInstance : $scope.osi.context) || {};
+                $scope.stepContext = ($scope.isCurrentMaybeInactive ? workflow.data.currentStepInstance : $scope.osi.context) || {};
                 $scope.isFocusStep = $scope.workflow.tag && ($scope.workflow.tag.stepIndex === index);
                 $scope.isFocusTask = false;
 
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 86a79342..5f0b78b4 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
@@ -19,7 +19,7 @@
 <div class="workflow-step-outer">
 
     <div class="workflow-step-status-indicators">
-        <span ng-if="isCurrent">
+        <span ng-if="isCurrentAndActive">
             <span ng-if="isRunning" class="running-status">
                 <brooklyn-status-icon value="STARTING"></brooklyn-status-icon>
             </span>
@@ -31,7 +31,7 @@
             </span>
 <!--            <span ng-if="osi.countCompleted > 1">{{ osi.countCompleted }}</span>-->
         </span>
-        <span ng-if="osi.countStarted && osi.countStarted != osi.countCompleted && !(isCurrent && isRunning)">
+        <span ng-if="osi.countStarted && osi.countStarted != osi.countCompleted && !isCurrentAndActive">
             <span class="color-failed" ng-if="isWorkflowError">
                 <i class="fa fa-times-circle"></i>
             </span>
@@ -86,7 +86,7 @@
                             </span>
                         </span>
                         <span ng-if="osi.countCompleted != osi.countStarted">
-                            <span ng-if="isCurrent">
+                            <span ng-if="isCurrentAndActive">
                                 <span ng-if="osi.countCompleted == osi.countStarted - 1">
                                     This step is currently running
                                 </span>
@@ -94,24 +94,24 @@
                                     This step has had errors previously and is currently running
                                 </span>
                             </span>
-                            <span ng-if="!isCurrent">
+                            <span ng-if="!isCurrentAndActive">
                                 <span ng-if="osi.countStarted == 1">
                                     This step had errors when it ran
                                 </span>
-                                <span ng-if="osi.countStarted > 2 && osi.countCompleted==0">
-                                    This step has had errors on all previous runs, including when last run
+                                <span ng-if="osi.countStarted >= 2 && osi.countCompleted==0">
+                                    This step has had errors on all previous runs, including the last run,
                                 </span>
-                                <span ng-if="osi.countStarted > 2 && osi.countCompleted>0">
+                                <span ng-if="osi.countStarted >= 2 && osi.countCompleted>0">
                                     This step has had errors on some previous runs. It most recently ran
                                 </span>
                             </span>
                         </span>
 
                         <span ng-if="isFocusTask">
-                            as the activity currently being viewed (<span class="monospace">{{ stepContext.taskId }}</span>).
+                            in the task focused on in this page (<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 <span class="monospace">{{ stepContext.taskId }}</span></a>.
+                            in <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId, workflowId })">task <span class="monospace">{{ stepContext.taskId }}</span></a>.
                         </span>
                     </span>
                 </div>
@@ -130,18 +130,18 @@
                 <div ng-if="osi.countStarted > 1 && osi.countStarted > osi.countCompleted" class="space-above">
                     <div class="data-row"><div class="A">Runs</div> <div class="B"><b>{{ osi.countStarted }}</b></div></div>
                     <div class="data-row"><div class="A">Succeeded</div> <div class="B">{{ osi.countCompleted }}</div></div>
-                    <div class="data-row"><div class="A">Failed</div> <div class="B">{{ osi.countCompleted - osi.countStarted - (isCurrent ? 1 : 0) }}</div></div>
+                    <div class="data-row"><div class="A">Failed</div> <div class="B">{{ osi.countStarted - osi.countCompleted - (isCurrentAndActive ? 1 : 0) }}</div></div>
                 </div>
 
                 <div class="more-space-above" ng-if="stepContext.taskId">
                     <div class="data-row">
-                        <div class="A"><span ng-if="isCurrent">CURRENT</span><span ng-if="!isCurrent">LAST</span> EXECUTION</div>
+                        <div class="A"><span ng-if="isCurrentAndActive">CURRENT</span><span ng-if="!isCurrent">LAST</span> EXECUTION</div>
                         <div class="B">
                                     <span ng-if="isFocusTask">
                                         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 })"
+                                         <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId, workflowId })"
                                             >task <span class="monospace">{{ stepContext.taskId }}</span></a>
                                     </span>
                         </div>
@@ -150,16 +150,16 @@
                         <div class="data-row nested"><div class="A">Preceeded by</div> <div class="B">
                             <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 })"
+                                (<a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: osi.previousTaskId, workflowId })"
                                     >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">
+                        <div class="data-row nested" ng-if="!isCurrentMaybeInactive"><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 })"
+                                (<a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: osi.nextTaskId, workflowId })"
                                     >task <span class="monospace">{{ osi.nextTaskId }}</span></a>)
                             </span>
                             <span ng-if="!osi.nextTaskId">(workflow end)</span>
@@ -184,7 +184,7 @@
                         <div class="data-row nested" ng-if="stepContext.otherMetadata" ng-repeat="(key,value) in stepContext.otherMetadata" id="$key">
                             <div class="A">{{ key }}</div> <div class="B multiline-code">{{ vm.yamlOrPrimitive(value) }}</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>
+                        <div class="data-row nested" ng-if="!isCurrentMaybeInactive && stepContext.output"><div class="A">Output</div> <div class="B multiline-code">{{ vm.yaml(stepContext.output) }}</div></div>
                     </div>
                 </div>
 
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index 8c3110b7..39f0622b 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -76,14 +76,14 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
             Object.values(vm.workflows || {})
                 .forEach(wf => {
                 (wf.replays || []).forEach(wft => {
-                    let newActivity = newActivitiesMap[wtf.taskId];
+                    let newActivity = newActivitiesMap[wft.taskId];
                     if (!newActivity) {
                         // create stub tasks for the replays of workflows
-                        newActivity = makeTaskStubFromWorkflowRecord(wf, wtf);
-                        newActivitiesMap[wtf.taskId] = newActivity;
+                        newActivity = makeTaskStubFromWorkflowRecord(wf, wft);
+                        newActivitiesMap[wft.taskId] = newActivity;
                     }
-                    newActivity.workflowId = wtf.workflowId;
-                    newActivity.isWorkflowOldReplay = wtf.workflowId !== wtf.taskId;
+                    newActivity.workflowId = wft.workflowId;
+                    newActivity.isWorkflowOldReplay = wft.workflowId !== wft.taskId;
                 });
             });
             newActivitiesMap['extra'] = makeTaskStubMock("Extra workflow", "extra", applicationId, entityId);
@@ -116,7 +116,7 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                 vm.error = undefined;
             }));
         }).catch((error) => {
-            $log.warn('Error loading activity children deep for '+activityId, error);
+            $log.warn('Error loading activity children deep for entity '+entityId, error);
             vm.error = 'Cannot load activities (deep) for entity with ID: ' + entityId;
         });
 
@@ -151,9 +151,9 @@ export function makeTaskStubFromWorkflowRecord(wf, wft) {
         id: wft.taskId,
         displayName: wf.name + (wft.reasonForReplay ? " ("+wft.reasonForReplay+")" : ""),
         entityId: (wf.entity || {}).id,
-        isError: wtf.isError===false ? false : true,
-        currentStatus: vm.isNullish(wtf.isError) ? "Unavailable" : wtf.status,
-        submitTimeUtc: wft.submittedTimeUtc,
+        isError: wft.isError===false ? false : true,
+        currentStatus: _.isNil(wft.isError) ? "Unavailable" : wft.status,
+        submitTimeUtc: wft.submitTimeUtc,
         startTimeUtc: wft.startTimeUtc,
         endTimeUtc: wft.endTimeUtc,
         tags: [
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 648142b9..b8dfc022 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,7 +23,7 @@ import {makeTaskStubFromWorkflowRecord, makeTaskStubMock} from "../activities.co
 
 export const detailState = {
     name: 'main.inspect.activities.detail',
-    url: '/:activityId',
+    url: '/:activityId?workflowId',
     template: template,
     controller: ['$scope', '$state', '$stateParams', '$log', '$uibModal', '$timeout', '$sanitize', '$sce', 'activityApi', 'entityApi', 'brUtilsGeneral', DetailController],
     controllerAs: 'vm',
@@ -34,8 +34,9 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
     const {
         applicationId,
         entityId,
-        activityId
+        activityId,
     } = $stateParams;
+    $scope.workflowId = $stateParams.workflowId;
 
     let vm = this;
     vm.model = {
@@ -50,6 +51,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
 
     vm.modalTemplate = modalTemplate;
     vm.wideKilt = false;
+    vm.actions = {};
 
     let observers = [];
 
@@ -62,8 +64,10 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
             }
             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};
+            $scope.workflowId = workflowTag.workflowId || $scope.workflowId || activityId;
+            return entityApi.getWorkflow(workflowTag.applicationId || applicationId, workflowTag.entityId || entityId, $scope.workflowId).then(wResponse => {
+                $scope.workflowId = wResponse.data.workflowId;
+                workflowTag = {applicationId, entityId, workflowId: $scope.workflowId, ...workflowTag};
                 if (optimistic) {
                     vm.model.workflow.tag = workflowTag;
                 }
@@ -99,7 +103,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                     throw error;
                 }
 
-                console.log("ERROR loading workflow " + workflowTag.workflowId, error);
+                console.log("ERROR loading workflow " + $scope.workflowId, error);
                 vm.model.workflow.loading = 'error';
             });
         };
@@ -107,7 +111,6 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
         activityApi.activity(activityId).then((response)=> {
             vm.model.activity = response.data;
 
-            vm.actions = vm.actions || {};
             delete vm.actions['effector'];
             delete vm.actions['invokeAgain'];
             if ((vm.model.activity.tags || []).find(t => t=="EFFECTOR")) {
@@ -126,8 +129,9 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 vm.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 = findWorkflowTag(vm.model.activity);
+                const workflowTag = getTaskWorkflowTag(vm.model.activity);
                 if (workflowTag) {
                     vm.model.workflow.tag = workflowTag;
                     loadWorkflow(workflowTag);
@@ -151,10 +155,10 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
             // in case it corresponds to a workflow and not a task, try loading as a workflow
 
             loadWorkflow(null).then(()=> {
-                const wft = (wf.mainTasks || []).find(t => t.taskId === activityId);
+                const wft = (vm.model.workflow.data.replays || []).find(t => t.taskId === activityId);
                 if (wft) {
-                    vm.model.activity = makeTaskStubFromWorkflowRecord(wf, wft);
-                    vm.model.workflow.tag = findWorkflowTag(vm.model.activity);
+                    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";
                 }
@@ -251,7 +255,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
             $state.go('main.inspect.activities.detail', {
                 applicationId: applicationId,
                 entityId: entityId,
-                activityId: response.data.id
+                activityId: response.data.id,
             });
         });
     }
@@ -285,7 +289,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
     vm.isNonEmpty = x => !vm.isEmpty(x);
 }
 
-function findWorkflowTag(task) {
+export function getTaskWorkflowTag(task) {
     if (!task) return null;
     if (!task.tags) return null;
     return task.tags.find(t => t.workflowId);
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 63068c45..38eb4b34 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
@@ -228,3 +228,8 @@
         margin-right: 36px;
     }
 }
+
+.dropdown-menu-replays {
+    width: auto;
+    max-width: 32em;
+}
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 fd5f5fe0..c96f9b75 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
@@ -122,7 +122,7 @@
                                             {{vm.model.activity.submitTimeUtc | timeAgoFilter}}
                                         </div>
                                         <div class="utcTime fade" ng-show="showUTC">
-                                            {{vm.model.activity.submitTimeUtc | date : 'MMM dd, yyyy @ H:mm:ss.sss'}}
+                                            {{vm.model.activity.submitTimeUtc | dateFilter }}
                                         </div>
                                     </div>
                                 </div>
@@ -133,7 +133,7 @@
                                             {{vm.model.activity.startTimeUtc | timeAgoFilter}}
                                         </div>
                                         <div class="utcTime fade" ng-show="showUTC">
-                                            {{vm.model.activity.startTimeUtc | date : 'MMM dd, yyyy @ H:mm:ss.sss'}}
+                                            {{vm.model.activity.startTimeUtc | dateFilter }}
                                         </div>
                                     </div>
                                 </div>
@@ -144,7 +144,7 @@
                                             {{vm.model.activity.endTimeUtc | timeAgoFilter}}
                                         </div>
                                         <div class="utcTime fade" ng-show="showUTC">
-                                            {{vm.model.activity.endTimeUtc | date : 'MMM dd, yyyy @ H:mm:ss.sss'}}
+                                            {{vm.model.activity.endTimeUtc | dateFilter }}
                                         </div>
                                     </div>
                                 </div>
@@ -191,12 +191,15 @@
                                         <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">
+                                        <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 }}">
-                                                <a href="" ui-sref="main.inspect.activities.detail({activityId: replay.taskId})" ng-class="{'selected' : vm.model.activityId === 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"></i>
 <!--                                                    <span class="monospace">{{ replay.taskId }}</span>-->
-                                                    {{ replay.submitTimeUtc | date : 'MMM dd, yyyy @ H:mm:ss' }} - {{ replay.reasonForReplay || '(no reason supplied)' }}
+                                                    <span ng-if="replay.reasonForReplay">{{ replay.reasonForReplay }} (</span
+                                                    ><span>{{ replay.submitTimeUtc | dateFilter: 'short' }}</span
+                                                    ><span ng-if="replay.reasonForReplay">)</span>
+
                                                 </a> </li>
                                             <li role="menuitem">
                                                 <a href="" ng-click="vm.showReplayHelp()" ng-class="{'selected' : showReplayHelp}"><i>More information</i></a>
@@ -205,14 +208,20 @@
                                     </div>
                                 </div>
                                 <div style="margin-top: 12px; margin-bottom: 24px;">
-                                    This task is for
-                                    <span ng-if="!vm.isNullish(vm.model.workflow.tag.stepIndex)">step <b>{{ vm.model.workflow.tag.stepIndex+1 }}</b>
+                                    This task is
+                                    <span ng-if="!vm.isNullish(vm.model.workflow.tag.stepIndex)">for 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})">
+                                        <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.isNullish(vm.model.workflow.tag.stepIndex)">
+                                    <span ng-if="vm.isNullish(vm.model.workflow.tag.stepIndex) && !vm.model.activity.id">
+                                        part of
+                                        workflow <span class="monospace">{{vm.model.workflow.data.workflowId}}</span>:
+                                        <b>{{vm.model.workflow.data.name}}</b>.
+                                    </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>
@@ -269,12 +278,14 @@
                         <pre>{{vm.model.activity.detailedStatus}}</pre>
                     </br-collapsible>
 
-                    <br-collapsible ng-if="vm.model.activity.detailedStatus" state="vm.model.accordion.jsonOpen">
+                    <br-collapsible ng-if="vm.model.activity.id || vm.model.workflow.data" state="vm.model.accordion.jsonOpen">
                         <heading> JSON</heading>
                         <b>Activity</b>
                         <pre>{{vm.stringify(vm.model.activity)}}</pre>
-                        <b>Workflow</b>
-                        <pre>{{vm.stringify(vm.model.workflow)}}</pre>
+                        <div ng-if="vm.model.workflow.data">
+                            <b>Workflow</b>
+                            <pre>{{vm.stringify(vm.model.workflow)}}</pre>
+                        </div>
                     </br-collapsible>
 
                     <div>
diff --git a/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html b/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html
index cf0a4cd1..f563735d 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html
+++ b/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html
@@ -87,7 +87,7 @@
                                 {{highlight.time > 0 ? (highlight.time | timeAgo) : 'ongoing'}}
                             </span>
                             <span class="utcTime fade" ng-show="showUTC">
-                                {{highlight.time > 0 ? (highlight.time | date : 'MMM dd, yyyy @ H:mm:ss.sss') : 'ongoing'}}
+                                {{highlight.time > 0 ? (highlight.time | dateFilter ) : 'ongoing'}}
                             </span>
                         </div>
                     </div>
diff --git a/ui-modules/home/app/views/about/about.controller.js b/ui-modules/home/app/views/about/about.controller.js
index 727da541..a5ab425e 100644
--- a/ui-modules/home/app/views/about/about.controller.js
+++ b/ui-modules/home/app/views/about/about.controller.js
@@ -251,6 +251,8 @@ export function aboutStateController($scope, $rootScope, $element, $q, $uibModal
 
 export function timeAgoFilter() {
     return function (input) {
+        if (!input || input<=0) return '-';
+
         if (input) {
             return fromNow(input);
         }


[brooklyn-ui] 23/24: improve task filters - subtasks, scheduled, initialization

Posted by he...@apache.org.
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 3c2649e46bca57d4dc8536c5787c5699fedfbeb8
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 20 13:33:21 2022 +0100

    improve task filters - subtasks, scheduled, initialization
---
 .../components/task-list/task-list.directive.js    | 70 +++++++++++++++++++---
 .../app/components/task-list/task-list.less        | 35 ++++++++---
 .../components/task-list/task-list.template.html   |  8 +--
 .../components/workflow/workflow-step.directive.js |  4 ++
 .../workflow/workflow-step.template.html           |  5 +-
 .../app/components/workflow/workflow-steps.less    |  5 ++
 .../inspect/activities/activities.controller.js    |  8 +++
 .../app/views/main/inspect/inspect.controller.js   |  2 +-
 8 files changed, 113 insertions(+), 24 deletions(-)

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 5f12c8a4..4e0d754e 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
@@ -157,15 +157,25 @@ export function taskListDirective() {
             if ($scope.taskType) {
                 selectFilter($scope.taskType);
             } else {
-                // defaults
-                selectFilter('EFFECTOR');
-                selectFilter('WORKFLOW');
+                if (!isActivityChildren) {
+                    // defaults (when not in subtask view; in subtask view it is as above)
+                    selectFilter('EFFECTOR');
+                    selectFilter('WORKFLOW');
+                } else {
+                    selectFilter('SUB-TASK');
+                }
             }
             selectFilter("_workflowReplayedTopLevel");
             selectFilter("_workflowNonLastReplayHidden");
 
+            // pick other filter combos until we get some conetnt
+            if ($scope.tasksFilteredByTag.length==0) {
+                selectFilter('INITIALIZATION');
+            }
+            if ($scope.tasksFilteredByTag.length==0) {
+                selectFilter("_anyTypeTag", true);
+            }
             if ($scope.tasksFilteredByTag.length==0) {
-                // if nothing found at top level then broaden
                 selectFilter("_top", false);
             }
 
@@ -215,6 +225,8 @@ export function taskListDirective() {
                         globalFilters.transient.checked = !globalFilters.transient.include;
                         setFiltersForTasks(scope, isActivityChildren);
                     },
+                    category: 'status',
+                    categoryForEvaluation: 'status-transient',
                 };
                 globalFilters.transient.action();
             }
@@ -369,9 +381,18 @@ export function taskListDirective() {
                 ...(extra || {}),
             }
         }
-        // put these first
-        addTagFilter('EFFECTOR', filtersFullList, 'Effectors', { displaySummary: 'effector', includeIfZero: true });
-        addTagFilter('WORKFLOW', filtersFullList, 'Workflow', { includeIfZero: true });
+
+        // put these first if present, to get this order, then remove if false
+        if (!isActivityChildren) {
+            addTagFilter('EFFECTOR', filtersFullList, 'Effectors', {displaySummary: 'effector', includeIfZero: true});
+            addTagFilter('WORKFLOW', filtersFullList, 'Workflow', { includeIfZero: true });
+        } else {
+            filtersFullList['EFFECTOR'] = false;
+            filtersFullList['WORKFLOW'] = false;
+        }
+        filtersFullList['SENSOR'] = false;
+        filtersFullList['INITIALIZATION'] = false;
+        filtersFullList['SUB-TASK'] = false;
 
         // add filters for other tags
         tasks.forEach(t =>
@@ -379,6 +400,31 @@ export function taskListDirective() {
                     addTagFilter(tag, filtersFullList, 'Tag: ' + tag.toLowerCase())
             ));
 
+        ['EFFECTOR', 'WORKFLOW', 'SUB-TASK', 'SENSORS', 'INITIALIZATION'].forEach(t => { if (!filtersFullList[t]) delete filtersFullList[t]; });
+        (filtersFullList['SUB-TASK'] || {}).display = 'Sub-tasks';
+        (filtersFullList['SENSOR'] || {}).display = 'Sensors';
+        (filtersFullList['INITIALIZATION'] || {}).display = 'Initialization';
+
+        filtersFullList['_active'] = {
+            display: 'Only show active tasks',
+            displaySummary: 'active',
+            filter: tasks => tasks.filter(t => !t.endTimeUtc || t.endTimeUtc<0),
+            category: 'status',
+            categoryForEvaluation: 'status-active',
+        }
+        filtersFullList['_scheduled_sub'] = {
+            display: 'Only show scheduled tasks',
+            displaySummary: 'scheduled',
+            filter: tasks => tasks.filter(t => {
+                // show scheduled tasks (the parent) and each scheduled run, if sub-tasks are selected
+                if (!t || !t.submittedByTask) return false;
+                if (isScheduled(t)) return true;
+                let submitter = tasksById[t.submittedByTask.metadata.id];
+                return isScheduled(submitter);
+            }),
+            category: 'status',
+            categoryForEvaluation: 'status-scheduled',
+        }
 
         const filterWorkflowsReplayedTopLevel = t => !t.isWorkflowFirstRun && t.isWorkflowLastRun && t.isWorkflowTopLevel;
         const countWorkflowsReplayedTopLevel = tasksAll.filter(filterWorkflowsReplayedTopLevel).length;
@@ -525,8 +571,14 @@ function isTopLevelTask(t, tasksById) {
     if (t.forceTopLevel) return true;
     if (t.tags && t.tags.includes("TOP-LEVEL")) return true;
     let submitter = tasksById[t.submittedByTask.metadata.id];
-    if (!submitter) return true;
-    if (isScheduled(submitter) && (!t.endTimeUtc || t.endTimeUtc<=0)) return true;
+
+    // we could include those which are submitted but the submitter is forgotten
+    // (but they are accesible as CrossEntity or NestedSameEntity so don't include for now)
+    //if (!submitted) return true;
+
+    // active scheduled tasks
+    //if (isScheduled(submitter) && (!t.endTimeUtc || t.endTimeUtc<=0)) return true;
+
     return false;
 }
 function isNonTopLevelTask(t, tasksById) {
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.less b/ui-modules/app-inspector/app/components/task-list/task-list.less
index a111b031..19a3db97 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.less
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.less
@@ -20,17 +20,18 @@ task-list {
   display: block;
   position: relative;
 
-  i.sub-task-link {
-    font-size: 40px;
-    color: @brand-primary;
-  }
-
   brooklyn-status-icon {
     margin-right: 15px;
     width: 30px;
     height: 30px;
     display: block;
   }
+
+  i.sub-task-link {
+    font-size: 40px;
+    color: @brand-primary;
+  }
+
   .custom-status-task-icon {
     color: @brand-success;
   }
@@ -213,15 +214,33 @@ task-list {
 
     }
 
-    &.status a {
-      padding-top: 0;
-      padding-bottom: 0;
+    &.status {
+      a {
+        padding-top: 0;
+        padding-bottom: 0;
+
+        display: flex;
+        justify-content: center;
+
+        brooklyn-status-icon {
+          margin: 0;
+          width: 24px;
+          height: 24px;
+        }
+      }
     }
+
+    &.no-tasks-found {
+      padding: 24px;
+      font-size: 125%;
+    }
+
   }
 
   tr > td > a:hover {
     text-decoration: none;
   }
+
 }
 
 .no-activities {
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.template.html b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
index 02679002..76cd8c5f 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.template.html
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
@@ -77,7 +77,7 @@
         <table class="table table-bordered">
             <thead>
             <tr>
-                <th>
+                <th style="text-align: center;">
                     Status
                 </th>
                 <th>
@@ -125,11 +125,11 @@
             </tr>
             {{ model.filterResult = filterResult; "comment: update those interested in the result of the filter"; "" }}
             <tr ng-if="filterResult.length == 0 ">
-                <td colspan="4" class="text-center"><h4>
+                <td colspan="4" class="text-center no-tasks-found">
                     No tasks found matching
                     <span ng-if="filterValue">current search <code>{{filterValue}}</code> and</span>
-                    filter <code>{{filters.selectedDisplayName}}</code>
-                </h4></td>
+                    filter options
+                </td>
             </tr>
             </tbody>
         </table>
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 1171d247..39bd3a64 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
@@ -90,6 +90,10 @@ export function workflowStepDirective() {
                     $scope.stepTitle.code = step.shorthandTypeName || step.type || '';
                     if (step.input) $scope.stepTitle.code += ' ...';
                 }
+                if ("workflow" === $scope.stepTitle.code) {
+                    $scope.stepTitle.code = null;
+                    $scope.stepTitle.leftExtra = "nested workflow";
+                }
 
                 if (step.name) {
                     $scope.stepTitle.name = step.name;
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 97a51d61..4480b0c7 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
@@ -52,7 +52,8 @@
 
         <div class="step-block-title">
             <span class="step-index">{{ stepTitle.index }}</span>
-            <span class="step-title-code">{{ stepTitle.code }}</span>
+            <span ng-if="stepTitle.code" class="step-title-code">{{ stepTitle.code }}</span>
+            <span ng-if="stepTitle.leftExtra" class="step-left-extra">{{ stepTitle.leftExtra}}</span>
         </div>
 
         <div ng-if="expanded" class="step-details">
@@ -128,7 +129,7 @@
 
                 <div class="more-space-above" ng-if="stepContext.taskId">
                     <div class="data-row">
-                        <div class="A"><span ng-if="isCurrentAndActive">CURRENT</span><span ng-if="!isCurrent">LAST</span> EXECUTION</div>
+                        <div class="A"><span ng-if="isCurrentAndActive">CURRENT</span><span ng-if="!isCurrentAndActive">LAST</span> EXECUTION</div>
                         <div class="B">
                                     <span ng-if="isFocusTask">
                                         task <span class="monospace">{{ stepContext.taskId }}</span>
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 9948ee90..e1fda49f 100644
--- a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
@@ -90,6 +90,11 @@
         font-weight: 300;
         color: @gray-light;
       }
+      .step-left-extra {
+        font-size: 90%;
+        //font-weight: 300;
+        color: @gray-light;
+      }
     }
 
     .step-details {
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index b005142f..2ec31d57 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -195,5 +195,13 @@ export function makeTaskStubFromWorkflowRecord(wf, wft) {
             },
         ],
     };
+    if (wft.submittedByTaskId) {
+        result.submittedByTask = {
+            metadata: {
+                id: wft.submittedByTaskId,
+                entityId: result.entityId,
+            }
+        };
+    }
     return result;
 };
diff --git a/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js b/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
index 04808be3..636825e9 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
@@ -153,7 +153,7 @@ export function runWorkflowController($scope, $http, $uibModalInstance, applicat
     function runWorkflow() {
         $scope.running = true;
         $scope.errorMessage = null;
-        $http.post('/v1/applications/' + applicationId + '/entities/' + entityId + '/workflow?start=true&timeout=20ms', $scope.workflowYaml)
+        $http.post('/v1/applications/' + applicationId + '/entities/' + entityId + '/workflows?start=true&timeout=20ms', $scope.workflowYaml)
             .then((response)=> {
                 $scope.running = false;
                 $uibModalInstance.close(response.data);


[brooklyn-ui] 11/24: align effectors in table better

Posted by he...@apache.org.
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 73eb80336fd2f932e0dc1fe796e6ea6c1237aa6a
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 6 15:20:54 2022 +0100

    align effectors in table better
---
 .../entity-effector/entity-effector.html           | 18 ++++----
 .../entity-effector/entity-effector.less           | 50 +++++++++++-----------
 2 files changed, 35 insertions(+), 33 deletions(-)

diff --git a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.html b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.html
index 1c92f102..f29a7843 100644
--- a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.html
+++ b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.html
@@ -28,17 +28,23 @@
             </br-button>
         </div>
         <div class="row-contents">
+            <div class="effector-name">
+                {{effector.name}}
+            </div>
+            <div class="effector-description" ng-if="effector.description">{{effector.description}}</div>
+        </div>
+        <div class="row-tail">
             <a ui-sref="main.inspect.activities({applicationId: applicationId, entityId: entityId, filter: 'EFFECTOR', search: effector.name})"
-                    ng-if="activities && activitiesSummary()" title="{{activitiesSummary()}}"><span class="pull-right effector-pills">
+                ng-if="activities && activitiesSummary()" title="{{activitiesSummary()}}"><span class="effector-pills">
                 <div class="effector-pill effector-active" ng-if="activities.active">
                     <i class="fa fa-play-circle"></i>
-<!--                    <i class="fa fa-dot-circle-o"></i></div>-->
+                    <!--                    <i class="fa fa-dot-circle-o"></i></div>-->
                     {{activities.active}}
                 </div>
                 <div class="effector-pill effector-cancelled" ng-if="activities.cancelled">
 <!--                    <i class="fa fa-pause-circle"></i></div>-->
-<!--                    <i class="fa fa-exclamation-circle"></i></div>-->
-<!--                    <i class="fa fa-minus-circle"></i></div>-->
+                    <!--                    <i class="fa fa-exclamation-circle"></i></div>-->
+                    <!--                    <i class="fa fa-minus-circle"></i></div>-->
                     <i class="fa fa-stop-circle"></i>
                     {{activities.cancelled}}
                 </div>
@@ -51,10 +57,6 @@
                     {{activities.succeeded}}
                 </div>
             </span></a>
-            <span class="effector-name">
-                {{effector.name}}
-            </span>
-            <span class="effector-description">{{effector.description}}</span>
         </div>
     </div>
 </div>
diff --git a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
index dd6eaf97..421dae54 100644
--- a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
+++ b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
@@ -23,7 +23,6 @@
         font-size: 18px;
         color: @brand-primary;
         display: block;
-        margin-top: -5px;
 
         overflow: hidden;
         text-overflow: ellipsis;
@@ -32,6 +31,7 @@
     .effector-description {
         font-size: 12px;
         color: #7F7F7F;
+        margin-top: 4px;
     }
 
     border-top: 1px solid #E1E1E1;
@@ -56,37 +56,37 @@
         margin-right: 12px;
         gap: 12px;
     }
-    .row-buttons {
+    .row-buttons, .row-tail {
         align-items: center;
         display: flex;
     }
     .row-contents {
         flex: 1 1 auto;
-        .effector-pills {
-            display: flex;
-            &:hover {
-                .effector-pill {
-                    background: mix(@gray-light, @gray-lighter, 30%);
-                }
-            }
+    }
+    .effector-pills {
+        display: flex;
+        &:hover {
             .effector-pill {
-                background: @gray-lighter;
-                padding: 2px 6px;
-                &:first-child {
-                    padding-left: 10px;
-                    border-bottom-left-radius: 12px;
-                    border-top-left-radius: 12px;
-                }
-                &:last-child {
-                    padding-right: 10px;
-                    border-bottom-right-radius: 12px;
-                    border-top-right-radius: 12px;
-                }
+                background: mix(@gray-light, @gray-lighter, 30%);
+            }
+        }
+        .effector-pill {
+            background: @gray-lighter;
+            padding: 2px 6px;
+            &:first-child {
+                padding-left: 10px;
+                border-bottom-left-radius: 12px;
+                border-top-left-radius: 12px;
+            }
+            &:last-child {
+                padding-right: 10px;
+                border-bottom-right-radius: 12px;
+                border-top-right-radius: 12px;
             }
-            .effector-succeeded { color: @color-succeeded; }
-            .effector-failed { color: @color-failed; }
-            .effector-cancelled { color: @color-cancelled; }
-            .effector-active { color: @color-active; }
         }
+        .effector-succeeded { color: @color-succeeded; }
+        .effector-failed { color: @color-failed; }
+        .effector-cancelled { color: @color-cancelled; }
+        .effector-active { color: @color-active; }
     }
 }
\ No newline at end of file


[brooklyn-ui] 16/24: minor fixes for workflow arrows and indicators

Posted by he...@apache.org.
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 afd62cf2fc787af1267931e106aaae66aaf36aee
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Mon Oct 10 11:57:14 2022 +0100

    minor fixes for workflow arrows and indicators
---
 .../components/workflow/workflow-step.directive.js |  8 +++++++
 .../workflow/workflow-step.template.html           | 26 ++++++++--------------
 .../workflow/workflow-steps.directive.js           |  6 ++++-
 3 files changed, 22 insertions(+), 18 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 7d6bb40c..dadf25a7 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
@@ -114,6 +114,14 @@ export function workflowStepDirective() {
                 $scope.isFocusStep = $scope.workflow.tag && ($scope.workflow.tag.stepIndex === index);
                 $scope.isFocusTask = false;
 
+                $scope.stepCurrentError = (($scope.task || {}).currentStatus === 'Error') ? 'This step returned an error.'
+                    : ($scope.isWorkflowError && $scope.isCurrentMaybeInactive) ? 'The workflow encountered an error around this step.'
+                    : null;
+                const incomplete = $scope.osi.countStarted - $scope.osi.countCompleted > ($scope.isCurrentAndActive ? 1 : 0);
+                $scope.stepCurrentWarning = incomplete ? 'This step has previously been interrupted.' : null;
+                $scope.stepCurrentSuccess = (!$scope.isCurrentAndActive && !incomplete && $scope.osi.countCompleted > 0)
+                    ? 'This step has completed without errors.' : null;
+
                 if ($scope.task) {
                     if (!vm.isNullish($scope.stepContext.taskId) && $scope.stepContext.taskId === $scope.task.id) {
                         $scope.isFocusTask = true;
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 57cb6d71..5ec2a31c 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
@@ -19,25 +19,17 @@
 <div class="workflow-step-outer">
 
     <div class="workflow-step-status-indicators">
-        <span ng-if="isCurrentAndActive">
-            <span ng-if="isRunning" class="running-status">
-                <brooklyn-status-icon value="STARTING"></brooklyn-status-icon>
-            </span>
+        <span ng-if="isCurrentAndActive" class="running-status">
+            <brooklyn-status-icon value="STARTING"></brooklyn-status-icon>
         </span>
-
-        <span ng-if="osi.countCompleted && osi.countStarted === osi.countStarted">
-            <span class="color-succeeded">
-                <i class="fa fa-check-circle"></i>
-            </span>
-<!--            <span ng-if="osi.countCompleted > 1">{{ osi.countCompleted }}</span>-->
+        <span ng-if="stepCurrentSuccess" class="color-succeeded" title="{{ stepCurrentSuccess }}">
+            <i class="fa fa-check-circle"></i>
+        </span>
+        <span ng-if="stepCurrentWarning" class="color-cancelled" title="{{ stepCurrentWarning }}">
+            <i class="fa fa-exclamation-circle"></i>
         </span>
-        <span ng-if="osi.countStarted && osi.countStarted != osi.countCompleted && !isCurrentAndActive">
-            <span class="color-failed" ng-if="isWorkflowError">
-                <i class="fa fa-times-circle"></i>
-            </span>
-            <span class="color-cancelled" ng-if="!isWorkflowError">
-                <i class="fa fa-exclamation-circle"></i>
-            </span>
+        <span ng-if="stepCurrentError" class="color-failed" title="{{ stepCurrentError }}">
+            <i class="fa fa-times-circle"></i>
         </span>
     </div>
 
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 32ca366e..c808f1f8 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
@@ -222,7 +222,11 @@ function makeArrows(workflow, steps) {
             if (to!=-1 && from!=-1 && to!=from) {
                 jumpSizes[Math.abs(from-to)] = true;
             }
-            arrowSpecs[[from,to]] = { from, to, ...(opts||{}) };
+            if (arrowSpecs[[from,to]]) {
+                // prefer earlier additions (real steps) over theoretical ones
+            } else {
+                arrowSpecs[[from, to]] = {from, to, ...(opts || {})};
+            }
         }
 
         for (var i = -1; i < steps.length - 1; i++) {


[brooklyn-ui] 21/24: tweaks to messages for clarity

Posted by he...@apache.org.
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 21e2f2eccdb2a67125e1a98c48ff1f3baa1ac2c0
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 20 09:43:03 2022 +0100

    tweaks to messages for clarity
---
 .../app/components/workflow/workflow-step.directive.js           | 2 +-
 .../app/components/workflow/workflow-step.template.html          | 9 +++++----
 .../app/views/main/inspect/activities/activities.controller.js   | 1 +
 .../views/main/inspect/activities/detail/detail.controller.js    | 2 ++
 .../views/main/inspect/activities/detail/detail.template.html    | 9 +++++++--
 5 files changed, 16 insertions(+), 7 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 7cc22544..1171d247 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
@@ -87,7 +87,7 @@ export function workflowStepDirective() {
                 let shorthand = step.userSuppliedShorthand || step.s || step.shorthand;
                 $scope.stepTitle.code = shorthand;
                 if (!shorthand) {
-                    $scope.stepTitle.code = step.type || '';
+                    $scope.stepTitle.code = step.shorthandTypeName || step.type || '';
                     if (step.input) $scope.stepTitle.code += ' ...';
                 }
 
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 0554213c..97a51d61 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
@@ -38,13 +38,13 @@
             <div ng-if="stepTitle.name" class="step-name">
                 {{ stepTitle.name}}
             </div>
+            <div ng-if="isFocusTask" class="workflow-step-pill focus-step label-info" title="This step instance is for the task currently selected in the activity view.">
+                selected
+            </div>
             <div ng-if="stepTitle.id" class="workflow-step-pill step-id">
                 <i class="fa fa-id-card-o"></i>
                 {{ stepTitle.id }}
             </div>
-            <div ng-if="isFocusTask" class="workflow-step-pill focus-step label-info" title="This step instance is for the task currently selected in the activity view.">
-                selected
-            </div>
             <div ng-click="vm.toggleExpandState()" class="expand-toggle">
                 <i ng-class="expanded ? 'fa fa-chevron-up' : 'fa fa-chevron-down'"></i>
             </div>
@@ -100,7 +100,8 @@
                         </span>
 
                         <span ng-if="isFocusTask">
-                            in the task focused on in this page (<span class="monospace">{{ stepContext.taskId }}</span>).
+                            in the task this page is focused on (<span class="monospace">{{ stepContext.taskId }}</span>).
+                            More details may be found in the other sections on this page.
                         </span>
                         <span ng-if="!isFocusTask">
                             in <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId, workflowId })">task <span class="monospace">{{ stepContext.taskId }}</span></a>.
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index b005142f..dbb487e3 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -176,6 +176,7 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
 }
 
 export function makeTaskStubFromWorkflowRecord(wf, wft) {
+    console.log("WFT", wft.status, wft);
     const result = {
         id: wft.taskId,
         displayName: wf.name + (wft.reasonForReplay ? " ("+wft.reasonForReplay+")" : ""),
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 1e7aedba..c994e19a 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
@@ -114,6 +114,8 @@ function DetailController($scope, $state, $stateParams, $location, $log, $uibMod
                     // change the workflow object so widgets get refreshed
                     vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data };
 
+                    vm.model.workflow.isError = !!(vm.model.workflow.data.status && vm.model.workflow.data.status.startsWith("ERROR"));
+
                     const replays = (vm.model.workflow.data.replays || []);
 
                     vm.model.workflow.runMultipleTimes = replays.length > 1;
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 3916a9eb..74cdf90b 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
@@ -214,7 +214,7 @@
                                     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.runMultipleTimes">
                                             <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>
@@ -236,10 +236,15 @@
                                                 <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>
+                                            <span ng-if="vm.model.workflow.runIsLatest && vm.model.workflow.runMultipleTimes">the most recent run 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.isError && vm.model.workflow.data.currentStepInstance && vm.model.workflow.data.currentStepInstance.taskId">
+                                            Details of the failure can be found by opening
+                                            <a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.data.currentStepInstance.taskId, workflowId})">
+                                                the failed task</a>.
+                                        </span>
                                     </span>
                                 </div>
 


[brooklyn-ui] 14/24: improve dropdowns, code, extend logic for workflows

Posted by he...@apache.org.
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 8d2f46689d6700247c93117b9bc0942761109334
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Oct 7 21:27:24 2022 +0100

    improve dropdowns, code, extend logic for workflows
---
 .../components/task-list/task-list.directive.js    | 415 ++++++++++-----------
 .../app/components/task-list/task-list.less        |  26 +-
 .../components/task-list/task-list.template.html   |  29 +-
 .../inspect/activities/activities.controller.js    |  36 +-
 .../inspect/activities/activities.template.html    |   2 +-
 .../inspect/activities/detail/detail.template.html |   2 +-
 .../inspect/management/detail/detail.template.html |   2 +-
 ui-modules/utils/br-core/style/variables.less      |   1 +
 8 files changed, 263 insertions(+), 250 deletions(-)

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 e890a283..db0d30c1 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
@@ -39,7 +39,9 @@ export function taskListDirective() {
         restrict: 'E',
         scope: {
             tasks: '=',
-            taskType: '@',
+            tasksLoaded: '<?',  // if tasks might complete initial loading late, the caller should pass a watchable expression that resolves to true when initially loaded
+            taskType: '@?',
+            parentTaskId: '@?',
             filteredCallback: '&?',
             search: '<',
         },
@@ -47,14 +49,15 @@ export function taskListDirective() {
     };
 
     function controller($scope, $element) {
-        const isActivityChildren = $scope.taskType === 'activityChildren';
+        const isActivityChildren = !! $scope.parentTaskId;
 
+        // selected filters are shared with other views esp kilt view so they can see what is and isn't included.
+        // currently only used for transient.
         $scope.globalFilters = {
             // transient set when those tags seen
         };
 
         $scope.isEmpty = x => _.isNil(x) || x.length==0 || (typeof x === "object" && Object.keys(x).length==0);
-        $scope.filters = { available: {}, selectedFilters: {}, selectedIds: {} };
         $scope.model = {
             appendTo: $element,
             filterResult: null,
@@ -65,13 +68,13 @@ export function taskListDirective() {
             let result = tasks || [];
 
             if (selected) {
-                _.uniq(Object.values(selected).map(f => f.category)).forEach(category => {
+                _.uniq(Object.values(selected).map(f => f.categoryForEvaluation || f.category)).forEach(category => {
                     if (categoryToExclude === '' || categoryToExclude != category) {
                         let newResult = [];
                         if ($scope.filters.startingSetFilterForCategory[category]) {
                             newResult = $scope.filters.startingSetFilterForCategory[category](result);
                         }
-                        Object.values(selected).filter(f => f.category === category).forEach(f => {
+                        Object.values(selected).filter(f => (f.categoryForEvaluation || f.category) === category).forEach(f => {
                             const filter = f.filter;
                             if (!filter) {
                                 console.warn("Incomplete activities tag filter", tagF);
@@ -98,31 +101,21 @@ export function taskListDirective() {
 
             // now update name
             const enabledCategories = _.uniq(Object.values($scope.filters.selectedFilters).map(f => f.category));
-            let filterNameParts = Object.entries($scope.filters.displayNameForCategory).map(([category, nameFn]) => {
+            $scope.filters.selectedDisplay = [];
+            Object.entries($scope.filters.displayNameFunctionForCategory).forEach(([category, nameFn]) => {
                 if (!enabledCategories.includes(category)) return null;
-                let nf = $scope.filters.displayNameForCategory[category];
-                return nf ? nf(Object.values($scope.filters.selectedFilters).filter(f => f.category === category)) : null;
-            }).filter(x => x);
-            $scope.filters.selectedDisplayName = filterNameParts.length ? filterNameParts.join('; ') :
-                isActivityChildren ? 'all sub-tasks' : 'all tasks';
+                let nf = $scope.filters.displayNameFunctionForCategory[category];
+                let badges = nf ? nf(Object.values($scope.filters.selectedFilters).filter(f => (f.categoryForBadges || f.category) === category)) : null;
+                badges = (badges || []).filter(x=>x);
+                if (badges.length) $scope.filters.selectedDisplay.push({ class: 'dropdown-category-'+category, badges });
+            });
+            if (!$scope.filters.selectedDisplay.length) $scope.filters.selectedDisplay.push({ class: 'dropdown-category-default', badges: ['all'] });
         };
 
         function selectFilter(filterId, state) {
-            // annoying, but since task list is live updated, we store the last value of selectedIds in the event filters come and go;
-            // mainly tried because initial order could be too strange, but now we correct that, so this isn't so important
-            let oldTheoreticalEnablement = $scope.filters.selectedIds[filterId];
-
             const f = $scope.filters.available[filterId];
             if (!f) {
-                console.log("FILTER "+filterId+" not available yet, storing theoretical enablement");
-
-                if (!_.isNil(state) ? state : !oldTheoreticalEnablement) {
-                    $scope.filters.selectedIds[filterId] = 'theoretically-enabled';
-                } else {
-                    delete $scope.filters.selectedIds[filterId];
-                }
-
-                // we tried to select eg effector, when it didn't exist
+                // we tried to select eg effector, when it didn't exist, just ignore
                 return false;
             } else {
                 f.select(filterId, f, state);
@@ -130,50 +123,54 @@ export function taskListDirective() {
             }
         }
 
-        setFiltersForTasks($scope, isActivityChildren);
         $scope.filterValue = $scope.search;
 
-        selectFilter("_top", true);
-        selectFilter("_anyTypeTag", true);
-        if ($scope.taskType === 'activity') {
-            // default?
-            selectFilter('EFFECTOR');
-            selectFilter('WORKFLOW');
-        } else if ($scope.taskType) {
-            selectFilter($scope.taskType);
-        } else {
-            // TODO when is this called?
-            selectFilter('EFFECTOR');
-            selectFilter('WORKFLOW');
-        }
-
-        cacheSelectedIdsFromFilters($scope);
-        selectFilter("_workflowReplayed");
-        selectFilter("_workflowNonLastReplayHidden");
+        $scope.isScheduled = isScheduled;
 
-        console.log($scope.filters);
+        $scope.getTaskDuration = function(task) {
+            if (!task.startTimeUtc) {
+                return null;
+            }
+            if (!_.isNil(task.endTimeUtc) && task.endTimeUtc <= 0) return null;
+            return (task.endTimeUtc === null ? new Date().getTime() : task.endTimeUtc) - task.startTimeUtc;
+        }
+        $scope.getTaskWorkflowId = task => {
+            const tag = getTaskWorkflowTag(task);
+            if (tag) return tag.workflowId;
+            return null;
+        };
 
-        // // this would be nice, but it doesn't play nice with dynamic task updates
-        // // sometimes no tasks are loaded yet and this enables the "all" but then tasks get loaded
-        // if ($scope.tasksFilteredByTag.length==0) {
-        //     // if nothing found at top level then broaden
-        //     selectFilter("_top", false);
-        // }
+        $scope.$watch('model.filterResult', function () {
+            if ($scope.filteredCallback && $scope.model.filterResult) $scope.filteredCallback()( $scope.model.filterResult, $scope.globalFilters );
+        });
 
+        let tasksLoadedTrueReceived = false;
 
+        function refreshDropdownsUntilTasksAreLoaded() {
+            if (tasksLoadedTrueReceived || $scope.uiDropdownInteraction) return;
+            tasksLoadedTrueReceived = $scope.tasksLoaded;
 
-        // TODO check taskType=activity...  .... can they not all just leave it off, to send the default; send the default?
-        // and make sure others send EFFECTOR
+            $scope.filters = { available: {}, selectedFilters: {} };
+            setFiltersForTasks($scope, isActivityChildren);
+            selectFilter("_top", true);
+            selectFilter("_anyTypeTag", true);
+            if ($scope.taskType) {
+                selectFilter($scope.taskType);
+            } else {
+                // defaults
+                selectFilter('EFFECTOR');
+                selectFilter('WORKFLOW');
+            }
+            selectFilter("_workflowReplayedTopLevel");
+            selectFilter("_workflowNonLastReplayHidden");
 
-        // if ((!$scope.taskType || $scope.taskType.startsWith('activity')) && (!filterPreselected || $scope.tasksFilteredByTag.length==0 )) {
-        //     // if nothing found with filters, try disabling the filters
-        //     filterPreselected = selectFilter('_top', false);
-        //     if (!filterPreselected || $scope.tasksFilteredByTag.length == 0 ) {
-        //         selectFilter('_top', true);
-        //     }
-        // }
+            if ($scope.tasksFilteredByTag.length==0) {
+                // if nothing found at top level then broaden
+                selectFilter("_top", false);
+            }
 
-        $scope.isScheduled = isScheduled;
+            $scope.recomputeTasks();
+        }
 
         $scope.$watch('tasks', ()=>{
             $scope.recomputeTasks();
@@ -182,24 +179,10 @@ export function taskListDirective() {
             $scope.recomputeTasks();
         });
 
-        $scope.getTaskDuration = function(task) {
-            if (!task.startTimeUtc) {
-                return null;
-            }
-            if (!_.isNil(task.endTimeUtc) && task.endTimeUtc <= 0) return null;
-            return (task.endTimeUtc === null ? new Date().getTime() : task.endTimeUtc) - task.startTimeUtc;
-        }
-
-        $scope.$watch('model.filterResult', function () {
-            if ($scope.filteredCallback && $scope.model.filterResult) $scope.filteredCallback()( $scope.model.filterResult, $scope.globalFilters );
+        $scope.$watch('tasksLoaded', v => {
+            refreshDropdownsUntilTasksAreLoaded();
         });
-        $scope.getTaskWorkflowId = task => {
-            const tag = getTaskWorkflowTag(task);
-            if (tag) return tag.workflowId;
-            return null;
-        };
-
-        $scope.recomputeTasks();
+        refreshDropdownsUntilTasksAreLoaded();
     }
 
     function setFiltersForTasks(scope, isActivityChildren) {
@@ -213,9 +196,23 @@ export function taskListDirective() {
                 // only default to filtering transient if some but not all are transient
                 globalFilters.transient = {
                     include: true,
+                    checked: false,
+                    display: 'Exclude transient tasks',
+                    help: 'Routine, low-level, usually uninteresting tasks are tagged as TRANSIENT so they can be easily ignored' +
+                        'to simplify display and preserve memory for more interesting tasks. ' +
+                        'These are by default excluded from this view. ' +
+                        'They can be included by de-selecting this option. ' +
+                        'Note that transient tasks may be cleared from memory very quickly when they are completed ' +
+                        'and can subsequently give warnings in this UI.',
+                    filter: inputs => inputs.filter(t => !isTaskWithTag(t, 'TRANSIENT')),
+                    onClick: ()=> {
+                        globalFilters.transient.action();
+                        // need to recompute as the filters are changed now
+                        scope.recomputeTasks();
+                    },
                     action: ()=>{
                         globalFilters.transient.include = !globalFilters.transient.include;
-                        globalFilters.transient.display = (globalFilters.transient.include ? 'Hide' : 'Show') + ' transient tasks';
+                        globalFilters.transient.checked = !globalFilters.transient.include;
                         setFiltersForTasks(scope, isActivityChildren);
                     },
                 };
@@ -226,46 +223,21 @@ export function taskListDirective() {
         const tasks = tasksAfterGlobalFilters(tasksAll, globalFilters);
 
         function defaultToggleFilter(tag, value, forceValue, fromUi, skipRecompute) {
-            if ((scope.filters.selectedIds[tag] && _.isNil(forceValue)) || forceValue===false) {
-                delete scope.filters.selectedIds[tag];
+            if ((scope.filters.selectedFilters[tag] && _.isNil(forceValue)) || forceValue===false) {
                 delete scope.filters.selectedFilters[tag];
                 if (value.onDisabledPost) value.onDisabledPost(tag, value, forceValue);
             } else {
                 if (value.onEnabledPre) value.onEnabledPre(tag, value, forceValue);
-                scope.filters.selectedIds[tag] = 'enabled';
                 scope.filters.selectedFilters[tag] = value;
             }
             if (fromUi) {
                 // on a UI click, don't try to be too clever about remembered IDs
-                cacheSelectedIdsFromFilters(scope);
+                scope.uiDropdownInteraction = true;
             }
 
             if (!skipRecompute) scope.recomputeTasks();
         }
 
-        /*
-          MENU should look like following, with group-specific behaviour for filtering and enablement,
-          e.g. auto-enable all of first group if only is de-selected:
-
-          Only show top-level tasks
-        x Show tasks called from other entities
-                submittedByTask==null ||
-                submittedByTask.metadata.entityId != entityId
-          Show tasks nested within this entity
-          ---
-          Any task type/tag
-        x Effector calls
-        x Workflows
-          Tag: tag1
-          Tag: tag2
-
-
-TODO workflow ui
-          ? most recent run of workflow only
-             combine others under last if loaded
-          ? show individual workflows resumed on startup! ? label as top-level?
-         */
-
         function clearCategory(category) {
             return function(filterId, filter, forceValue) {
                 Object.entries(scope.filters.selectedFilters).forEach( ([k,v])=> {
@@ -302,7 +274,7 @@ TODO workflow ui
             }
         }
 
-        const defaultFilters = {};
+        const filtersFullList = {};
 
         let tasksById = tasksAll.reduce( (result,t) => { result[t.id] = t; return result; }, {} );
         function filterTopLevelTasks(tasks) { return filterWithId(tasks, tasksById, isTopLevelTask); }
@@ -316,35 +288,33 @@ TODO workflow ui
         function getFilterOrEmpty(id) {
             return id && (id.filter ? id : scope.filters.available[id]) || {};
         }
-        scope.filters.displayNameForCategory = {
+        scope.filters.displayNameFunctionForCategory = {
             nested: set => {
                 if (!set || !set.length) return null;
                 let nestedFiltersAvailable = Object.values(scope.filters.available).filter(f => f.category === 'nested');
                 if (set.length == nestedFiltersAvailable.length-1 && !set[0].isDefault) {
                     // everything but first is selected, so no message
-                    return null;
+                    return [ 'all' ];
                 }
-                if (set.length==1) {
-                    return getFilterOrEmpty(set[0]).displaySummary;
-                }
-                // all tasks
-                return null;
+                return set.map(s => s.displaySummary || '');
+                // if (set.length==1) {
+                //     return [ getFilterOrEmpty(set[0]).displaySummary ];
+                // }
+                // // only happens if we have
+                // return null;
             },
-            'type/tag': set => {
+            'type-tag': set => {
                 if (!set || !set.length) return null;
                 if (set.length<=3) {
-                    let tags = set.map(s => (getFilterOrEmpty(s).displaySummary || '').toLowerCase()).filter(x => x);
-                    if (tags.length==0) return null;
-                    if (tags.length==1) return tags[0];
-                    if (tags.length==2) return tags[0] + ' or ' + tags[1];
-                    if (tags.length==3) return tags[0] + ', ' + tags[1] + ', or ' + tags[2];
+                    return set.map(s => (getFilterOrEmpty(s).displaySummary || '').toLowerCase()).filter(x => x);
+                } else {
+                    return ['any of '+set.length+' tags'];
                 }
-                return 'any of multiple tags'
             },
         };
-        defaultFilters['_top'] = {
+        filtersFullList['_top'] = {
             display: 'Only show ' + (isActivityChildren ? 'direct sub-tasks' : 'top-level tasks'),
-            displaySummary: 'only top-level tasks',
+            displaySummary: 'only top-level',
             isDefault: true,
             filter: filterTopLevelTasks,  // redundant with starting set, but contributes the right count
             category: 'nested',
@@ -352,15 +322,15 @@ TODO workflow ui
             onDisabledPost: enableOthersIfCategoryEmpty('_top'),
         }
         if (!isActivityChildren) {
-            defaultFilters['_cross_entity'] = {
+            filtersFullList['_cross_entity'] = {
                 display: 'Include cross-entity sub-tasks',
-                displaySummary: 'cross-entity tasks',
+                displaySummary: 'cross-entity',
                 filter: filterCrossEntityTasks,
                 category: 'nested',
                 onEnabledPre: clearOther('_top'),
                 onDisabledPost: enableFilterIfCategoryEmpty('_top'),
             }
-            defaultFilters['_recursive'] = {
+            filtersFullList['_recursive'] = {
                 display: 'Include sub-tasks on this entity',
                 displaySummary: 'sub-tasks',
                 filter: filterNestedSameEntityTasks,
@@ -369,9 +339,9 @@ TODO workflow ui
                 onDisabledPost: enableFilterIfCategoryEmpty('_top'),
             }
         } else {
-            defaultFilters['_recursive'] = {
+            filtersFullList['_recursive'] = {
                 display: 'Show all sub-tasks',
-                displaySummary: 'sub-tasks',
+                displaySummary: 'all sub-tasks',
                 filter: filterNonTopLevelTasks,
                 category: 'nested',
                 onEnabledPre: clearOther('_top'),
@@ -379,134 +349,131 @@ TODO workflow ui
             }
         }
 
-        const countWorkflowsWhichAreNestedButHaveReplayed = tasksAll.filter(t =>
-            t.isReplayedWorkflowLatest && t.submittedByTask
-        ).length;
-        defaultFilters['_workflowReplayed'] = {
-            display: 'Include workflow sub-tasks which are replayed',
-            displaySummary: null,
-            filter: tasks => tasks.filter(t => t.isReplayedWorkflowLatest && t.submittedByTask),
-            category: 'nested',
-            count: countWorkflowsWhichAreNestedButHaveReplayed,
-            countAbsolute: countWorkflowsWhichAreNestedButHaveReplayed,
-            onEnabledPre: clearCategory(),
-            onDisabledPost: enableOthersIfCategoryEmpty('_anyTypeTag'),
-        }
-
-        const countWorkflowsWhichArePreviousReplays = tasksAll.filter(t => t.isWorkflowPreviousRun).length;
-        defaultFilters['_workflowNonLastReplayHidden'] = {
-            display: 'Exclude old runs of workflows',
-            help: 'Some workflows have been replayed, either manually or on a server restart or failover. ' +
-                'To simplify the display, old runs of workflow invocations which have been replayed are excluded here by default. ' +
-                'The most recent replay will be included, subject to other filters, and previous replays can be accessed ' +
-                'on the workflow page.',
-            displaySummary: null,
-            filter: tasks => tasks.filter(t => {
-                    return _.isNil(t.isWorkflowPreviousRun) || !t.isWorkflowPreviousRun;
-                }),
-            count: countWorkflowsWhichArePreviousReplays,
-            countAbsolute: countWorkflowsWhichArePreviousReplays,
-            category: 'workflow',
-            onEnabledPre: null,
-            onDisabledPost: null,
-        }
-
-        const countWorkflowsWithoutTaskWhichAreCompleted = tasksAll.filter(t => t.endTimeUtc>0 && t.isTaskStubFromWorkflowRecord).length;
-        defaultFilters['_workflowCompletedWithoutTaskHidden'] = {
-            display: 'Exclude old completed workflows',
-            help: 'Some older workflows no longer have a task record, '+
-                'either because they completed in a previous server prior to a server restart or failover, ' +
-                'or because their tasks have been cleared from memory in this server. ' +
-                'These can be excluded to focus on more recent tasks.',
-            displaySummary: null,
-            // filter: tasks => tasks.filter(t => t.isWorkflowPreviousRun !== false),
-            filter: tasks => tasks.filter(t => !(t.endTimeUtc>0 && t.isTaskStubFromWorkflowRecord)),
-            count: countWorkflowsWithoutTaskWhichAreCompleted,
-            countAbsolute: countWorkflowsWithoutTaskWhichAreCompleted,
-            category: 'workflow2',
-            onEnabledPre: null,
-            onDisabledPost: null,
-        }
-
-        defaultFilters['_anyTypeTag'] = {
+        filtersFullList['_anyTypeTag'] = {
             display: 'Any task type or tag',
             displaySummary: null,
             filter: input => input,
-            category: 'type/tag',
+            category: 'type-tag',
             onEnabledPre: clearCategory(),
             onDisabledPost: enableOthersIfCategoryEmpty('_anyTypeTag'),
         }
 
-        function addTagFilter(tag, target, display, displaySummary) {
+        function addTagFilter(tag, target, display, extra) {
             if (!target[tag]) target[tag] = {
                 display: display,
-                displaySummary: displaySummary || tag.toLowerCase(),
+                displaySummary: tag.toLowerCase(),
                 filter: filterForTasksWithTag(tag),
-                category: 'type/tag',
+                category: 'type-tag',
                 onEnabledPre: clearOther('_anyTypeTag'),
                 onDisabledPost: enableFilterIfCategoryEmpty('_anyTypeTag'),
+                ...(extra || {}),
             }
         }
         // put these first
-        addTagFilter('EFFECTOR', defaultFilters, 'Effectors', 'effector');
-        addTagFilter('WORKFLOW', defaultFilters, 'Workflow');
-
-        const filtersIncludingTags = {...defaultFilters};
+        addTagFilter('EFFECTOR', filtersFullList, 'Effectors', { displaySummary: 'effector', includeIfZero: true });
+        addTagFilter('WORKFLOW', filtersFullList, 'Workflow', { includeIfZero: true });
 
         // add filters for other tags
         tasks.forEach(t =>
             (t.tags || []).filter(tag => typeof tag === 'string' && tag.length < 32).forEach(tag =>
-                    addTagFilter(tag, filtersIncludingTags, 'Tag: ' + tag.toLowerCase())
+                    addTagFilter(tag, filtersFullList, 'Tag: ' + tag.toLowerCase())
             ));
 
-        // fill in fields
 
-        Object.entries(filtersIncludingTags).forEach(([k, f]) => {
-            if (!f.select) f.select = defaultToggleFilter;
-            if (!f.onClick) f.onClick = (filterId, filter) => defaultToggleFilter(filterId, filter, null, true);
+        const filterWorkflowsReplayedTopLevel = t => !t.isWorkflowFirstRun && t.isWorkflowLastRun && t.isWorkflowTopLevel;
+        const countWorkflowsReplayedTopLevel = tasksAll.filter(filterWorkflowsReplayedTopLevel).length;
+        filtersFullList['_workflowReplayedTopLevel'] = {
+            display: 'Include replayed top-level workflows',
+            help: 'Some workflows have been replayed, either manually or on a server restart or failover. ' +
+                'Top-level workflows which have been replayed can be listed explicitly to make ' +
+                'them easier to find, because they usually have had issues which may require attention.',
+            displaySummary: null,
+            filter: tasks => tasks.filter(filterWorkflowsReplayedTopLevel),
+            categoryForEvaluation: 'nested',
+            category: 'workflow',
+            count: countWorkflowsReplayedTopLevel,
+            countAbsolute: countWorkflowsReplayedTopLevel,
+        }
 
-            if (_.isNil(f.count)) f.count = scope.findTasksExcludingCategory(f.filter(tasks), scope.filters.selectedFilters, f.category).length;
-            if (_.isNil(f.countAbsolute)) f.countAbsolute = f.filter(tasks).length;
-        });
+        const filterWorkflowsReplayedNested = t => !t.isWorkflowFirstRun && t.isWorkflowLastRun && !t.isWorkflowTopLevel;
+        const countWorkflowsReplayedNested = tasksAll.filter(filterWorkflowsReplayedNested).length;
+        filtersFullList['_workflowReplayedNested'] = {
+            display: 'Include replayed sub-workflows',
+            help: 'Some nested workflows have been replayed, either manually or on a server restart or failover. ' +
+                'Nested workflows are those invoked by other workflows, and their replay is usually due to a replay of their parent workflow. '+
+                'To simplify the display, these are excluded in this list by default. ' +
+                'Their root workflow or task will be shown, subject to other filters, and can be navigated on the workflow page. ' +
+                'If this option is enabled, these tasks will included here.',
+            displaySummary: null,
+            filter: tasks => tasks.filter(filterWorkflowsReplayedNested),
+            categoryForEvaluation: 'nested',
+            category: 'workflow',
+            count: countWorkflowsReplayedNested,
+            countAbsolute: countWorkflowsReplayedNested,
+        }
 
+        const filterWorkflowsWhichAreNotPreviousReplays = t => _.isNil(t.isWorkflowLastRun) || t.isWorkflowLastRun;
+        const filterWorkflowsWhichAreActuallyPreviousReplays = t => !_.isNil(t.isWorkflowLastRun) && !t.isWorkflowLastRun;
+        const countWorkflowsWhichArePreviousReplays = tasksAll.filter(filterWorkflowsWhichAreActuallyPreviousReplays).length;
+        filtersFullList['_workflowNonLastReplayHidden'] = {
+            display: 'Exclude old runs of workflows',
+            help: 'Some workflows have been replayed, either manually or on a server restart or failover. ' +
+                'To simplify the display, old runs of workflow invocations which have been replayed are excluded in this list by default. ' +
+                'The most recent replay will be included, subject to other filters, and previous replays can be accessed on the workflow page. ' +
+                'If this option is enabled, these tasks will not be excluded here.',
+            displaySummary: null,
+            filter: tasks => tasks.filter(filterWorkflowsWhichAreNotPreviousReplays),
+            count: countWorkflowsWhichArePreviousReplays,
+            countAbsolute: countWorkflowsWhichArePreviousReplays,
+            categoryForEvaluation: 'workflow1',
+            category: 'workflow',
+        }
+
+        const filterWorkflowsWithoutTaskWhichAreCompleted = t => t.endTimeUtc>0 && t.isTaskStubFromWorkflowRecord;
+        const countWorkflowsWithoutTaskWhichAreCompleted = tasksAll.filter(filterWorkflowsWithoutTaskWhichAreCompleted).length;
+        filtersFullList['_workflowCompletedWithoutTaskHidden'] = {
+            display: 'Exclude old completed workflows',
+            help: 'Some older workflows no longer have a task record, '+
+                'either because they completed in a previous server prior to a server restart or failover, ' +
+                'or because their tasks have been cleared from memory in this server. ' +
+                'These can be excluded to focus on more recent tasks.',
+            displaySummary: null,
+            filter: tasks => tasks.filter(filterWorkflowsWithoutTaskWhichAreCompleted),
+            count: countWorkflowsWithoutTaskWhichAreCompleted,
+            countAbsolute: countWorkflowsWithoutTaskWhichAreCompleted,
+            categoryForEvaluation: 'workflow2',
+            category: 'workflow',
+        }
+
+        // fill in fields
         function updateSelectedFilters(newValues) {
-            const deferredCalls = [];
-            Object.entries(scope.filters.selectedIds).forEach(([filterId,filterSelectionNote]) => {
+            Object.entries(scope.filters.selectedFilters).forEach(([filterId, oldValue]) => {
                 const newValue = newValues[filterId];
-                const oldValue = scope.filters.selectedFilters[filterId];
-                //console.log("enabling ",filterId,filterSelectionNote,newValue,oldValue);
                 scope.filters.selectedFilters[filterId] = newValue;
-                scope.filters.selectedIds[filterId] = newValue ? 'updated' : filterSelectionNote;
                 if (!newValue) delete scope.filters.selectedFilters[filterId];
-
-                if (newValue && filterSelectionNote==="theoretically-enabled") {
-                    deferredCalls.push(()=> {
-                        // trigger the handler, update other categories, if a category becomes available late
-                        console.log("Delayed enablement of filter ", filterId);
-                        // console.log("=");
-                        newValue.select(filterId, newValue, true, false, true);
-                        // console.log("--");
-                        // console.log("CATS 1", Object.keys(scope.filters.selectedIds));
-                        // console.log("CATS 2", Object.keys(scope.filters.selectedFilters));
-                        // console.log("CATS 3", Object.keys(scope.filters.selectedIds));
-                    });
-                }
             });
-            deferredCalls.forEach(c => c());
         }
 
+        updateSelectedFilters(filtersFullList);
+
         // add counts
-        //updateSelectedFilters(filtersIncludingTags);
+        Object.entries(filtersFullList).forEach(([k, f]) => {
+            if (!f.select) f.select = defaultToggleFilter;
+            if (!f.onClick) f.onClick = (filterId, filter) => defaultToggleFilter(filterId, filter, null, true);
+
+            if (_.isNil(f.count)) f.count = scope.findTasksExcludingCategory(f.filter(tasks), scope.filters.selectedFilters, f.category).length;
+            if (_.isNil(f.countAbsolute)) f.countAbsolute = f.filter(tasks).length;
+        });
 
         // filter and move to new map
         let result = {};
-        Object.entries(filtersIncludingTags).forEach(([k, f]) => {
-            if (f.countAbsolute > 0) result[k] = f;
+        Object.entries(filtersFullList).forEach(([k, f]) => {
+            if (f.countAbsolute > 0 || f.includeIfZero) result[k] = f;
         });
 
         // and delete categories that are redundant
-        function deleteCategoryIfAllCountsAreEqual(category) {
-            if (_.uniq(Object.values(result).filter(f => f.category === category).map(f => f.countAbsolute)).length==1) {
+        function deleteCategoryIfAllCountsAreEqualOrZero(category) {
+            if (_.uniq(Object.values(result).filter(f => f.category === category).filter(f => f.countAbsolute).map(f => f.countAbsolute)).length==1) {
                 Object.entries(result).filter(([k,f]) => f.category === category).forEach(([k,f])=>delete result[k]);
             }
         }
@@ -519,7 +486,7 @@ TODO workflow ui
         }
         deleteFiltersInCategoryThatAreEmpty('nested');
         deleteCategoryIfSize1('nested');
-        deleteCategoryIfAllCountsAreEqual('type/tag');  // because all tags are on all tasks
+        deleteCategoryIfAllCountsAreEqualOrZero('type-tag');  // because all tags are on all tasks
 
         if (!result['_cross_entity'] && result['_recursive']) {
             // if we don't have cross-entity sub-tasks, tidy this message
@@ -533,14 +500,16 @@ TODO workflow ui
         // now add dividers between categories
         let lastCat = null;
         for (let v of Object.values(result)) {
-            if (lastCat!=null && lastCat!=v.category) {
+            let thisCat = v.categoryForDisplay || v.category;
+            if (lastCat!=null && lastCat!=thisCat) {
                 v.classes = (v.classes || '') + ' divider-above';
             }
-            lastCat = v.category;
+            lastCat = thisCat;
         }
 
         scope.filters.available = result;
         updateSelectedFilters(result);
+
         return result;
     }
 }
@@ -622,15 +591,13 @@ function filterForTasksWithTag(tag) {
 
 function tasksAfterGlobalFilters(inputs, globalFilters) {
     if (inputs) {
-        if (globalFilters && globalFilters.transient && !globalFilters.transient.include) {
-            inputs = inputs.filter(t => !isTaskWithTag(t, 'TRANSIENT'));
-        }
+        Object.values(globalFilters || {}).filter(gf => !gf.include).forEach(gf => {
+            inputs = gf.filter(inputs);
+        });
     }
     return inputs;
 }
 
-function cacheSelectedIdsFromFilters(scope) { scope.filters.selectedIds = { ...scope.filters.selectedFilters }; }
-
 export function activityFilter($filter) {
     return function (activities, searchText) {
         if (activities && searchText && searchText.length > 0) {
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.less b/ui-modules/app-inspector/app/components/task-list/task-list.less
index 4c92ad3c..a111b031 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.less
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.less
@@ -47,16 +47,29 @@ task-list {
         flex: 0;
 
         .selection-summary {
-          background-color: @gray-dark;
-          color: @gray-lighter;
-          padding: 3px 6px;
-          border-radius: 5px;
+          .funnal {
+            color: @gray-light;
+            margin-right: 1ex;
+          }
+          .dropdown-badges-for-category {
+            margin: 0 0.2ex;
+          }
+          .dropdown-badge-for-category {
+            background-color: @gray-dark;
+            color: @gray-lightest;
+            padding: 3px 6px;
+            border-radius: 5px;
+            margin: 0 0.3ex;
+
+            &.dropdown-category-type-tag {
+              background-color: @color-labels-dark;
+            }
+          }
         }
-
+        margin-right: 0.6em;
       }
       .activity-name-filter {
         flex: 1;
-        margin-left: 0.5em;
       }
   }
 
@@ -123,6 +136,7 @@ task-list {
 
   .dropdown-menu.with-checks {
     width: auto;
+    max-height: 400px;
 
     li {
       padding-left: 2em;
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.template.html b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
index 54ce89f1..02679002 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.template.html
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.template.html
@@ -19,9 +19,14 @@
 <div class="no-activities" ng-if="tasks.length === 0">No activities</div>
 <div ng-if="tasks.length !== 0" class="task-list">
     <div class="form-group search-bar-with-controls">
-        <div class="btn-group activity-tag-filter" uib-dropdown keyboard-nav="true" dropdown-append-to="model.appendTo" ng-if="filters.selectedDisplayName">
+        <div class="btn-group activity-tag-filter" uib-dropdown keyboard-nav="true" dropdown-append-to="model.appendTo"
+                ng-if="filters.selectedDisplay && (globalFilters.transient || !isEmpty(filters.available))">
             <button id="single-button" type="button" class="btn btn-default" uib-dropdown-toggle>
-                Show <span class="selection-summary">{{filters.selectedDisplayName}}</span> <span class="caret"></span>
+                <i class="fa fa-filter" class="funnel"></i> <span class="selection-summary">
+                    <span class="dropdown-badges-for-category" ng-repeat="classAndBadges in filters.selectedDisplay" id="{{ classAndBadges.class }}"
+                        ><span class="dropdown-badge-for-category {{ classAndBadges.class }}" ng-repeat="badge in classAndBadges.badges" id="{{ badge }}">{{ badge }}</span
+                    ></span>
+                </span> <span class="caret"></span>
             </button>
             <ul class="dropdown-menu with-checks" uib-dropdown-menu role="menu" aria-labelledby="single-button">
                 <li role="menuitem" ng-repeat="(tag,value) in filters.available track by tag"
@@ -53,8 +58,11 @@
                         {{ value.countAbsolute - value.count}}
                     </span>
                 </li>
-                <li role="menuitem" class="activity-tag-filter-action" ng-if="globalFilters.transient" ng-click="globalFilters.transient.action()">
-                    <i><span class="main">{{globalFilters.transient.display}}</span></i>
+                <li role="menuitem" class="activity-tag-filter-action divider-above" style="padding-bottom: 9px;"
+                        ng-if="globalFilters.transient" ng-click="globalFilters.transient.onClick()"
+                        ng-class="{'selected': globalFilters.transient.checked}">
+                    <i class="fa fa-check check if-selected"></i>
+                    <span class="main" title="{{globalFilters.transient.help}}">{{globalFilters.transient.display}}</span>
                 </li>
                 <li role="menuitem" class="activity-tag-filter-error" ng-if="!globalFilters.transient && isEmpty(filters.available)">
                     <i><span class="main">No filter options</span></i>
@@ -92,11 +100,16 @@
                     </a>
                 </td>
                 <td class="name">
-                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">{{task.displayName}}
+                    <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})"
+                       title="Task {{ task.id
+                            }}{{ task.workflowId ? '\n'+'Workflow '+task.workflowId : ''
+                            }}{{ task.isTaskStubFromWorkflowRecord ? '\n\n'+'This task is no longer available at the server. This stub was created from the limited information in the workflow record.' : ''
+                            }}{{ task.workflowId && !task.isWorkflowTopLevel ? '\n\n'+'This is a nested workflow, launched from workflow '+task.workflowParentId+'.' : ''
+                            }}{{ !task.isWorkflowFirstRun && task.isWorkflowLastRun ? '\n\n'+'Workflow replayed. This is the most recent.' : ''
+                            }}{{ task.workflowId && !task.isWorkflowLastRun ? '\n\n'+'Workflow replayed. This is an earlier run.' : ''
+                            }}"
+                    >{{task.displayName}}
                     </a>
-                    {{ task.id }} -
-                    prevRun={{ task.isWorkflowPreviousRun }}
-                    replLatest={{ task.isReplayedWorkflowLatest }}
                 </td>
                 <td class="started">
                     <a ui-sref="main.inspect.activities.detail({entityId: task.entityId, activityId: task.id, workflowId: getTaskWorkflowId(task)})">
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index 96c15948..b005142f 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -61,6 +61,7 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
         // if we switch from child state to us and we haven't been initialized
         onStateChange();
     })
+    $scope.activitiesLoaded = false;
 
     function mergeActivities() {
         // merge activitiesRaw records with workflows records, into vm.activities;
@@ -76,7 +77,7 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                 .forEach(wf => {
                     const last = wf.replays[wf.replays.length-1];
                     let submitted = {};
-                    let lastTask;
+                    let firstTask, lastTask;
 
                     wf.replays.forEach(wft => {
                         let t = newActivitiesMap[wft.taskId];
@@ -85,29 +86,36 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                             t = makeTaskStubFromWorkflowRecord(wf, wft);
                             newActivitiesMap[wft.taskId] = t;
                         }
-                        t.workflowId = wft.workflowId;
+                        t.workflowId = wf.workflowId;
+                        t.workflowParentId = wf.parentId;
 
                         // overriding submitters breaks things (infinite loop, in kilt?)
                         // so instead just set whether it is the latest replay
-                        t.isWorkflowPreviousRun = last && wft.taskId !== last.taskId;
+                        t.isWorkflowFirstRun = false;
+                        t.isWorkflowLastRun = false;
+                        t.isWorkflowTopLevel = !wf.parentId;
+                        if (!firstTask) firstTask = t;
                         lastTask = t;
                     });
-                    if (wf.replays.length>=2) lastTask.isReplayedWorkflowLatest = true;
+                    firstTask.isWorkflowFirstRun = true;
+                    lastTask.isWorkflowLastRun = true;
                 });
 
             vm.activitiesMap = newActivitiesMap;
             vm.activities = Object.values(vm.activitiesMap);
-            // TODO weird bug
-            // vm.activitiesUniq = _.uniq(Object.values(vm.activitiesMap));
-            // if (vm.activities.length != Object.values(vm.activitiesMap).length) {
-            //     console.log("MISMATCH", vm.activitiesMap);
-            // }
         }
     }
 
+    let activitiesRawLoadAttemptFinished = false;
+    let workflowLoadAttemptFinished = false;
+
     function onStateChange() {
       if ($state.current.name === activitiesState.name && !vm.activities) {
         // only run if we are the active state
+
+        const checkTasksLoadAttemptsFinished = () => {
+            $scope.activitiesLoaded = activitiesRawLoadAttemptFinished && workflowLoadAttemptFinished;
+        }
         entityApi.entityActivities(applicationId, entityId).then((response) => {
             vm.activitiesRaw = response.data;
             mergeActivities();
@@ -116,9 +124,13 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                 mergeActivities();
                 vm.error = undefined;
             }));
+            activitiesRawLoadAttemptFinished = true;
+            checkTasksLoadAttemptsFinished();
         }).catch((error) => {
             $log.warn('Error loading activities for entity '+entityId, error);
             vm.error = 'Cannot load activities for entity with ID: ' + entityId;
+            activitiesRawLoadAttemptFinished = true;
+            checkTasksLoadAttemptsFinished();
         });
 
         entityApi.getWorkflows(applicationId, entityId).then((response) => {
@@ -128,8 +140,12 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                 vm.workflows = response.data;
                 mergeActivities();
             }));
+            workflowLoadAttemptFinished = true;
+            checkTasksLoadAttemptsFinished();
         }).catch((error) => {
             $log.warn('Error loading workflows for entity ' + entityId, error);
+            workflowLoadAttemptFinished = true;
+            checkTasksLoadAttemptsFinished();
         });
 
         entityApi.entityActivitiesDeep(applicationId, entityId).then((response) => {
@@ -151,6 +167,8 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
       }
     }
 
+    // these are passed around so that the task list and the kilt view share info on DST's, at least
+    // (would be nice to share more but that gets trickier, and this is the essential!)
     vm.onFilteredActivitiesChange = function (newActivities, globalFilters) {
         vm.focusedActivities = newActivities;
         $scope.globalFilters = globalFilters;
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.template.html b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.template.html
index 49e30830..81c5c71e 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.template.html
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.template.html
@@ -21,7 +21,7 @@
   <div class="row">
     <div ng-class="{ 'col-md-12': true, 'col-lg-8': !vm.wideKilt && vm.isNonEmpty(vm.activitiesDeep), 'col-lg-12': vm.wideKilt || !vm.isNonEmpty(vm.activitiesDeep)}">
         <loading-state error="vm.error" ng-if="!vm.activities"></loading-state>
-        <task-list task-type="{{filter || 'activity'}}" search="search" tasks="vm.activities" filtered-callback="vm.onFilteredActivitiesChange" ng-if="vm.activities"></task-list>
+        <task-list task-type="{{filter}}" search="search" tasks="vm.activities" tasks-loaded="activitiesLoaded" filtered-callback="vm.onFilteredActivitiesChange" ng-if="vm.activities"></task-list>
     </div>
 
     <div ng-class="{ 'col-md-12': true, 'col-lg-4': !vm.wideKilt, 'col-lg-12': vm.wideKilt }" ng-if="vm.isNonEmpty(vm.activitiesDeep)">
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 dd8d6115..1efc6860 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
@@ -259,7 +259,7 @@
 
                         <div class="row">
                             <div ng-class="{ 'col-md-12': true, 'col-lg-8': !vm.wideKilt && vm.isNonEmpty(vm.model.activitiesDeep), 'col-lg-12': vm.wideKilt || !vm.isNonEmpty(vm.model.activitiesDeep)}">
-                                <task-list tasks="vm.model.activityChildren" task-type="activityChildren" filtered-callback="vm.onFilteredActivitiesChange"></task-list>
+                                <task-list tasks="vm.model.activityChildren" parent-task-id="vm.model.activityId" filtered-callback="vm.onFilteredActivitiesChange"></task-list>
                             </div>
                             <div ng-class="{ 'col-md-12': true, 'col-lg-4': !vm.wideKilt, 'col-lg-12': vm.wideKilt }" ng-if="vm.isNonEmpty(vm.model.activitiesDeep)">
                                 <expandable-panel expandable-template="vm.modalTemplate" class="panel-table">
diff --git a/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html b/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html
index f563735d..ba57067b 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html
+++ b/ui-modules/app-inspector/app/views/main/inspect/management/detail/detail.template.html
@@ -115,7 +115,7 @@
   <div class="row">
     <div class="col-md-12">
         <loading-state error="vm.error" ng-if="!vm.activities"></loading-state>
-        <task-list task-type="activity" tasks="vm.activities" ng-if="vm.activities"></task-list>
+        <task-list tasks="vm.activities" ng-if="vm.activities"></task-list>
     </div>
     <!-- kilt view not shown, as it requires to load activities deep -->
   </div>
diff --git a/ui-modules/utils/br-core/style/variables.less b/ui-modules/utils/br-core/style/variables.less
index efa00800..fbb41ddd 100644
--- a/ui-modules/utils/br-core/style/variables.less
+++ b/ui-modules/utils/br-core/style/variables.less
@@ -24,6 +24,7 @@
 @color-failed: #820;
 @color-cancelled: #660;
 @color-active: #6a2;
+@color-labels-dark: #478;
 
 // brand-success and others usually used for BG, so make lighter
 @color-succeeded-bg: #484;


[brooklyn-ui] 07/24: improve workflow ui for GC'd tasks, plus other minor ui fixes

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-ui.git

commit efb5511e1a56059b2755a411e02ce8dc42785962
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 6 10:42:23 2022 +0100

    improve workflow ui for GC'd tasks, plus other minor ui fixes
    
    create stub tasks where a workflow references a task;
    use improved UI for details on workflow replays;
    show graphic for additional task statuses
---
 .../components/providers/entity-api.provider.js    |   2 +-
 .../components/task-list/task-list.directive.js    |   4 +
 .../components/workflow/workflow-step.directive.js |   2 +-
 .../workflow/workflow-step.template.html           |   2 +-
 .../inspect/activities/activities.controller.js    | 100 +++++++++++++++++++--
 .../inspect/activities/detail/detail.controller.js |  24 ++++-
 .../inspect/activities/detail/detail.template.html |  10 ++-
 ui-modules/utils/status/status.js                  |   8 +-
 8 files changed, 133 insertions(+), 19 deletions(-)

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


[brooklyn-ui] 06/24: workflow ui tidies following review

Posted by he...@apache.org.
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 50e6787d2af2f4eaf9a1e99db296be3aa80e38a7
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Wed Oct 5 15:51:51 2022 +0100

    workflow ui tidies following review
---
 .../app/components/workflow/workflow-steps.directive.js           | 8 ++++----
 .../app-inspector/app/components/workflow/workflow-steps.less     | 8 +++++++-
 .../app/components/workflow/workflow-steps.template.html          | 2 +-
 3 files changed, 12 insertions(+), 6 deletions(-)

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 8eb950d0..e81fa495 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
@@ -221,11 +221,11 @@ function makeArrows(workflow, steps) {
                 }
                 if (prev > i) rank = rank + 0.5;
                 width = 0.2 + 0.6 * (rank + 0.5) / (jumpSizes.length + 0.5);
-                // curveX = 0.8 + 0.2*width;
-                // curveY = 0.8 + 0.2*width;
+                curveX = 0.8 + 0.2*width;
+                curveY = 0.8 + 0.2*width;
                 // higher values (above) look nicer, but make disambiguation of complex paths harder
-                curveX = 0.5 + 0.3*width;
-                curveY = 0.4 + 0.4*width;
+                // curveX = 0.5 + 0.3*width;
+                // curveY = 0.4 + 0.4*width;
             }
             return arrowStep(prev, i, {hideArrowhead: prev==i, width, curveX, curveY, ...opts});
         }
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 2c067037..59f2a854 100644
--- a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
@@ -89,7 +89,7 @@
 
       .step-name {
         font-weight: 600;
-        font-size: 100%;
+        font-size: 90%;
       }
 
       .step-id {
@@ -249,4 +249,10 @@
     overflow: hidden;
   }
 
+  svg.workflow-arrows {
+    //opacity: 10%;
+  }
+  svg.workflow-arrows:hover {
+    opacity: 100%;
+  }
 }
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html b/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html
index 309ce4a2..b571686e 100644
--- a/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html
@@ -18,7 +18,7 @@
 -->
 <div class="workflow-steps" ng-class="vm.getWorkflowStepsClasses()">
 
-    <svg width="100%" height="100%" style="position: absolute;">
+    <svg width="100%" height="100%" style="position: absolute;" class="workflow-arrows">
         <g transform="scale(-1,1)" transform-origin="center" id="workflow-step-arrows">
         </g>
     </svg>


[brooklyn-ui] 20/24: fix license header

Posted by he...@apache.org.
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 0e566e4ed25a6835ed2eb23be95d24a64b477e39
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Wed Oct 19 10:48:24 2022 +0100

    fix license header
---
 .../inspect/activities/detail/dropdown-nested.js     | 20 ++++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

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 93ec9d95..c8cabfea 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
@@ -1,5 +1,21 @@
-import {drop} from "lodash/array";
-
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
 const MODULE_NAME = 'ui.bootstrap.dropdown.nested';
 
 export default MODULE_NAME;


[brooklyn-ui] 17/24: replay buttons and arrow tidies

Posted by he...@apache.org.
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 62c2b8668eb6910a8a08c1a886fc8bf8d1020313
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Mon Oct 10 21:26:43 2022 +0100

    replay buttons and arrow tidies
---
 .../components/providers/entity-api.provider.js    |  5 ++
 .../components/workflow/workflow-step.directive.js |  2 +-
 .../workflow/workflow-steps.directive.js           | 20 ++++--
 .../inspect/activities/detail/detail.controller.js | 72 ++++++++++++++++------
 .../inspect/activities/detail/detail.template.html | 14 ++---
 5 files changed, 83 insertions(+), 30 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 62d64f76..aa34cfd3 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
@@ -74,6 +74,7 @@ function EntityApi($http, $q) {
 
         getWorkflows: getWorkflows,
         getWorkflow: getWorkflow,
+        replayWorkflow: replayWorkflow,
     };
 
     function getEntity(applicationId, entityId) {
@@ -196,4 +197,8 @@ function EntityApi($http, $q) {
     function getWorkflow(applicationId, entityId, workflowId) {
         return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflows/' + workflowId, {observable: true, ignoreLoadingBar: true});
     }
+    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});
+    }
 }
\ No newline at end of file
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 dadf25a7..7cc22544 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
@@ -118,7 +118,7 @@ export function workflowStepDirective() {
                     : ($scope.isWorkflowError && $scope.isCurrentMaybeInactive) ? 'The workflow encountered an error around this step.'
                     : null;
                 const incomplete = $scope.osi.countStarted - $scope.osi.countCompleted > ($scope.isCurrentAndActive ? 1 : 0);
-                $scope.stepCurrentWarning = incomplete ? 'This step has previously been interrupted.' : null;
+                $scope.stepCurrentWarning = incomplete && !$scope.stepCurrentError ? 'This step has previously had an error' : null;
                 $scope.stepCurrentSuccess = (!$scope.isCurrentAndActive && !incomplete && $scope.osi.countCompleted > 0)
                     ? 'This step has completed without errors.' : null;
 
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 c808f1f8..143b665c 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
@@ -89,6 +89,7 @@ function makeArrows(workflow, steps) {
 
     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><');
 
     if (steps) {
         let gradientCount = 0;
@@ -179,6 +180,8 @@ function makeArrows(workflow, steps) {
             return arrowSvg(s1, s2, opts);
         }
 
+        let jumpSizes = {1: true};
+
         function arrowStep2(prev, i, opts) {
             let curveX = 0.5;
             let curveY = 0.75;
@@ -216,7 +219,6 @@ function makeArrows(workflow, steps) {
             return 'rgb('+gray+','+gray+','+gray+')';
         }
 
-        let jumpSizes = {1: true};
         let arrowSpecs = {};
         function recordTransition(from, to, opts) {
             if (to!=-1 && from!=-1 && to!=from) {
@@ -259,22 +261,32 @@ function makeArrows(workflow, steps) {
 
         for (var i = 0; i < steps.length; i++) {
             const s = workflow.data.stepsDefinition[i];
+
+            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' });
+            }
+
+            opts = { ...opts, color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 };
+
             let next = null;
             if (s.next) {
                 if (s.next.toLowerCase()=='end') next = -1;
                 else if (indexOfId[s.next]) next = indexOfId[s.next];
             }
             if (isStepType(s, 'return')) next = -1;
+
             if (next!=null) {
                 // special next per step
-                recordTransition(i, next, { insertionPoint: 0, color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 });
+                recordTransition(i, next, opts);
                 if (!s.condition) continue;
             }
             // if nothing special, or if was conditional, then go to next step
+            // (only go forward 1, even if it is conditional, otherwise too many arrows)
+
             next = i+1;
             if (i + 1 >= steps.length) next = -1;
-
-            recordTransition(i, next, { insertionPoint: 0, color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 });
+            recordTransition(i, next, opts);
         }
 
         jumpSizes = Object.keys(jumpSizes).sort();
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 de1fa975..a5cbe6b2 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
@@ -51,7 +51,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
 
     vm.modalTemplate = modalTemplate;
     vm.wideKilt = false;
-    vm.actions = {};
+    $scope.actions = {};
 
     let observers = [];
 
@@ -76,21 +76,57 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 vm.model.workflow.applicationId = workflowTag.applicationId;
                 vm.model.workflow.entityId = workflowTag.entityId;
 
-                vm.actions.workflowReplays = [];
+                $scope.actions.workflowReplays = [];
                 if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) {
                     // can't replay if active (same logic as 'cancel')
                 } else {
-                    [
-                        // TODO get from server
-                        // [ 'step 3 (continuing)', null ],
-                        // [ 'step 3 (replay point)', [2] ],
-                        // [ 'start (replay point)', [0] ],
-                    ].forEach(r => vm.actions.workflowReplays.push(r));
-                    vm.actions.workflowReplays.forEach(r => {
-                        r.push( () => console.log("TODO - replay from "+r[0], r[1]) );
-                    })
+                    $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', targetName: 'Resume '+(stepIndex>=0 ? 'workflow ' : '')+' (at step '+(vm.model.workflow.data.replayableLastStep+1)+')' });
+                    }
+
+                    // 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 });
+                        }
+                    }
+
+                    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 = '';
+
+                        $scope.actions.workflowReplays.push({targetId: 'start', targetName: 'Restart '+(stepIndex>=0 ? 'workflow ' : '')+reason});
+                    }
+
+                    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)";
+                        }
+                        r.action = () => {
+                            entityApi.replay(applicationId, entityId, $scope.workflowId. targetId, opts);
+                        };
+                    });
                 }
-                if (!vm.actions.workflowReplays.length) delete vm.actions['workflowReplays'];
+                if (!$scope.actions.workflowReplays.length) delete $scope.actions['workflowReplays'];
 
                 if (vm.model.workflow.data.status === 'RUNNING') wResponse.interval(1000);
                 observers.push(wResponse.subscribe((wResponse2)=> {
@@ -112,22 +148,22 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
         activityApi.activity(activityId).then((response)=> {
             vm.model.activity = response.data;
 
-            delete vm.actions['effector'];
-            delete vm.actions['invokeAgain'];
+            delete $scope.actions['effector'];
+            delete $scope.actions['invokeAgain'];
             if ((vm.model.activity.tags || []).find(t => t=="EFFECTOR")) {
                 const effectorName = (vm.model.activity.tags.find(t => t.effectorName) || {}).effectorName;
                 const effectorParams = (vm.model.activity.tags.find(t => t.effectorParams) || {}).effectorParams;
                 if (effectorName) {
-                    vm.actions.effector = {effectorName};
+                    $scope.actions.effector = {effectorName};
                     if (effectorParams) {
-                        vm.actions.invokeAgain = {effectorName, effectorParams, doAction: () => vm.invokeEffector(effectorName, effectorParams) };
+                        $scope.actions.invokeAgain = {effectorName, effectorParams, doAction: () => vm.invokeEffector(effectorName, effectorParams) };
                     }
                 }
             }
 
-            delete vm.actions['cancel'];
+            delete $scope.actions['cancel'];
             if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) {
-                vm.actions.cancel = { doAction: () => { activityApi.cancelActivity(activityId); } };
+                $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
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 1efc6860..08b4b889 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
@@ -45,27 +45,27 @@
                     <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 style="float:right;" ng-if="vm.isNonEmpty(vm.actions)" class="btn-group dropdown-nested dropdown-menu-right" uib-dropdown-nested dropdown-append-to-body="true">
+                <div style="float:right;" ng-if="vm.isNonEmpty(actions)" class="btn-group dropdown-nested dropdown-menu-right" uib-dropdown-nested dropdown-append-to-body="true">
                     <br-button uib-dropdown-toggle-nested type="btn-primary">
                         Actions <span class="caret"></span>
                     </br-button>
                     <ul uib-dropdown-menu-nested class="dropdown-at-root dropdown-menu-right">
 
-                        <li><a href="" ng-if="vm.actions.cancel" ng-click="vm.actions.cancel.doAction()">Cancel</a></li>
+                        <li><a href="" ng-if="actions.cancel" ng-click="actions.cancel.doAction()">Cancel</a></li>
 
-                        <li ng-if="vm.actions.workflowReplays" uib-dropdown-nested dropdown-append-to-body="true">
+                        <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 vm.actions.workflowReplays track by $index" id="replay {{ replay[0] }}">
-                                    <a class="dropdown-item" href="" ng-click="replay[2]()">From {{ replay[0] }}</a>
+                                <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>
                                 </li>
                             </ul>
 
                         </li>
 
-                        <li><a href="" ng-if="vm.actions.invokeAgain" ng-click="vm.actions.invokeAgain.doAction()">Reinvoke effector</a></li>
-                        <li><a href="" ng-if="vm.actions.effector" ui-sref="main.inspect.effectors({search: vm.actions.effector.effectorName})">Open in effector tab</a></li>
+                        <li><a href="" ng-if="actions.invokeAgain" ng-click="actions.invokeAgain.doAction()">Reinvoke effector</a></li>
+                        <li><a href="" ng-if="actions.effector" ui-sref="main.inspect.effectors({search: actions.effector.effectorName})">Open in effector tab</a></li>
 
                     </ul>
                 </div>


[brooklyn-ui] 22/24: support running a workflow from ui

Posted by he...@apache.org.
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 961e2ad96710f261c0eb9b59840a4a70da48e39d
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Oct 20 10:13:26 2022 +0100

    support running a workflow from ui
---
 .../inspect/activities/activities.controller.js    |  1 -
 .../app/views/main/inspect/inspect.controller.js   | 46 ++++++++++++++++++++++
 .../app/views/main/inspect/inspect.template.html   |  3 ++
 .../main/inspect/run-workflow-modal.template.html  | 33 ++++++++++++++++
 4 files changed, 82 insertions(+), 1 deletion(-)

diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index dbb487e3..b005142f 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -176,7 +176,6 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
 }
 
 export function makeTaskStubFromWorkflowRecord(wf, wft) {
-    console.log("WFT", wft.status, wft);
     const result = {
         id: wft.taskId,
         displayName: wf.name + (wft.reasonForReplay ? " ("+wft.reasonForReplay+")" : ""),
diff --git a/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js b/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
index 0248fb0e..04808be3 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
@@ -18,6 +18,7 @@
  */
 import template from "./inspect.template.html";
 import addChildModalTemplate from "./add-child-modal.template.html";
+import runWorkflowModalTemplate from "./run-workflow-modal.template.html";
 import confirmModalTemplate from "./confirm.modal.template.html";
 
 export const inspectState = {
@@ -75,6 +76,25 @@ export function inspectController($scope, $stateParams, $uibModal, brSnackbar, e
         })
     };
 
+    this.runWorkflow = function() {
+        $uibModal.open({
+            animation: true,
+            template: runWorkflowModalTemplate,
+            controller: ['$scope', '$http', '$uibModalInstance', 'applicationId', 'entityId', runWorkflowController],
+            size: 'lg',
+            resolve: {
+                applicationId: ()=>(applicationId),
+                entityId: ()=>(entityId),
+            }
+        }).result.then((closeData)=> {
+            $state.go('main.inspect.activites', {
+                applicationId: applicationId,
+                entityId: closeData.entityId,
+                activityId: closeData.id
+            });
+        })
+    };
+
     this.resetEntityProblems = function() {
         entityApi.resetEntityProblems(applicationId, entityId).catch((error)=> {
             brSnackbar.create('Cannot reset entity problems: the entity [' + entityId + '] or sensor [service.notUp.indicators] is undefined');
@@ -113,6 +133,7 @@ export function addChildController($scope, $http, $uibModalInstance, application
                 $scope.deploying = false;
                 $uibModalInstance.close(response.data);
             }, (error)=> {
+                console.log("Error adding child", error);
                 $scope.deploying = false;
                 if (error.data.hasOwnProperty('message')) {
                     $scope.errorMessage = error.data.message;
@@ -122,3 +143,28 @@ export function addChildController($scope, $http, $uibModalInstance, application
             });
     }
 }
+
+export function runWorkflowController($scope, $http, $uibModalInstance, applicationId, entityId) {
+    $scope.workflowYaml = 'steps:\n  - ';
+    $scope.errorMessage = null;
+    $scope.running = false;
+    $scope.runWorkflow = runWorkflow;
+
+    function runWorkflow() {
+        $scope.running = true;
+        $scope.errorMessage = null;
+        $http.post('/v1/applications/' + applicationId + '/entities/' + entityId + '/workflow?start=true&timeout=20ms', $scope.workflowYaml)
+            .then((response)=> {
+                $scope.running = false;
+                $uibModalInstance.close(response.data);
+            }, (error)=> {
+                console.log("Error running workflow", error);
+                $scope.running = false;
+                if (error.data.hasOwnProperty('message')) {
+                    $scope.errorMessage = error.data.message;
+                } else {
+                    $scope.errorMessage = 'Could not run workflow ... unknown error';
+                }
+            });
+    }
+}
diff --git a/ui-modules/app-inspector/app/views/main/inspect/inspect.template.html b/ui-modules/app-inspector/app/views/main/inspect/inspect.template.html
index 58126b41..aa348ffd 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/inspect.template.html
+++ b/ui-modules/app-inspector/app/views/main/inspect/inspect.template.html
@@ -37,6 +37,9 @@
             <i class="fa fa-ellipsis-v" aria-hidden="true" ></i>
         </a>
         <ul uib-dropdown-menu class="entity-advanced-dropdown dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="single-button">
+            <li role="menuitem">
+                <a ng-click="vm.runWorkflow()">Run Workflow</a>
+            </li>
             <li role="menuitem">
                 <a ng-click="vm.addChildToEntity()">Add Child</a>
             </li>
diff --git a/ui-modules/app-inspector/app/views/main/inspect/run-workflow-modal.template.html b/ui-modules/app-inspector/app/views/main/inspect/run-workflow-modal.template.html
new file mode 100644
index 00000000..9bb3e654
--- /dev/null
+++ b/ui-modules/app-inspector/app/views/main/inspect/run-workflow-modal.template.html
@@ -0,0 +1,33 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<div class="modal-header">
+    <br-svg type="close" class="pull-right" ng-click="$dismiss('Close modal')"></br-svg>
+    <h3 class="modal-title">Run Workflow</h3>
+</div>
+
+<div>
+    <div ng-if="errorMessage" class="alert alert-danger" role="alert">
+        <strong>Error</strong> {{errorMessage}}
+    </div>
+    <br-yaml-editor value="workflowYaml" enable-auto-complete></br-yaml-editor>
+</div>
+
+<div class="modal-footer">
+    <button class="btn btn-success" ng-disabled="running" ng-click="runWorkflow()" type="button">Run</button>
+</div>


[brooklyn-ui] 18/24: ui for replay, replay scope improvements, and misc tidies to recent workflow ui

Posted by he...@apache.org.
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);


[brooklyn-ui] 19/24: Merge branch 'master' into workflow-ui

Posted by he...@apache.org.
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 f9cefb39290f8e215566e926e3b649d159824bd9
Merge: f66c75d5 51293560
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Wed Oct 19 09:45:58 2022 +0100

    Merge branch 'master' into workflow-ui

 ui-modules/utils/utils/momentp.spec.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


[brooklyn-ui] 24/24: more task list filter selection tweaks

Posted by he...@apache.org.
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 fa05e72ace12476d420ade79e1c73ca6b5ebfd9a
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Oct 21 14:54:05 2022 +0100

    more task list filter selection tweaks
---
 .../components/task-list/task-list.directive.js    | 53 +++++++++++++++-------
 .../components/workflow/workflow-step.directive.js |  1 +
 .../workflow/workflow-step.template.html           | 14 +++++-
 .../workflow/workflow-steps.directive.js           |  2 +-
 .../inspect/activities/activities.controller.js    | 23 ++++++++--
 .../inspect/activities/detail/detail.controller.js |  4 +-
 .../main/inspect/activities/detail/detail.less     |  2 +-
 .../inspect/activities/detail/detail.template.html | 29 ++++++++----
 .../views/main/inspect/confirm.modal.template.html |  2 +-
 .../app/views/main/inspect/inspect.controller.js   |  8 ++--
 10 files changed, 100 insertions(+), 38 deletions(-)

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 4e0d754e..2b9c7c58 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
@@ -165,11 +165,14 @@ export function taskListDirective() {
                     selectFilter('SUB-TASK');
                 }
             }
+            if (!isActivityChildren) selectFilter("_workflowStepsHidden");
             selectFilter("_workflowReplayedTopLevel");
             selectFilter("_workflowNonLastReplayHidden");
+            selectFilter("_workflowCompletedWithoutTaskHidden");
 
             // pick other filter combos until we get some conetnt
             if ($scope.tasksFilteredByTag.length==0) {
+                selectFilter('_cross_entity');
                 selectFilter('INITIALIZATION');
             }
             if ($scope.tasksFilteredByTag.length==0) {
@@ -305,15 +308,10 @@ export function taskListDirective() {
                 if (!set || !set.length) return null;
                 let nestedFiltersAvailable = Object.values(scope.filters.available).filter(f => f.category === 'nested');
                 if (set.length == nestedFiltersAvailable.length-1 && !set[0].isDefault) {
-                    // everything but first is selected, so no message
+                    // everything but first is selected, so no message (assume _top is always shown)
                     return [ 'all' ];
                 }
                 return set.map(s => s.displaySummary || '');
-                // if (set.length==1) {
-                //     return [ getFilterOrEmpty(set[0]).displaySummary ];
-                // }
-                // // only happens if we have
-                // return null;
             },
             'type-tag': set => {
                 if (!set || !set.length) return null;
@@ -332,6 +330,7 @@ export function taskListDirective() {
             category: 'nested',
             onEnabledPre: clearCategory(),
             onDisabledPost: enableOthersIfCategoryEmpty('_top'),
+            includeIfZero: true,
         }
         if (!isActivityChildren) {
             filtersFullList['_cross_entity'] = {
@@ -395,11 +394,12 @@ export function taskListDirective() {
         filtersFullList['SUB-TASK'] = false;
 
         // add filters for other tags
-        tasks.forEach(t =>
-            (t.tags || []).filter(tag => typeof tag === 'string' && tag.length < 32).forEach(tag =>
-                    addTagFilter(tag, filtersFullList, 'Tag: ' + tag.toLowerCase())
-            ));
+        let tags = _.uniq(tasks.flatMap(t => (t.tags || []).filter(tag => typeof tag === 'string' && tag.length < 32)));
+        tags.sort( (t1,t2) => t1.toLowerCase().localeCompare(t2.toLowerCase()) );
+        // same tag with different cases will be shown multiple times, unable to disambiguate, but that's unlikely
+        tags.forEach(tag => addTagFilter(tag, filtersFullList, 'Tag: ' + tag.toLowerCase()) );
 
+        Object.entries(filtersFullList).forEach(([k,v]) => { if (!v) delete filtersFullList[k]; });
         ['EFFECTOR', 'WORKFLOW', 'SUB-TASK', 'SENSORS', 'INITIALIZATION'].forEach(t => { if (!filtersFullList[t]) delete filtersFullList[t]; });
         (filtersFullList['SUB-TASK'] || {}).display = 'Sub-tasks';
         (filtersFullList['SENSOR'] || {}).display = 'Sensors';
@@ -426,6 +426,7 @@ export function taskListDirective() {
             categoryForEvaluation: 'status-scheduled',
         }
 
+
         const filterWorkflowsReplayedTopLevel = t => !t.isWorkflowFirstRun && t.isWorkflowLastRun && t.isWorkflowTopLevel;
         const countWorkflowsReplayedTopLevel = tasksAll.filter(filterWorkflowsReplayedTopLevel).length;
         filtersFullList['_workflowReplayedTopLevel'] = {
@@ -470,7 +471,7 @@ export function taskListDirective() {
             filter: tasks => tasks.filter(filterWorkflowsWhichAreNotPreviousReplays),
             count: countWorkflowsWhichArePreviousReplays,
             countAbsolute: countWorkflowsWhichArePreviousReplays,
-            categoryForEvaluation: 'workflow1',
+            categoryForEvaluation: 'workflow-non-last-replays',
             category: 'workflow',
         }
 
@@ -483,10 +484,26 @@ export function taskListDirective() {
                 'or because their tasks have been cleared from memory in this server. ' +
                 'These can be excluded to focus on more recent tasks.',
             displaySummary: null,
-            filter: tasks => tasks.filter(filterWorkflowsWithoutTaskWhichAreCompleted),
+            filter: tasks => tasks.filter(t => !filterWorkflowsWithoutTaskWhichAreCompleted(t)),
             count: countWorkflowsWithoutTaskWhichAreCompleted,
             countAbsolute: countWorkflowsWithoutTaskWhichAreCompleted,
-            categoryForEvaluation: 'workflow2',
+            categoryForEvaluation: 'workflow-old-completed',
+            category: 'workflow',
+        }
+
+        const filterWorkflowTasksWhichAreSteps = t => getTaskWorkflowTag(t) && !_.isNil(getTaskWorkflowTag(t));
+        const countWorkflowTasksWhichAreSteps = tasksAll.filter(filterWorkflowTasksWhichAreSteps).length;
+        filtersFullList['_workflowStepsHidden'] = {
+            display: 'Exclude individual workflow steps',
+            help: 'Individual steps within workflows are hidden in most views, except where showing workflow tasks. ' +
+                'This makes it easier to navigate to primary tasks, such as workflows, and from there explore the steps within. ' +
+                'If this option is disabled and if nested sub-tasks are enabled, then individual steps will be listed in this view ' +
+                'to facilitate finding a specific step.',
+            displaySummary: null,
+            filter: tasks => tasks.filter(t => _.isNil((getTaskWorkflowTag(t) || {}).stepIndex)),
+            count: countWorkflowTasksWhichAreSteps,
+            countAbsolute: countWorkflowTasksWhichAreSteps,
+            categoryForEvaluation: 'workflow-steps',
             category: 'workflow',
         }
 
@@ -512,6 +529,7 @@ export function taskListDirective() {
 
         // filter and move to new map
         let result = {};
+        // include non-zero filters or those included if zero
         Object.entries(filtersFullList).forEach(([k, f]) => {
             if (f.countAbsolute > 0 || f.includeIfZero) result[k] = f;
         });
@@ -522,14 +540,15 @@ export function taskListDirective() {
                 Object.entries(result).filter(([k,f]) => f.category === category).forEach(([k,f])=>delete result[k]);
             }
         }
-        function deleteFiltersInCategoryThatAreEmpty(category) {
-            Object.entries(result).filter(([k,f]) => f.category === category && f.countAbsolute==0).forEach(([k,f])=>delete result[k]);
-        }
+        // function deleteFiltersInCategoryThatAreEmpty(category) {
+        //     // redundant with population of 'result' above
+        //     Object.entries(result).filter(([k,f]) => f.category === category && f.countAbsolute==0 && !f.includeIfZero).forEach(([k,f])=>delete result[k]);
+        // }
         function deleteCategoryIfSize1(category) {
             const found = Object.entries(result).filter(([k,f]) => f.category === category);
             if (found.length==1) delete result[found[0][0]];
         }
-        deleteFiltersInCategoryThatAreEmpty('nested');
+        // deleteFiltersInCategoryThatAreEmpty('nested');
         deleteCategoryIfSize1('nested');
         deleteCategoryIfAllCountsAreEqualOrZero('type-tag');  // because all tags are on all tasks
 
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 39bd3a64..3d9dbc60 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
@@ -117,6 +117,7 @@ export function workflowStepDirective() {
                 $scope.stepContext = ($scope.isCurrentMaybeInactive ? workflow.data.currentStepInstance : $scope.osi.context) || {};
                 $scope.isFocusStep = $scope.workflow.tag && ($scope.workflow.tag.stepIndex === index);
                 $scope.isFocusTask = false;
+                $scope.isErrorHandler = $scope.workflow.tag && ($scope.workflow.tag.errorHandlerForTask);
 
                 $scope.stepCurrentError = (($scope.task || {}).currentStatus === 'Error') ? 'This step returned an error.'
                     : ($scope.isWorkflowError && $scope.isCurrentMaybeInactive) ? 'The workflow encountered an error around this step.'
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 4480b0c7..8d8f3e0f 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
@@ -110,7 +110,19 @@
                     </span>
                 </div>
 
-                <div ng-if="isFocusStep && !isFocusTask" class="space-above">
+                <div ng-if="isErrorHandler" class="space-above">
+                    The task on this page is for the error handler for this step.
+                    More details may be found in the other sections on this page.
+                </div>
+
+                <div ng-if="stepContext.errorHandlerTaskId && !isErrorHandler" class="space-above">
+                    The error triggered an error handler in
+                    <b><a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: stepContext.errorHandlerTaskId, workflowId})">
+                        <span class="monospace">task {{stepContext.errorHandlerTaskId}}</span
+                        ></a></b>.
+                </div>
+
+                <div ng-if="isFocusStep && !isFocusTask && !isErrorHandler" class="space-above">
                     <b>The activity currently being viewed (<span class="monospace">{{ task.id }}</span>) is for a previous run of this step.</b>
                 </div>
 
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 15c9dce9..ab49d345 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
@@ -232,7 +232,7 @@ function makeArrows(workflow, steps) {
             }
         }
 
-        for (var i = -1; i < steps.length - 1; i++) {
+        for (var i = -1; i < steps.length; i++) {
             const prevsHere = stepsPrev[i];
             if (prevsHere && prevsHere.length) {
                 prevsHere.forEach(prev => {
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
index 2ec31d57..449504e6 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/activities.controller.js
@@ -72,6 +72,7 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                 newActivitiesMap[activity.id] = activity;
             });
 
+            const workflowActivities = {}
             Object.values(vm.workflows || {})
                 .filter(wf => wf.replays && wf.replays.length)
                 .forEach(wf => {
@@ -84,7 +85,8 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                         if (!t) {
                             // create stub tasks for the replays of workflows
                             t = makeTaskStubFromWorkflowRecord(wf, wft);
-                            newActivitiesMap[wft.taskId] = t;
+                            workflowActivities[wft.taskId] = t;
+                            //newActivitiesMap[wft.taskId] = t;
                         }
                         t.workflowId = wf.workflowId;
                         t.workflowParentId = wf.parentId;
@@ -101,8 +103,23 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
                     lastTask.isWorkflowLastRun = true;
                 });
 
+            // workflow stubs need sorting by us
+            let workflowStubsToSort = Object.values(workflowActivities);
+            function firstDate(d1, d2, nextSupplier) {
+                if (d1==d2) return nextSupplier();
+                if (!(d1>0) && !(d2>0)) return nextSupplier();
+                if (d1>0 && d2>0) return d2-d1;
+                return d1>0 ? 1 : -1;
+            }
+            workflowStubsToSort.sort( (w1,w2) =>
+                firstDate(w1.endTimeUtc, w2.endTimeUtc,
+                    () => firstDate(w1.startTimeUtc, w2.startTimeUtc,
+                        () => firstDate(w1.submitTimeUtc, w2.submitTimeUtc,
+                            () => 0))) );
+            workflowStubsToSort.forEach(wst => newActivitiesMap[wst.id] = wst);
+
             vm.activitiesMap = newActivitiesMap;
-            vm.activities = Object.values(vm.activitiesMap);
+            vm.activities = Object.values(newActivitiesMap);
         }
     }
 
@@ -178,7 +195,7 @@ function ActivitiesController($scope, $state, $stateParams, $log, $timeout, enti
 export function makeTaskStubFromWorkflowRecord(wf, wft) {
     const result = {
         id: wft.taskId,
-        displayName: wf.name + (wft.reasonForReplay ? " ("+wft.reasonForReplay+")" : ""),
+        displayName: wf.name + (wft.reasonForReplay && wft.reasonForReplay!="initial run" ? " ("+wft.reasonForReplay+")" : ""),
         entityId: (wf.entity || {}).id,
         isError: wft.isError===false ? false : true,
         currentStatus: _.isNil(wft.isError) ? "Unavailable" : wft.status,
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 c994e19a..79476af8 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
@@ -106,7 +106,7 @@ function DetailController($scope, $state, $stateParams, $location, $log, $uibMod
                     // 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 run appears to have been interrupted, either by a server restart or a failure or cancellation and removal from memory."
                             : 'The workflow is known but this task is no longer stored in memory.'));
                 }
 
@@ -171,7 +171,7 @@ function DetailController($scope, $state, $stateParams, $location, $log, $uibMod
                             else if (replayableContinuing) w2 = '';
 
                             $scope.actions.workflowReplays.push({targetId: 'start', reason: 'Restart workflow from UI',
-                                label: 'Restart '+(stepIndex>=0 ? 'workflow ' : '')+reason});
+                                label: w1+' '+(stepIndex>=0 ? 'workflow ' : '')+w2});
                         }
 
                         if (!replayableFromStart) {
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 f72d2922..c26d5989 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
@@ -234,7 +234,7 @@
     }
     div.workflow-preface-para {
         margin-top: 12px;
-        margin-bottom: 24px;
+        margin-bottom: 12px;
     }
 }
 
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 74cdf90b..b8c245c6 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
@@ -118,10 +118,10 @@
                                 <div class="col-md-3 summary-item summary-item-timestamp">
                                     <div class="summary-item-label">Submitted</div>
                                     <div class="summary-item-value">
-                                        <div class="humanized fade" ng-show="!showUTC">
+                                        <div class="humanized fade" ng-show="!showUTC || !(vm.model.activity.submitTimeUtc>0)">
                                             {{vm.model.activity.submitTimeUtc | timeAgoFilter}}
                                         </div>
-                                        <div class="utcTime fade" ng-show="showUTC">
+                                        <div class="utcTime fade" ng-show="showUTC && vm.model.activity.submitTimeUtc>0">
                                             {{vm.model.activity.submitTimeUtc | dateFilter }}
                                         </div>
                                     </div>
@@ -129,10 +129,10 @@
                                 <div class="col-md-3 summary-item summary-item-timestamp">
                                     <div class="summary-item-label">Started</div>
                                     <div class="summary-item-value">
-                                        <div class="humanized fade" ng-show="!showUTC">
+                                        <div class="humanized fade" ng-show="!showUTC || !(vm.model.activity.startTimeUtc>0)">
                                             {{vm.model.activity.startTimeUtc | timeAgoFilter}}
                                         </div>
-                                        <div class="utcTime fade" ng-show="showUTC">
+                                        <div class="utcTime fade" ng-show="showUTC && vm.model.activity.startTimeUtc>0">
                                             {{vm.model.activity.startTimeUtc | dateFilter }}
                                         </div>
                                     </div>
@@ -140,10 +140,10 @@
                                 <div ng-if="vm.model.activity.endTimeUtc" class="col-md-3 summary-item summary-item-timestamp">
                                     <div class="summary-item-label">Finished</div>
                                     <div class="summary-item-value">
-                                        <div class="humanized fade" ng-show="!showUTC">
+                                        <div class="humanized fade" ng-show="!showUTC || !(vm.model.activity.endTimeUtc>0)">
                                             {{vm.model.activity.endTimeUtc | timeAgoFilter}}
                                         </div>
-                                        <div class="utcTime fade" ng-show="showUTC">
+                                        <div class="utcTime fade" ng-show="showUTC && vm.model.activity.endTimeUtc>0">
                                             {{vm.model.activity.endTimeUtc | dateFilter }}
                                         </div>
                                     </div>
@@ -210,7 +210,7 @@
                                         </ul>
                                     </div>
                                 </div>
-                                <div style="margin-top: 12px; margin-bottom: 24px;">
+                                <div class="workflow-preface-para">
                                     This task is
                                     <span ng-if="!vm.isNullish(vm.model.workflow.tag.stepIndex)">for step <b>{{ vm.model.workflow.tag.stepIndex+1 }}</b>
                                         in
@@ -255,8 +255,21 @@
                                     ></a></b>.
                                 </div>
 
+                                <div ng-if="vm.model.workflow.data.errorHandlerTaskId" class="workflow-preface-para">
+                                    <span ng-if="vm.model.activityId==vm.model.workflow.data.errorHandlerTaskId">
+                                        This is the page for the error handler for this workflow.
+                                        More details of how the error was handled can be found in other sections on this page.
+                                    </span>
+                                    <span ng-if="vm.model.activityId!=vm.model.workflow.data.errorHandlerTaskId">
+                                        This workflow had an error which ran error handler
+                                        <b><a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.data.errorHandlerTaskId, workflowId})">
+                                            <span class="monospace">task {{vm.model.workflow.data.errorHandlerTaskId}}</span
+                                        ></a></b>.
+                                    </span>
+                                </div>
+
                                 <div ng-if="vm.model.workflow.runIsOld" class="workflow-preface-para">
-                                    For previous runs, the subtask view
+                                    For previous runs such as this, 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
diff --git a/ui-modules/app-inspector/app/views/main/inspect/confirm.modal.template.html b/ui-modules/app-inspector/app/views/main/inspect/confirm.modal.template.html
index 97e443b8..a7a7567d 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/confirm.modal.template.html
+++ b/ui-modules/app-inspector/app/views/main/inspect/confirm.modal.template.html
@@ -29,5 +29,5 @@
 
 <div class="modal-footer">
     <button class="btn btn-default" ng-click="$dismiss('Close modal')" type="button">Cancel</button>
-    <button class="btn btn-danger" ng-click="$close()" type="button">Ok</button>
+    <button class="btn btn-danger" ng-click="$close()" type="button">OK</button>
 </div>
diff --git a/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js b/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
index 636825e9..e3ab4fe2 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/inspect.controller.js
@@ -26,11 +26,11 @@ export const inspectState = {
     name: 'main.inspect',
     url: 'application/:applicationId/entity/:entityId',
     template: template,
-    controller: ['$scope', '$stateParams', '$uibModal', 'brSnackbar', 'entityApi', inspectController],
+    controller: ['$scope', '$state', '$stateParams', '$uibModal', 'brSnackbar', 'entityApi', inspectController],
     controllerAs: 'vm'
 };
 
-export function inspectController($scope, $stateParams, $uibModal, brSnackbar, entityApi) {
+export function inspectController($scope, $state, $stateParams, $uibModal, brSnackbar, entityApi) {
     const {
         applicationId,
         entityId
@@ -68,7 +68,7 @@ export function inspectController($scope, $stateParams, $uibModal, brSnackbar, e
                 entityId: ()=>(entityId),
             }
         }).result.then((closeData)=> {
-            $state.go('main.inspect.activites', {
+            $state.go('main.inspect.activities.detail', {
                 applicationId: applicationId,
                 entityId: closeData.entityId,
                 activityId: closeData.id
@@ -87,7 +87,7 @@ export function inspectController($scope, $stateParams, $uibModal, brSnackbar, e
                 entityId: ()=>(entityId),
             }
         }).result.then((closeData)=> {
-            $state.go('main.inspect.activites', {
+            $state.go('main.inspect.activities.detail', {
                 applicationId: applicationId,
                 entityId: closeData.entityId,
                 activityId: closeData.id


[brooklyn-ui] 15/24: fix a couple things on step arrows

Posted by he...@apache.org.
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 6c0644381b6970519abcff6e8b9b1094975904b6
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Sat Oct 8 16:10:21 2022 +0100

    fix a couple things on step arrows
    
    - conditions and next for theoretical steps
    - early return
    - code tidy
---
 .../workflow/workflow-steps.directive.js           | 101 ++++++++++-----------
 1 file changed, 50 insertions(+), 51 deletions(-)

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 e81fa495..32ca366e 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
@@ -40,7 +40,6 @@ export function workflowStepsDirective() {
 
     function controller($sce, $timeout, $scope, $element) {
         let vm = this;
-        //console.log("controller for workflow steps", $scope.workflow);
 
         vm.stringify = stringify;
 
@@ -180,29 +179,6 @@ function makeArrows(workflow, steps) {
             return arrowSvg(s1, s2, opts);
         }
 
-        function colorFor(step, references) {
-            if (!references) return 'red';
-            const i = references.indexOf(step);
-            if (i==-1) return 'red';
-            // skew quadratically for lightness
-            const skewTowards1 = x => (1 - (1-x)*(1-x));
-            let gray = Math.round(240 * skewTowards1(i / references.length) );
-            return 'rgb('+gray+','+gray+','+gray+')';
-        }
-
-        let jumpSizes = {1: 0};
-        for (var i = -1; i < steps.length - 1; i++) {
-            const prevsHere = stepsPrev[i];
-            if (prevsHere && prevsHere.length) {
-                prevsHere.forEach(prev => {
-                    if (i!=-1 && prev!=-1 && i!=prev) {
-                        jumpSizes[Math.abs(prev - i)] = true;
-                    }
-                });
-            }
-        }
-        jumpSizes = Object.keys(jumpSizes).sort();
-
         function arrowStep2(prev, i, opts) {
             let curveX = 0.5;
             let curveY = 0.75;
@@ -230,19 +206,34 @@ function makeArrows(workflow, steps) {
             return arrowStep(prev, i, {hideArrowhead: prev==i, width, curveX, curveY, ...opts});
         }
 
-        for (var i = -1; i < steps.length; i++) {
+        function colorFor(step, references) {
+            if (!references) return 'red';
+            const i = references.indexOf(step);
+            if (i==-1) return 'red';
+            // skew quadratically for lightness
+            const skewTowards1 = x => (1 - (1-x)*(1-x));
+            let gray = Math.round(240 * skewTowards1(i / references.length) );
+            return 'rgb('+gray+','+gray+','+gray+')';
+        }
+
+        let jumpSizes = {1: true};
+        let arrowSpecs = {};
+        function recordTransition(from, to, opts) {
+            if (to!=-1 && from!=-1 && to!=from) {
+                jumpSizes[Math.abs(from-to)] = true;
+            }
+            arrowSpecs[[from,to]] = { from, to, ...(opts||{}) };
+        }
+
+        for (var i = -1; i < steps.length - 1; i++) {
             const prevsHere = stepsPrev[i];
             if (prevsHere && prevsHere.length) {
-                let insertionPoint = 0;
                 prevsHere.forEach(prev => {
-                    const colorStart = colorFor(i, stepsNext[prev]);
-                    const colorEnd = colorFor(prev, prevsHere);
-
                     // last in list has higher z-order; this ensures within each prevStep we preserve order,
                     // so inbound arrows are correct. currently we also prefer earlier steps, which isn't quite right for outbound arrows;
                     // ideally we'd reconstruct the flow order, but that's a bit more work than we want to do just now.
                     // so insertion point is always 0. (header items added at end so we don't need to include those here.)
-                    arrows.splice(insertionPoint, 0, arrowStep2(prev, i, { colorStart, colorEnd }));
+                    recordTransition(prev, i, { insertionPoint: 0, visited: true, colorStart: colorFor(i, stepsNext[prev]), colorEnd: colorFor(prev, prevsHere) });
                 });
             }
         }
@@ -251,37 +242,45 @@ function makeArrows(workflow, steps) {
         var indexOfId = {};
         for (var i = 0; i < steps.length; i++) {
             const s = workflow.data.stepsDefinition[i];
-            if (s.id) {
-                indexOfId[s.id] = i;
-            }
+            if (s.id) indexOfId[s.id] = i;
         }
-        if (steps.length>0) {
-            arrows.splice(0, 0, arrowStep2(-1, 0, {color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 }));
+
+        function isStepType(step, type) {
+            if (!step) return false;
+            if (step.type) return step.type == type;
+            let s = step.startsWith ? step : step.s || step.shorthand || step.userSuppliedShorthand;
+            if (s) return s == type || s.startsWith(type);
+            return false;
         }
+
         for (var i = 0; i < steps.length; i++) {
             const s = workflow.data.stepsDefinition[i];
-            var next = null;
+            let next = null;
             if (s.next) {
-                if (indexOfId[s.next]) {
-                    next = indexOfId[s.next];
-                } else {
-                    next = null;
-                }
-            } else {
-                if (s.type === 'return' || (s.userSuppliedShorthand && s.userSuppliedShorthand.startsWith("return"))) {
-                    next = -1;
-                } else {
-                    next = i + 1;
-                    if (next >= steps.length) next = -1;  //end
-                }
+                if (s.next.toLowerCase()=='end') next = -1;
+                else if (indexOfId[s.next]) next = indexOfId[s.next];
             }
-            if (next!=null) arrows.splice(0, 0, arrowStep2(i, next, { color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 }));
+            if (isStepType(s, 'return')) next = -1;
+            if (next!=null) {
+                // special next per step
+                recordTransition(i, next, { insertionPoint: 0, color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 });
+                if (!s.condition) continue;
+            }
+            // if nothing special, or if was conditional, then go to next step
+            next = i+1;
+            if (i + 1 >= steps.length) next = -1;
+
+            recordTransition(i, next, { insertionPoint: 0, color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 });
         }
 
-        // put defs at start
+        jumpSizes = Object.keys(jumpSizes).sort();
+
+        // insert arrows
+        Object.values(arrowSpecs).forEach(arrowSpec =>
+            arrows.splice(arrowSpec.insertionPoint || 0, 0, arrowStep2(arrowSpec.from, arrowSpec.to, arrowSpec)) );
+        // then defs at start
         arrows.splice(0, 0, '<defs>'+defs.join('')+'</defs>');
     }
-
     return arrows;
 }