ui-modules/app-inspector/app/components/task-list/task-list.directive.js (707 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import angular from "angular"; import {fromNow, duration} from "brooklyn-ui-utils/utils/momentp"; import moment from "moment"; import template from "./task-list.template.html"; import {getTaskWorkflowTag} from "../../views/main/inspect/activities/detail/detail.controller"; const MODULE_NAME = 'inspector.task-list'; angular.module(MODULE_NAME, []) .directive('taskList', taskListDirective) .filter('timeAgoFilter', timeAgoFilter) .filter('dateFilter', dateFilter) .filter('durationFilter', durationFilter) .filter('activityFilter', ['$filter', activityFilter]); export default MODULE_NAME; export function taskListDirective() { return { template: template, restrict: 'E', scope: { tasks: '=', tasksLoaded: '<?', // if tasks might complete initial loading late, the caller should pass a watchable expression that resolves to true when initially loaded taskType: '@?', parentTaskId: '@?', filteredCallback: '&?', search: '<', entityId: '<?', contextKey: '@?', // a key to uniquely identify the calling context to save filter settings activityColumnTitle: '@?', }, controller: ['$scope', '$element', controller] }; function controller($scope, $element) { const isActivityChildren = !! $scope.parentTaskId; // selected filters are shared with other views esp kilt view so they can see what is and isn't included. // currently only used for transient. $scope.globalFilters = { // transient set when those tags seen }; $scope.isEmpty = x => _.isNil(x) || x.length==0 || (typeof x === "object" && Object.keys(x).length==0); $scope.model = { appendTo: $element, filterResult: null, }; $scope.tasksFilteredByTag = []; $scope.findTasksExcludingCategory = (tasks, selected, categoryToExclude) => { let result = tasks || []; if (selected) { _.uniq(Object.values(selected).map(f => f.categoryForEvaluation || f.category)).forEach(category => { if (categoryToExclude === '' || categoryToExclude != category) { let newResult = []; if ($scope.filters.startingSetFilterForCategory[category]) { newResult = $scope.filters.startingSetFilterForCategory[category](result); } Object.values(selected).filter(f => (f.categoryForEvaluation || f.category) === category).forEach(f => { const filter = f.filter; if (!filter) { console.warn("Incomplete activities tag filter", f); } 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, '') .sort((t1,t2) => { if (!t1.endTimeUtc || !t2.endTimeUtc) { if (!t1.endTimeUtc && !t2.endTimeUtc) return t2.startTimeUtc - t1.startTimeUtc; if (t1.endTimeUtc) return 1; return -1; } return t2.endTimeUtc - t1.endTimeUtc || // if same end time, sort by start time (t2.startTimeUtc && t1.startTimeUtc && t2.startTimeUtc - t1.startTimeUtc) || (t2.submitTimeUtc && t1.submitTimeUtc && t2.submitTimeUtc - t1.submitTimeUtc); }); // do this to update the counts setFiltersForTasks($scope, isActivityChildren); // now update name const enabledCategories = _.uniq(Object.values($scope.filters.selectedFilters).map(f => f.category)); $scope.filters.selectedDisplay = []; Object.entries($scope.filters.displayNameFunctionForCategory).forEach(([category, nf]) => { if (!enabledCategories.includes(category)) return null; let badges = nf ? nf(Object.values($scope.filters.selectedFilters).filter(f => (f.categoryForBadges || f.category) === category)) : null; badges = (badges || []).filter(x=>x); if (badges.length) $scope.filters.selectedDisplay.push({ class: 'dropdown-category-'+category, badges }); }); if (!$scope.filters.selectedDisplay.length) $scope.filters.selectedDisplay.push({ class: 'dropdown-category-default', badges: ['all'] }); }; function selectFilter(filterId, explicitNewValueOrUndefinedForToggle) { //console.debug("selecting filter: "+filterId+" = "+explicitNewValueOrUndefinedForToggle); const f = $scope.filters.available[filterId]; if (!f) { //console.debug("selected filter not found; available are", $scope.filters.available); // we tried to select eg effector, when it didn't exist, just ignore return false; } else { f.select(filterId, f, explicitNewValueOrUndefinedForToggle); // see defaultToggleFilter for params return true; } } $scope.clickFilter = (filter, tag) => { filter.onClick(tag, filter); if ($scope.contextKey) { try { const filters = JSON.stringify(Object.keys($scope.filters.selectedFilters)); const storageKey = 'brooklyn-task-list-filters-' + $scope.contextKey; sessionStorage.setItem(storageKey, filters); //console.debug("Saved filters to session storage", storageKey, filters); } catch (e) { console.warn("Unable to save filiters from session storage for", $scope.contextKey, e); } } } $scope.filterValue = $scope.search; $scope.isScheduled = isScheduled; function roundChangingMillis(x) { if (x<100) return "-"; if (x<1000) return Math.round(x/100)*100; return Math.round(x/1000)*1000; } $scope.getTaskDuration = function(task) { if (!task.startTimeUtc) { return null; } if (!_.isNil(task.endTimeUtc) && task.endTimeUtc <= 0) return null; return (task.endTimeUtc === null ? roundChangingMillis(new Date().getTime() - task.startTimeUtc) : task.endTimeUtc - task.startTimeUtc); } $scope.getTaskWorkflowId = task => { const tag = getTaskWorkflowTag(task); if (tag) return tag.workflowId; return null; }; $scope.$watch('model.filterResult', function () { if ($scope.filteredCallback && $scope.model.filterResult) $scope.filteredCallback()( $scope.model.filterResult, $scope.globalFilters ); }); let tasksLoadedTrueReceived = false; let filtersFromSessionStorage = 'initializing'; // | 'absent' | 'loaded' $scope.resetFilters = () => { tasksLoadedTrueReceived = false; $scope.uiDropdownInteraction = false; filtersFromSessionStorage = 'initializing'; sessionStorage.removeItem('brooklyn-task-list-filters-' + $scope.contextKey) refreshDropdownsUntilTasksAreLoaded(); } function refreshDropdownsUntilTasksAreLoaded() { if (tasksLoadedTrueReceived || $scope.uiDropdownInteraction) return; tasksLoadedTrueReceived = $scope.tasksLoaded; let preselectedFilters; if (filtersFromSessionStorage=='initializing') { if (!$scope.contextKey) filtersFromSessionStorage = 'absent'; else { filtersFromSessionStorage = 'absent'; try { const filters = sessionStorage.getItem('brooklyn-task-list-filters-' + $scope.contextKey); if (filters) { // console.debug("Read filters for", $scope.contextKey, filters); preselectedFilters = JSON.parse(filters); } } catch (e) { console.warn("Unable to load filiters from session storage for", $scope.contextKey, e); } } } if (filtersFromSessionStorage=='loaded') { // don't auto-compute if taken from session storage } else { $scope.filters = {available: {}, selectedFilters: {}}; setFiltersForTasks($scope, isActivityChildren); if (preselectedFilters) { try { if ($scope.selectedFilters) Object.entries($scope.selectedFilters, (k,v) => selectFilter(k, v, false)); $scope.selectedFilters = {}; preselectedFilters.forEach(fid => { const f = $scope.filters.available[fid]; if (!f) { // don't keep retrying the load, unless tasks aren't loaded yet if (!$scope.tasksLoaded) { filtersFromSessionStorage = 'initializing'; // we don't have all the filters yet } } else { selectFilter(fid, f, true); } }); filtersFromSessionStorage = 'loaded'; } catch (e) { filtersFromSessionStorage = 'absent'; console.warn("Unable to process filiters from session storage for", $scope.contextKey, preselectedFilters, e); } } if (filtersFromSessionStorage == 'absent') { selectFilter("_top", true); selectFilter("_anyTypeTag", true); if ($scope.taskType) { if ($scope.taskType == "ALL") { selectFilter("_top", false); } else { selectFilter($scope.taskType); } } else { selectFilter('_cross_entity'); selectFilter('_all_effectors'); selectFilter('TOP-LEVEL'); selectFilter('EFFECTOR'); selectFilter('WORKFLOW'); selectFilter('_periodic'); selectFilter('_other_entity'); if (isActivityChildren) { // in children mode we also want sub-tasks // (previously selected no filters in subtask view) selectFilter('SUB-TASK'); } } if (!isActivityChildren) selectFilter("_workflowStepsHidden"); selectFilter("_workflowReplayedTopLevel"); selectFilter("_workflowNonLastReplayHidden"); selectFilter("_workflowCompletedWithoutTaskHidden"); // pick other filter combos until we get some conetnt if ($scope.tasksFilteredByTag.length == 0) { selectFilter('INITIALIZATION'); } if ($scope.tasksFilteredByTag.length == 0) { selectFilter("_anyTypeTag", true); } if (!isActivityChildren && $scope.tasksFilteredByTag.length == 0) { selectFilter("_top", false); } } } $scope.recomputeTasks(); } $scope.$watch('tasks', ()=>{ $scope.recomputeTasks(); }); $scope.$watch('globalFilters', ()=>{ $scope.recomputeTasks(); }); $scope.$watch('tasksLoaded', v => { refreshDropdownsUntilTasksAreLoaded(); }); refreshDropdownsUntilTasksAreLoaded(); } 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; } } const filterWorkflowsReplayedNested = t => !t.isWorkflowFirstRun && t.isWorkflowLastRun && !t.isWorkflowTopLevel; function isScheduled(task, optionalSubmitterFnIfSubmittersWanted) { if (task && task.currentStatus && task.currentStatus.startsWith("Schedule")) return true; if (!task || !task.submittedByTask || !optionalSubmitterFnIfSubmittersWanted) return false; let submitter = optionalSubmitterFnIfSubmittersWanted(task.submittedByTask.metadata.id); return isScheduled(submitter, optionalSubmitterFnIfSubmittersWanted); } function isTopLevelTask(t, tasksById) { if (filterWorkflowsReplayedNested(t)) return false; if (!t.submittedByTask) return true; if (t.forceTopLevel) return true; if (t.tags && t.tags.includes("TOP-LEVEL")) return true; let submitter = tasksById[t.submittedByTask.metadata.id]; // we could include those which are submitted but the submitter is forgotten // (but they are accesible as CrossEntity or NestedSameEntity so don't include for now) //if (!submitted) return true; // active scheduled tasks //if (isScheduled(submitter) && (!t.endTimeUtc || t.endTimeUtc<=0)) return true; return false; } function isNonTopLevelTask(t, tasksById) { return !isTopLevelTask(t, tasksById); } function isCrossEntityTask(t, tasksById) { if (isTopLevelTask(t, tasksById)) return false; return t.submittedByTask && t.submittedByTask.metadata.entityId !== t.entityId; } function isNestedSameEntityTask(t, tasksById) { if (isTopLevelTask(t, tasksById)) return false; return t.submittedByTask && t.submittedByTask.metadata.entityId === t.entityId; } function filterWithId(tasks, tasksById, nextFilter) { if (!tasks) return tasks; return tasks.filter(t => nextFilter(t, tasksById)); } export function timeAgoFilter() { function timeAgo(input) { if (!input || input<=0) return "-"; return fromNow(input); } timeAgo.$stateful = true; return timeAgo; } export function dateFilter() { function date(input, args) { // if (!input || input<=0) return "-"; if (args==='short') { return moment(input).format('MMM D, yyyy @ HH:mm:ss'); } else if (args==='iso') { return moment(input).format('yyyy-MM-DD HH:mm:ss.SSS'); } else { return moment(input).format('MMM D, yyyy @ HH:mm:ss.SSS'); } } return date; } export function durationFilter() { return function (input) { return duration(input); } } function isTaskWithTag(task, tag) { if (!task.tags) { // console.log("Task without tags: ", task); return false; } return task.tags.indexOf(tag)>=0; } function filterForTasksWithTag(tag) { return (tasks) => tasks.filter(t => isTaskWithTag(t, tag)); } function tasksAfterGlobalFilters(inputs, globalFilters) { if (inputs) { Object.values(globalFilters || {}).filter(gf => !gf.include).forEach(gf => { inputs = gf.filter(inputs); }); } return inputs; } export function activityFilter($filter) { return function (activities, searchText) { if (activities && searchText && searchText.length > 0) { return $filter('filter')(activities, (value, index, array) => { return (value.displayName && value.displayName.indexOf(searchText) > -1) || (value.description && value.description.indexOf(searchText) > -1); }); } else { return activities; } }; }