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 <[email protected]>
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;

Reply via email to