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

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

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>