in ui-modules/app-inspector/app/components/task-list/task-list.directive.js [300:755]
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;
}