function setFiltersForTasks()

in ui-modules/app-inspector/app/components/task-list/task-list.directive.js [301:756]


    function setFiltersForTasks(scope, isActivityChildren) {
        const tasksAll = scope.tasks || [];
        const globalFilters = scope.globalFilters;

        // include a toggle for transient tasks
        if (!globalFilters.transient) {
            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 = {
                    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.checked = !globalFilters.transient.include;
                        setFiltersForTasks(scope, isActivityChildren);
                    },
                    category: 'status',
                    categoryForEvaluation: 'status-transient',
                };
                globalFilters.transient.action();
            }
        }

        const tasks = tasksAfterGlobalFilters(tasksAll, globalFilters);

        function defaultToggleFilter(tag, filter, forceValue, fromUi, skipRecompute) {
            if ((scope.filters.selectedFilters[tag] && _.isNil(forceValue)) || forceValue===false) {
                delete scope.filters.selectedFilters[tag];
                if (filter.onDisabledPost) filter.onDisabledPost(tag, filter, forceValue);
            } else {
                if (filter.onEnabledPre) filter.onEnabledPre(tag, filter, forceValue);
                scope.filters.selectedFilters[tag] = filter;
            }
            if (fromUi) {
                // on a UI click, don't try to be too clever about remembered IDs
                scope.uiDropdownInteraction = true;
            }

            if (!skipRecompute) scope.recomputeTasks();
        }

        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];
                    }
                });
            }
        }
        function clearOther(idToClear) {
            return function(filterId, filter, forceValue) {
                delete scope.filters.selectedFilters[idToClear];
            }
        }
        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;
                }
            }
        }
        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 filtersFullList = {};

        let tasksById = tasksAll.reduce( (result,t) => { result[t.id] = t; return result; }, {} );
        const isChild = (t,tbyid) => {
            if (!t.submittedByTask) return false;
            return (t.submittedByTask.metadata.id == scope.parentTaskId);
        };
        const isNotChild = (t,tbyid) => !isChild(t, tbyid);
        function filterTopLevelTasks(tasks) {
            return filterWithId(tasks, tasksById, isActivityChildren ? isChild : isTopLevelTask);
        }
        function filterNonTopLevelTasks(tasks) {
            return filterWithId(tasks, tasksById, isActivityChildren ? isNotChild : isNonTopLevelTask);
        }
        function filterCrossEntityTasks(tasks) { return filterWithId(tasks, tasksById, isCrossEntityTask); }
        function filterNestedSameEntityTasks(tasks) { return filterWithId(tasks, tasksById, isNestedSameEntityTask); }

        scope.filters.startingSetFilterForCategory = {
            nested: filterTopLevelTasks,
        };
        function getFilterOrEmpty(id) {
            return id && (id.filter ? id : scope.filters.available[id]) || {};
        }
        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 (assume _top is always shown)
                    let statusFiltersEnabled = Object.values(scope.filters.selectedFilters).filter(f => f.category === 'status');
                    if (statusFiltersEnabled.length) return [ 'some' ];  // if filters applied, indicate that
                    else return [ 'all' ];
                }
                if (set.length > 1) return [ 'some' ];  // gets too big otherwise
                return set.map(s => s.displaySummary || '');
            },
            'type-tag': set => {
                if (!set || !set.length) return null;
                if (set.length<=3) {

                    if (scope.filters.selectedFilters['_all_effectors'] &&
                            Object.values(scope.filters.selectedFilters).filter(f => f.category === 'nested').length==1) {
                        // if all_effectors is the only nesting don't show '(effectors) (effector)'
                        set = set.filter(x => x.displaySummary != 'effector');  // don't show 'effectors' and effector
                    }

                    return set.map(s => (getFilterOrEmpty(s).displaySummary || '').toLowerCase()).filter(x => x);
                } else {
                    return ['any of '+set.length+' tags'];
                }
            },
        };
        filtersFullList['_top'] = {
            display: 'Only list ' + (isActivityChildren ? 'children sub-tasks' : 'top-level tasks'),
            displaySummary: 'only top-level',
            isDefault: true,
            filter: filterTopLevelTasks,  // redundant with starting set, but contributes the right count
            category: 'nested',
            onEnabledPre: clearCategory(),
            onDisabledPost: enableOthersIfCategoryEmpty('_top'),
            includeIfZero: true,
        }
        if (!isActivityChildren) {
            filtersFullList['_cross_entity'] = {
                display: 'Include cross-entity sub-tasks',
                displaySummary: 'cross-entity',
                filter: filterCrossEntityTasks,
                category: 'nested',
                onEnabledPre: clearOther('_top'),
                onDisabledPost: enableFilterIfCategoryEmpty('_top'),
            }
            filtersFullList['_recursive'] = {
                display: 'Include local sub-tasks',
                displaySummary: 'local',
                filter: filterNestedSameEntityTasks,
                category: 'nested',
                onEnabledPre: clearOther('_top'),
                onDisabledPost: enableFilterIfCategoryEmpty('_top'),
            }
            filtersFullList['_all_effectors'] = {
                display: 'Include effector sub-tasks',
                displaySummary: 'effectors',
                filter: filterForTasksWithTag('EFFECTOR'),
                category: 'nested',
                onEnabledPre: clearOther('_top'),
                onDisabledPost: enableFilterIfCategoryEmpty('_top'),
            }
        } else {
            filtersFullList['_recursive'] = {
                display: 'Include recursive sub-tasks',
                displaySummary: 'recursive',
                filter: filterNonTopLevelTasks,
                category: 'nested',
                onEnabledPre: clearOther('_top'),
                onDisabledPost: enableFilterIfCategoryEmpty('_top'),
            }
        }

        filtersFullList['_anyTypeTag'] = {
            display: 'Any task type or tag',
            displaySummary: null,
            filter: input => input,
            category: 'type-tag',
            onEnabledPre: clearCategory(),
            onDisabledPost: enableOthersIfCategoryEmpty('_anyTypeTag'),
        }

        filtersFullList['_periodic'] = {
            display: 'Periodic',
            displaySummary: 'periodic',
            filter: tasks => tasks.filter(t => isScheduled(t)),
            category: 'type-tag',
            onEnabledPre: clearOther('_anyTypeTag'),
            onDisabledPost: enableFilterIfCategoryEmpty('_anyTypeTag'),
        }

        function addTagFilter(tag, target, display, extra) {
            if (!target[tag]) target[tag] = {
                display: display,
                displaySummary: tag.toLowerCase(),
                filter: filterForTasksWithTag(tag),
                category: 'type-tag',
                onEnabledPre: clearOther('_anyTypeTag'),
                onDisabledPost: enableFilterIfCategoryEmpty('_anyTypeTag'),
                ...(extra || {}),
            }
        }

        // put these first if present, to get this order, then remove if false
        if (!isActivityChildren) {
            addTagFilter('EFFECTOR', filtersFullList, 'Effectors', {displaySummary: 'effector', includeIfZero: true});
            addTagFilter('WORKFLOW', filtersFullList, 'Workflow', { includeIfZero: true });
        } else {
            filtersFullList['EFFECTOR'] = false;
            filtersFullList['WORKFLOW'] = false;
        }
        filtersFullList['SENSOR'] = false;
        filtersFullList['INITIALIZATION'] = false;
        filtersFullList['SUB-TASK'] = false;
        filtersFullList['TOP-LEVEL'] = false;
        filtersFullList['inessential'] = false;

        // add filters for other tags
        let tags = _.uniq(tasks.flatMap(t => (t.tags || []).filter(tag => typeof tag === 'string' && tag.length < 32)));
        tags.sort( (t1,t2) => t1.toLowerCase().localeCompare(t2.toLowerCase()) );
        // same tag with different cases will be shown multiple times, unable to disambiguate, but that's unlikely
        tags.forEach(tag => addTagFilter(tag, filtersFullList, 'Tag: ' + tag.toLowerCase()) );

        Object.entries(filtersFullList).forEach(([k,v]) => { if (!v) delete filtersFullList[k]; });
        ['EFFECTOR', 'WORKFLOW', 'SUB-TASK', 'SENSORS', 'INITIALIZATION'].forEach(t => { if (!filtersFullList[t]) delete filtersFullList[t]; });
        (filtersFullList['SENSOR'] || {}).display = 'Sensors';
        (filtersFullList['INITIALIZATION'] || {}).display = 'Initialization';
        (filtersFullList['SUB-TASK'] || {}).display = 'Sub-tasks';
        (filtersFullList['TOP-LEVEL'] || {}).display = 'Important';
        (filtersFullList['TOP-LEVEL'] || {}).displaySummary = 'Important';
        (filtersFullList['inessential'] || {}).display = 'Non-essential';

        filtersFullList['_active'] = {
            display: 'Only show active tasks',
            displaySummary: 'active',
            filter: tasks => tasks.filter(t => !t.endTimeUtc || t.endTimeUtc<0),
            category: 'status',
            categoryForEvaluation: 'status-active',
        }
        if (scope.entityId) {
            filtersFullList['_other_entity'] = {
                display: 'Exclude tasks on other entities',
                displaySummary: 'other-entity',
                filter: tasks => tasks.filter(t => t.entityId === scope.entityId),
                category: 'status',
                categoryForEvaluation: 'other-entity',
                hideBadges: true, // counts don't interact with other filters so it is confusing
            }
        }
        filtersFullList['_scheduled_sub'] = {
            display: 'Only show periodic tasks',
            displaySummary: 'periodic',
            help: 'If debugging a scheduled repeating task such as a policy or sensor, it can be helpful to show only those tasks.',
            filter: tasks => tasks.filter(t => {
                // show scheduled tasks (the parent) and each scheduled run, if sub-tasks are selected
                // if (!t || !t.submittedByTask) return false;  // omit the parent
                if (isScheduled(t, taskId => tasksById[taskId])) return true;
            }),
            category: 'status',
            categoryForEvaluation: 'status-scheduled',
            onEnabledPre: clearOther('_non_scheduled_sub'),
        }
        filtersFullList['_non_scheduled_sub'] = {
            display: 'Exclude periodic sub-tasks',
            displaySummary: 'non-repeating',
            help: 'If there are a lot of repeating tasks, it can be helpful to filter them out '+
                'to find manual and triggers tasks more easily.',
            filter: tasks => tasks.filter(t => {
                return !isScheduled(t, taskId => tasksById[taskId]) ||
                    isScheduled(t) /* allow root periodic task */;
            }),
            category: 'status',
            categoryForEvaluation: 'status-scheduled',
            onEnabledPre: clearOther('_scheduled_sub'),
            hideBadges: true, // counts don't interact with other filters so it is confusing
        }

        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,
            hideBadges: true, // counts don't interact with other filters so it is confusing
        }

        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,
            hideBadges: true, // counts don't interact with other filters so it is confusing
        }

        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: 'workflow-non-last-replays',
            category: 'workflow',
            hideBadges: true, // counts don't interact with other filters so it is confusing
        }

        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(t => !filterWorkflowsWithoutTaskWhichAreCompleted(t)),
            count: countWorkflowsWithoutTaskWhichAreCompleted,
            countAbsolute: countWorkflowsWithoutTaskWhichAreCompleted,
            categoryForEvaluation: 'workflow-old-completed',
            category: 'workflow',
            hideBadges: true, // counts don't interact with other filters so it is confusing
        }

        const filterWorkflowTasksWhichAreSteps = t => getTaskWorkflowTag(t) && !_.isNil(getTaskWorkflowTag(t));
        const countWorkflowTasksWhichAreSteps = tasksAll.filter(filterWorkflowTasksWhichAreSteps).length;
        filtersFullList['_workflowStepsHidden'] = {
            display: 'Exclude individual workflow steps',
            help: 'Individual steps within workflows are hidden in most views, except where showing workflow tasks. ' +
                'This makes it easier to navigate to primary tasks, such as workflows, and from there explore the steps within. ' +
                'If this option is disabled and if nested sub-tasks are enabled, then individual steps will be listed in this view ' +
                'to facilitate finding a specific step.',
            displaySummary: null,
            filter: tasks => tasks.filter(t => _.isNil((getTaskWorkflowTag(t) || {}).stepIndex)),
            count: countWorkflowTasksWhichAreSteps,
            countAbsolute: countWorkflowTasksWhichAreSteps,
            categoryForEvaluation: 'workflow-steps',
            category: 'workflow',
            hideBadges: true, // counts don't interact with other filters so it is confusing
        }

        // fill in fields
        function updateSelectedFilters(newValues) {
            //console.debug("selected filters were", Object.keys(scope.filters.selectedFilters));
            Object.entries(scope.filters.selectedFilters).forEach(([filterId, oldValue]) => {
                const newValue = newValues[filterId];
                scope.filters.selectedFilters[filterId] = newValue;
                if (!newValue) delete scope.filters.selectedFilters[filterId];
            });
            //console.debug("selected filters now", Object.keys(scope.filters.selectedFilters));
        }

        updateSelectedFilters(filtersFullList);

        // add counts
        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 = {};
        // include non-zero filters or those included if zero
        Object.entries(filtersFullList).forEach(([k, f]) => {
            if (f.countAbsolute > 0 || f.includeIfZero) result[k] = f;
            //else console.debug("Removing filter", f.display);
        });

        // and delete categories that are redundant
        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])=> {
                    //console.debug("Removing category filter", f.display);
                    delete result[k];
                });
            }
        }
        // function deleteFiltersInCategoryThatAreEmpty(category) {
        //     // redundant with population of 'result' above
        //     Object.entries(result).filter(([k,f]) => f.category === category && f.countAbsolute==0 && !f.includeIfZero).forEach(([k,f])=>delete result[k]);
        // }
        function deleteCategoryIfSize1(category) {
            const found = Object.entries(result).filter(([k,f]) => f.category === category);
            if (found.length==1) {
                delete result[found[0][0]];
                //console.debug("Removing size 1 category", found[0][0]);
            }
        }
        // deleteFiltersInCategoryThatAreEmpty('nested');
        deleteCategoryIfSize1('nested');
        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
            result['_recursive'].display = 'Include nested 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)) {
            let thisCat = v.categoryForDisplay || v.category;
            if (lastCat!=null && lastCat!=thisCat) {
                v.classes = (v.classes || '') + ' divider-above';
            }
            lastCat = thisCat;
        }

        scope.filters.available = result;
        updateSelectedFilters(result);

        return result;
    }